diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml index 8667a8d81a..34e7e31515 100644 --- a/.github/workflows/plugin-tests.yml +++ b/.github/workflows/plugin-tests.yml @@ -34,7 +34,7 @@ jobs: run: | set -euo pipefail # Plugins that have an `npm test` script configured. - PLUGINS=(automations azure-devops-issue-provider caldav-calendar-provider clickup-issue-provider google-calendar-provider sync-md) + PLUGINS=(automations azure-devops-issue-provider caldav-calendar-provider clickup-issue-provider google-calendar-provider sync-md todoist-import) if [ "$EVENT_NAME" = "workflow_dispatch" ]; then CHANGED=$(printf '%s\n' "${PLUGINS[@]}") diff --git a/docs/plans/2026-07-09-todoist-import.md b/docs/plans/2026-07-09-todoist-import.md new file mode 100644 index 0000000000..bf0a523a40 --- /dev/null +++ b/docs/plans/2026-07-09-todoist-import.md @@ -0,0 +1,229 @@ +# Todoist → Super Productivity migration + +Status: **revised & verified against code** · 2026-07-09 + +Goal: let a Todoist user bring their **active** projects, tasks, sub-tasks, labels, +due dates and time estimates into Super Productivity in one pass, non-destructively, +without adding permanent weight to the core app. + +## Product framing + +- Migration runs **once per user** and is then dead weight. Per the manifesto (avoid + feature creep; new UI/settings are permanent costs), it lives at the edge, not in + core hot paths. +- It must be **additive**, never destructive — most people evaluating SP already have + some data. The existing `importCompleteBackup` path (wipes all state) is the wrong tool. +- It is a **one-time import**, not a live integration. The issue-provider framework + (`src/app/features/issue/`) is built for ongoing polling + remote-linked tasks and is + deliberately **not** used here. + +## Chosen approach + +**A bundled plugin does all the work; core gets only a launcher row.** + +- Front-end + parsing + mapping + preview UI = a **bundled plugin** + (`packages/plugin-dev/todoist-import/`, built into `src/assets/bundled-plugins/`). + Matches the maintainers' direction (Trello / Linear / ClickUp / Azure moved _out_ of + core into plugins); fully replaceable / community-extensible. +- Landing the data uses only existing plugin-API methods — **zero new core API**: + - `addProject` / `addTag` for containers, + - **`batchUpdateForProject`** for all task creation (see below — this was missed in + the first draft and changes the op-log story), + - `updateTask` follow-ups only for fields the batch op doesn't carry + (`dueDay`, `dueWithTime`, `tagIds`). +- The plugin ships `isSkipMenuEntry: true` — no permanent menu noise for a one-time + tool. **Discoverability** comes from a single launcher row in the Import/Export + settings screen (`src/app/imex/file-imex/`): `PluginService.activatePlugin(id, true)` + then navigate to the existing `plugins/:pluginId/index` route. Deliberately the + **in-memory** enable (plugin-management additionally persists via + `setPluginEnabled`): after a restart the importer is dormant again — zero standing + weight; relaunching from the same row re-activates it. + +### Key correction #1: use `batchUpdateForProject`, not per-task `addTask` + +Verified in `plugin-bridge.service.ts:1209` + `task-shared-meta-reducers/task-batch-update.reducer.ts`: + +- The batch API is **backed by a meta-reducer**: one dispatched chunk (≤ 50 ops, + `MAX_BATCH_OPERATIONS_SIZE`) = **one action = one op-log entry** (sync rule #3). + A 1000-task import ≈ 20 ops for structure instead of 1000+. +- It handles **parent references via temp IDs** (bridge pre-generates real IDs and + returns the mapping) and preserves creation order — root tasks land in + `project.taskIds` in array order (verified in + `validate-and-fix-data-consistency-after-batch-update.ts`). No reorder op needed for + freshly created projects. +- Per-task `addTask` would additionally **reverse ordering** (bridge hardcodes + `isAddToBottom: false`, i.e. prepend) — another reason the first draft's approach + was wrong. +- Batch create data carries `title / notes / isDone / parentId / timeEstimate` only; + `dueDay`, `dueWithTime` and `tagIds` are applied with one `updateTask` per task that + has them. Ops ≈ `ceil(tasks/50) per project + dated/labelled tasks` — a tolerable + one-time burst, and it removes the main driver for a v2 `importData` core primitive. +- **Hard constraints found in review** (the reducer enforces these silently): + - temp IDs **must** be `temp-`/`temp_`-prefixed — anything else leaves children with + dangling `parentId`s that the consistency pass **deletes** as orphans; + - a parent's create op must sit at a **lower index than its children's** — the bridge + chunks at 50 ops/action and the roots-first sort is per-chunk only; + - the plugin **chunks its own calls at ≤ 50 ops and awaits each** — every iframe call + is its own postMessage round-trip, so this keeps one dispatch per tick (sync rule + #6) even for 5k-task projects, where the bridge's internal `forEach` chunking would + dispatch 100 actions in one tick; + - **ordering alone is NOT enough across self-chunked calls** (caught by the + post-implementation multi-review): the bridge builds `createdTaskIds` **per call**, + so a child sent in a later call cannot resolve a `temp-` parent from an earlier + call — the executor must **rewrite already-created parents to their real IDs** + before sending each chunk (`resolveKnownParents` in `run-import.ts`; real IDs are + explicitly supported in `BatchTaskCreate.parentId`); + - the result is fire-and-forget (`success: true` always, `errors` never populated) — + the **post-import summary re-reads state** (`getTasks`) and compares landed vs + planned counts instead of trusting the return value. + +### Key correction #2: what the plugin API actually can't do (verified) + +1. **No `TaskRepeatCfg` creation** — `PluginTaskRepeatCfg` is read-only. v1 degrades: + keep next due date + append `Repeats: ` to notes. +2. **SP _does_ have a Section entity now** (`src/app/features/section/`) — the first + draft claimed it didn't. But there is **no plugin API / allowed action** to create + sections, so v1 still drops Todoist sections (task order within the project is + preserved; flagged as lossy). v2 candidate: expose section creation to plugins. +3. **No ProjectFolder creation API** (`Project.folderId` exists, but nothing creates + folders) — the first draft's `parent_id → folderId` mapping is unworkable. v1 + **flattens nested projects**; when two projects collide on title, the child is + disambiguated as `Parent / Child`. Flagged as lossy. +4. **Subtasks can't hold tags** (bridge forces `tagIds: []` for subtasks — SP model) + → labels on Todoist sub-tasks are dropped, flagged in the summary. + +### v2 (only if v1 validates demand) + +`addTaskRepeatCfg` and/or plugin-visible section/folder creation. Deferring is +deliberate (YAGNI): public plugin-API surface is hard to reverse. The bulk-import +primitive from the first draft is **no longer needed** — `batchUpdateForProject` +already folds structure into few ops. One more v2 candidate (from the perf review): +extend `BatchTaskUpdate` with `dueDay`/`dueWithTime`/`tagIds` — the per-task +`updateTask` follow-ups are the dominant import cost (one op each; O(k²) planner-day +scans at 5k dated tasks) and today there is no cheaper path. + +## Input source + +**API token only in v1.** The user pastes a Todoist personal token (Settings → +Integrations → Developer); the plugin makes an initial +`POST https://api.todoist.com/api/v1/sync` with `sync_token=*`, followed immediately +by an incremental request with the returned sync token. This applies changes that +arrived while Todoist prepared a potentially delayed full snapshot. Both requests use +`resource_types=["projects","items","sections","notes"]` (`notes` = task comments, +folded into SP task notes; the `labels` resource is deliberately NOT requested — +item labels arrive as names on the items themselves) via the gated `PluginAPI.request` +(`permissions:["http"]` + `allowedHosts:["api.todoist.com"]`). The old Sync **v9** +endpoint is deprecated — use unified **v1**. + +**Token privacy (hard rule):** the token lives in iframe memory for the session only — +never `persistDataSynced` (that syncs!), not even `setSecret`. Password-type input; UI +states "sent only to api.todoist.com, never stored". + +**CSV fallback: cut from v1 (YAGNI, folded review verdict).** It is a second parser + +fixture suite + multi-file UI for strictly worse fidelity (no labels, no comments, no +tz fidelity, one tedious export per project) — and its `DATE` column holds _localized +natural-language_ strings ("every day", "5 août") that cannot be parsed faithfully. +Named contingency for users who already closed their account; fast-follow, not v1. + +Completed-task history is out of scope for v1 (the sync endpoint returns only active +items by default — nothing extra to do). + +## Mapping + +| Todoist | → Super Productivity | Notes | +| --------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| project | Project | hierarchy **flattened** (no folder API, see correction #2); `Parent / Child` title only on collision | +| Inbox project (`inbox_project`) | Project `Inbox (Todoist)` | never merged into SP's own Inbox — additive & reviewable | +| section | — | **v1: ignore** (no plugin API to create SP sections); flagged lossy. Sibling order key = `(section.section_order, item.child_order)` — sync items arrive unordered! | +| label | Tag | item `labels` are **names** in unified v1; match existing SP tags by title (case-insensitive), else `addTag`; only labels actually used by imported top-level tasks (SP subtasks can't hold tags — dropped + counted; the plugin must enforce this itself, the host won't) | +| item content | task `title` | | +| item description | task `notes` | markdown passes through | +| comments (sync `notes`) | appended to task `notes` | same sync call; file attachments → keep the URL line, flag files as not imported | +| priority (API `4`=p1 … `1`=p4) | — | **inverted vs UI!** Opt-in, default **off**, top-level only; **never tag API priority 1** (p4 is Todoist's default on every task). Single control with two mappings: `p1`…`p3` **Tags**, or **Eisenhower** (reuses SP's `urgent`/`important` tags: p1→both, p2→important, p3→urgent — added post-review per #8882 feedback) | +| due (all-day, `YYYY-MM-DD`) | `dueDay` | via `updateTask` after batch create | +| due (floating, `YYYY-MM-DDTHH:MM:SS`) | `dueWithTime` (unix ms) | parse as **local** time | +| due (fixed-tz, trailing `Z`) | `dueWithTime` (unix ms) | parse as **UTC instant** — parsing as local shifts every fixed-time task | +| deadline | `dueDay` if no due; else `Deadline: ` appended to notes | nothing silently dropped | +| duration `minute` | `timeEstimate` (ms) | | +| duration `day` | — | **skip + count in summary** — fabricating 8h would corrupt time-tracking stats | +| sub-task (`parent_id`) | sub-task (2 levels) | SP nests **2 levels only** — depth ≥ 2 re-parents to the depth-0 ancestor in reading (DFS) order, demotion counted | +| assignee (`responsible_uid`) | — | imported like any task; "N tasks had collaborator assignees" in summary | +| recurring (`due.is_recurring` + `due.string`) | keep next due + append verbatim `Repeats: ` to notes | verbatim preserves `every!` (recur-from-completion) semantics; real `TaskRepeatCfg` only with v2 core work. Imported timed tasks get **no reminder** (`updateTask` bypasses `scheduleTaskWithTime`) — noted, acceptable: Todoist reminders aren't imported anyway | +| completed items | — | skip v1 | + +## Architecture / file layout (v1) + +``` +packages/plugin-dev/todoist-import/ + package.json # esbuild + jest, modeled on sync-md (no framework) + scripts/build.js # bundle ui/main.ts, INLINE bundle into index.html, copy manifest/icon/i18n + src/ + manifest.json # iFrame:true, isSkipMenuEntry:true, permissions incl. "http", + # allowedHosts:["api.todoist.com"], hooks:[] + plugin.js # stub (all logic lives in the iframe UI) + ui/index.html # minimal shell; built JS inlined (iframe uses srcdoc → + # the document must be fully self-contained, verified in + # plugin-iframe.util.ts) + ui/main.ts # wizard: token → preview (per-project checkboxes) → import → summary + parse/from-api.ts # unified-v1 sync JSON → normalized model (pure) + parse/normalized-model.ts + map/plan-import.ts # normalized model → batch ops + follow-up updates (pure) + map/run-import.ts # executes the plan via PluginAPI, per-project failure boundary + *.spec.ts # jest over fixtures: due shapes, depth-3 nesting, section order, + # labels, priority inversion, duration units, deadline, comments +``` + +UI wizard specifics (trust items from review): + +- **Preview = per-project checkboxes** with task/subtask counts; projects whose title + already exists in SP are flagged "already exists — possibly from a previous import" + and default **unchecked** (re-run safety without rollback machinery). Lossy items + listed up front (sections, demotions, day-durations, subtask labels, assignees). +- **Import runs project-by-project** (batch + follow-ups per project before the next), + so an abort leaves whole projects, and the summary can say "4/6 imported, failed at + 'Errands'". +- Post-import summary counts from re-read state, names everything dropped. + +- Register in `packages/plugin-dev/scripts/build-all.js` and + `src/app/plugins/plugin.service.ts` bundled list. +- Core touch (discoverability only): one launcher button in + `src/app/imex/file-imex/` + one `en.json` key. + +## Milestones (each with its check) + +- **M0 · Spike — ✅ DONE, verdict GREEN.** Token path viable on web, Electron, mobile + via gated `PluginAPI.request` (`plugin-bridge.service.ts:508`); app CSP is + `connect-src *`; Electron injects ACAO:\*. Todoist's unified API documentation says + all endpoints except the initial OAuth authorization endpoint support CORS for any + origin; retain one live web-build sanity check during M3. +- **M1 · Parse + normalize.** Sync-v1 JSON → normalized model. → _verify:_ jest + fixtures: parent chains incl. depth 3+, the three `due.date` shapes, deadline, + recurring strings, durations (minute/day), priority inversion, section ordering, + comments incl. attachments, Inbox, assignees. +- **M2 · Plan + create.** Pure op-builder (normalized model → project/tag creates, + ≤50-op batch chunks parent-before-child with `temp-` IDs, follow-up updates) + + executor. → _verify:_ unit tests on the op-builder; manual import of a fixture into + a scratch profile: counts, nesting, order, due dates. +- **M3 · UI + preview + summary.** Token input, per-project preview, import progress, + honest summary. → _verify:_ manual run web + Electron incl. full + incremental sync. +- **M4 · Discoverability + docs.** Launcher row in Import/Export + (`activatePlugin` + route to `plugins/todoist-import/index`); "Switch from Todoist" + docs page (search is how the day-they-quit-Todoist persona finds this — no + onboarding banners, per the manifesto). → _verify:_ new user completes an import + from a cold start. + +## Risks & open decisions + +- **Partial import on failure** — additive, not transactional; per-project execution + bounds the blast radius to whole projects and the summary names what landed. + Accepted for v1 (KISS) — no rollback machinery. +- **Archived-project re-runs** — `getAllProjects()` exposes active projects only, so + an archived prior import cannot be collision-flagged. Document the restore/delete + workaround; do not widen a permanent public plugin API solely for this importer. +- **Follow-up `updateTask` volume** — one per dated/labelled task; bounded by the + batch op for structure; acceptable one-time burst. Watch 5k+ item accounts. +- **Todoist API drift** — unified v1 is current (v9 deprecated); parser is defensive + (unknown fields ignored, missing fields defaulted) and covered by fixtures. +- **Decided during review:** CSV cut from v1 · priority→tag default off (and API + priority 1 never tagged) · duration `day` skipped, not 8h · Inbox → "Inbox + (Todoist)" · collision projects default-unchecked in preview. diff --git a/docs/wiki/2.00-How_To.md b/docs/wiki/2.00-How_To.md index 999a666665..2ffc0b0be3 100755 --- a/docs/wiki/2.00-How_To.md +++ b/docs/wiki/2.00-How_To.md @@ -20,3 +20,5 @@ Every How-To note in the wiki. For structure and how to write How-Tos, see [[0.0 - [[2.16-Set-Up-Development-Environment]] - [[2.17-Add-a-New-Issue-Integration]] - [[2.18-Contribute-Translations]] +- [[2.19-Sync-Proton-Drive-via-rclone]] +- [[2.20-Import-from-Todoist]] diff --git a/docs/wiki/2.20-Import-from-Todoist.md b/docs/wiki/2.20-Import-from-Todoist.md new file mode 100644 index 0000000000..6eeb1b7961 --- /dev/null +++ b/docs/wiki/2.20-Import-from-Todoist.md @@ -0,0 +1,55 @@ +# Import from Todoist + +Bring your active Todoist projects, tasks, sub-tasks, labels and due dates into Super Productivity in one pass. The import is **additive**: it only creates new projects, tags and tasks and never changes or removes existing Super Productivity data. + +## What you need + +- A Todoist account and its **API token**: in Todoist, open **Settings → Integrations → Developer** and copy the token. The token is sent only to `api.todoist.com` and is never stored. + +## Steps + +1. Open **Settings** (gear icon in the sidebar). +2. Open the **Sync & Backup** tab and find the **Import/Export** section. +3. Click **Import from Todoist**. +4. Paste your API token and click **Load preview**. +5. Review the preview: pick the projects to import (projects whose name already exists — for example from a previous run — are flagged and unchecked by default) and optionally choose how to carry over Todoist priorities (see [Priorities](#priorities) below). +6. Click **Import**. The summary lists what was created and everything that could not be carried over. + +## What is imported + +- Active projects (nested project hierarchy is flattened; the Todoist Inbox becomes a project named “Inbox (Todoist)”) +- Active tasks and sub-tasks (Super Productivity nests two levels; deeper sub-tasks become direct sub-tasks of their top-level task) +- Task descriptions and comments (into task notes; HTTP(S) comment file attachments keep their link) +- Labels used by imported tasks (as tags, on top-level tasks) +- Due dates, including times, and minute-based durations (as time estimates) +- Recurring tasks keep their next due date; the recurrence rule is added to the task notes (e.g. `Repeats: every 3 days`) + +## Priorities + +Todoist priorities are **off by default**. In the preview you can pick one of: + +- **p1–p3 tags** — adds a `p1`, `p2` or `p3` tag to each top-level task (Todoist's default p4 stays untagged). +- **Eisenhower matrix** — reuses Super Productivity's built-in `urgent` / `important` tags, so imported tasks appear in the Eisenhower Matrix board: p1 → urgent + important, p2 → important, p3 → urgent, p4 → neither. Because a single Todoist priority is being split across the two axes, this mapping is a sensible default rather than an exact translation. + +Priority tags are only added to top-level tasks — sub-tasks in Super Productivity can't hold tags. +The preview counts prioritized sub-tasks that will not receive the selected priority tags. + +## What is not imported + +- Completed tasks and task history +- Nested project hierarchy and sections (project and task order is preserved) +- Labels and mapped priorities on sub-tasks +- Reminders, full-day durations, collaborator assignees and attachment files +- Recurrence rules as real repeating tasks — recreate important ones via [[2.06-Manage-Repeating-Tasks]] + +Unusually long project names, task titles, labels and task notes are shortened to +safe import limits. The preview reports how many selected values are affected. + +If the import stops midway (e.g. connection loss), delete the project named in the +error because it may be incomplete. Then re-run the import and select that project +and any remaining projects. Projects already listed as imported are complete. To +undo an import, delete the created projects. + +The duplicate-title warning can check active Super Productivity projects only. If a +previously imported project was archived, restore or delete it before re-running the +import so that it can be detected. diff --git a/packages/plugin-dev/scripts/build-all.js b/packages/plugin-dev/scripts/build-all.js index 0e09f46071..08f9b73fff 100755 --- a/packages/plugin-dev/scripts/build-all.js +++ b/packages/plugin-dev/scripts/build-all.js @@ -393,6 +393,35 @@ const plugins = [ return 'Built and copied to assets'; }, }, + { + name: 'todoist-import', + path: 'todoist-import', + needsInstall: true, + copyToAssets: true, + buildCommand: async (pluginPath) => { + await execAsync(`cd ${pluginPath} && npm run build`); + const targetDir = path.join( + __dirname, + '../../../src/assets/bundled-plugins/todoist-import', + ); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + const distPath = path.join(pluginPath, 'dist'); + if (fs.existsSync(distPath)) { + const files = fs.readdirSync(distPath); + for (const file of files) { + copyRecursive(path.join(distPath, file), path.join(targetDir, file)); + } + } + assertFilesExist( + targetDir, + ['manifest.json', 'plugin.js', 'index.html', 'icon.svg', 'i18n/en.json'], + 'todoist-import', + ); + return 'Built and copied to assets'; + }, + }, ]; async function buildPlugin(plugin) { diff --git a/packages/plugin-dev/todoist-import/i18n/en.json b/packages/plugin-dev/todoist-import/i18n/en.json new file mode 100644 index 0000000000..406695ad9f --- /dev/null +++ b/packages/plugin-dev/todoist-import/i18n/en.json @@ -0,0 +1,76 @@ +{ + "TITLE": { + "IMPORT": "Import from Todoist", + "PREVIEW": "Preview", + "IMPORTING": "Importing…", + "FINISHED": "Import finished", + "INCOMPLETE": "Import incomplete" + }, + "TOKEN": { + "LABEL": "Todoist API token", + "PLACEHOLDER": "Paste your Todoist API token", + "INTRO": "Brings your active Todoist projects, tasks, sub-tasks, labels and due dates into Super Productivity. The import only adds data — nothing in Super Productivity is changed or removed.", + "HELP": "Find the token in Todoist under Settings → Integrations → Developer. It is sent only to api.todoist.com and never stored.", + "REQUIRED": "Please paste your Todoist API token first.", + "LOADING": "Loading your Todoist data…", + "NO_PROJECTS": "No active projects found for this Todoist account." + }, + "ERROR": { + "LOAD_FAILED": "Could not load data from Todoist: {{error}} — check the token and your connection.", + "IMPORT_FAILED": "Import failed: {{error}}", + "IMPORT_STOPPED": "Import stopped at “{{project}}”: {{error}}. That project was created only partially — delete “{{project}}” before re-running, then select it and the remaining projects again." + }, + "BUTTON": { + "LOAD_PREVIEW": "Load preview", + "IMPORT": "Import", + "BACK": "Back" + }, + "PREVIEW": { + "CHOOSE_PROJECTS": "Choose the projects to import:", + "PROJECT_COUNTS": "{{title}} — tasks: {{taskCount}}, sub-tasks: {{subTaskCount}}", + "ALREADY_EXISTS": "already exists — possibly imported before", + "PRIORITY_LEGEND": "Map Todoist priorities to:", + "PRIORITY_NONE": "Nothing", + "PRIORITY_TAGS": "p1–p3 tags (p4 stays untagged)", + "PRIORITY_EISENHOWER": "Eisenhower matrix — urgent / important tags", + "LOSS_HEADING": "What will not survive the move", + "SELECT_PROJECT": "Select at least one project to import." + }, + "LOSS": { + "NESTED_PROJECTS": "Nested projects flattened into the project list: {{count}}.", + "SECTIONS": "Sections dropped (tasks keep their order in the project): {{count}}.", + "DEMOTED_SUBTASKS": "Deeply nested sub-tasks made direct sub-tasks (2 levels max): {{count}}.", + "RECURRING": "Recurring tasks keeping only their next date (the rule is noted in task notes): {{count}}.", + "DAY_DURATIONS": "Full-day durations not imported: {{count}}.", + "SUBTASK_LABELS": "Sub-task labels not imported (sub-tasks have no tags): {{count}}.", + "SUBTASK_PRIORITIES": "Prioritized sub-tasks not receiving priority tags: {{count}}.", + "ASSIGNEES": "Task collaborator assignments dropped: {{count}}.", + "ATTACHMENTS": "Comment attachment links kept without their files: {{count}}.", + "TRUNCATED_FIELDS": "Unusually long values shortened to safe import limits: {{count}}.", + "COMPLETED_AND_REMINDERS": "Completed tasks and reminders are not imported." + }, + "IMPORT": { + "STARTING": "Starting…", + "PHASE_PROJECT": "creating project", + "PHASE_TASKS": "creating tasks", + "PHASE_DETAILS": "applying dates and tags {{current}}/{{total}}", + "PROGRESS": "Project {{current}} of {{total}}: {{title}} — {{detail}}" + }, + "SUMMARY": { + "PROJECT_RESULT": "{{title}}: {{landedTasks}} of {{plannedTasks}} tasks, {{landedSubTasks}} of {{plannedSubTasks}} sub-tasks", + "SHORTFALL": "some items did not land; please review", + "UNVERIFIED": "Could not verify the imported counts — the numbers above may show 0 even for tasks that landed.", + "CREATED_TAGS": "Created tags: {{tags}}", + "NOT_CARRIED_OVER": "Not carried over", + "SNACK_FINISHED": "Todoist import finished", + "UNDO": "The import is additive — to undo it, delete the created projects." + }, + "PARSE": { + "UNTITLED_PROJECT": "Untitled project", + "UNTITLED_TASK": "Untitled task", + "REPEATS": "Repeats: {{rule}}", + "DEADLINE": "Deadline: {{date}}", + "COMMENTS": "Comments:", + "FILE": "file" + } +} diff --git a/packages/plugin-dev/todoist-import/jest.config.cjs b/packages/plugin-dev/todoist-import/jest.config.cjs new file mode 100644 index 0000000000..98d8ac7256 --- /dev/null +++ b/packages/plugin-dev/todoist-import/jest.config.cjs @@ -0,0 +1,25 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + tsconfig: { + module: 'commonjs', + target: 'es2020', + esModuleInterop: true, + }, + }, + ], + }, + testMatch: ['**/*.spec.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + roots: ['/src'], + moduleNameMapper: { + '^@super-productivity/plugin-api$': + '/node_modules/@super-productivity/plugin-api/src/index.ts', + }, +}; diff --git a/packages/plugin-dev/todoist-import/package-lock.json b/packages/plugin-dev/todoist-import/package-lock.json new file mode 100644 index 0000000000..f8e3c25869 --- /dev/null +++ b/packages/plugin-dev/todoist-import/package-lock.json @@ -0,0 +1,5689 @@ +{ + "name": "todoist-import", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "todoist-import", + "version": "1.0.0", + "dependencies": { + "@super-productivity/plugin-api": "file:../../plugin-api" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^22.15.33", + "esbuild": "^0.28.1", + "jest": "^30.0.4", + "jest-environment-jsdom": "^30.0.4", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + } + }, + "../../plugin-api": { + "name": "@super-productivity/plugin-api", + "version": "1.0.1", + "license": "MIT", + "devDependencies": { + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", + "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.4.1", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.50", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.50.tgz", + "integrity": "sha512-ydBWw0G6WFwWHzh9RK4B5c690UkreOG0llq0r+DaI7LgKgxigf8mhHzIPI3S0850g1BPkq/zpuCfrq4QFgUlTQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@super-productivity/plugin-api": { + "resolved": "../../plugin-api", + "link": true + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.4.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.4.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.4.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", + "import-local": "^3.2.0", + "jest-cli": "30.4.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0", + "pretty-format": "30.4.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "parse-json": "^5.2.0", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "jest-util": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", + "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/environment-jsdom-abstract": "30.4.1", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.4.1", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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/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/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-jest": { + "version": "29.4.11", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", + "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.0", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/plugin-dev/todoist-import/package.json b/packages/plugin-dev/todoist-import/package.json new file mode 100644 index 0000000000..8cf9161596 --- /dev/null +++ b/packages/plugin-dev/todoist-import/package.json @@ -0,0 +1,23 @@ +{ + "name": "todoist-import", + "version": "1.0.0", + "description": "One-time import of active Todoist projects, tasks, labels and due dates into Super Productivity", + "private": true, + "scripts": { + "build": "node scripts/build.js", + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^22.15.33", + "esbuild": "^0.28.1", + "jest": "^30.0.4", + "jest-environment-jsdom": "^30.0.4", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "dependencies": { + "@super-productivity/plugin-api": "file:../../plugin-api" + } +} diff --git a/packages/plugin-dev/todoist-import/scripts/build.js b/packages/plugin-dev/todoist-import/scripts/build.js new file mode 100644 index 0000000000..23e3688a53 --- /dev/null +++ b/packages/plugin-dev/todoist-import/scripts/build.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { build } = require('esbuild'); + +const ROOT_DIR = path.join(__dirname, '..'); +const SRC_DIR = path.join(ROOT_DIR, 'src'); +const DIST_DIR = path.join(ROOT_DIR, 'dist'); + +const buildPlugin = async () => { + if (fs.existsSync(DIST_DIR)) { + fs.rmSync(DIST_DIR, { recursive: true }); + } + fs.mkdirSync(DIST_DIR); + + // Bundle the UI and inline it: the host loads index.html via iframe srcdoc, + // so the document must be fully self-contained (no relative script URLs). + const result = await build({ + entryPoints: [path.join(SRC_DIR, 'ui', 'main.ts')], + bundle: true, + write: false, + platform: 'browser', + target: 'es2020', + format: 'iife', + minify: true, + sourcemap: false, + logLevel: 'info', + }); + const bundle = result.outputFiles[0].text; + + const htmlTemplate = fs.readFileSync(path.join(SRC_DIR, 'ui', 'index.html'), 'utf8'); + const marker = ''; + if (!htmlTemplate.includes(marker)) { + throw new Error(`index.html is missing the ${marker} marker`); + } + // function replacer: a literal replacement string would corrupt the bundle + // if the minified JS ever contains `$&`/`$'`-style replacement patterns + const html = htmlTemplate.replace(marker, () => ``); + fs.writeFileSync(path.join(DIST_DIR, 'index.html'), html); + + for (const file of ['manifest.json', 'plugin.js', 'icon.svg']) { + fs.copyFileSync(path.join(SRC_DIR, file), path.join(DIST_DIR, file)); + } + fs.cpSync(path.join(ROOT_DIR, 'i18n'), path.join(DIST_DIR, 'i18n'), { + recursive: true, + }); + console.log('todoist-import build complete → dist/'); +}; + +buildPlugin().catch((err) => { + console.error('todoist-import build failed:', err); + process.exit(1); +}); diff --git a/packages/plugin-dev/todoist-import/src/icon.svg b/packages/plugin-dev/todoist-import/src/icon.svg new file mode 100644 index 0000000000..3edfaf8185 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/plugin-dev/todoist-import/src/manifest.json b/packages/plugin-dev/todoist-import/src/manifest.json new file mode 100644 index 0000000000..4cce2849ea --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/manifest.json @@ -0,0 +1,29 @@ +{ + "id": "todoist-import", + "manifestVersion": 1, + "name": "Todoist Import", + "version": "1.0.0", + "minSupVersion": "18.0.0", + "description": "One-time import of your active Todoist projects, tasks, sub-tasks, labels and due dates. Additive — your existing Super Productivity data is never touched.", + "author": "SuperProductivity", + "homepage": "https://github.com/super-productivity/super-productivity", + "hooks": [], + "permissions": [ + "http", + "getTasks", + "getAllProjects", + "addProject", + "getAllTags", + "addTag", + "batchUpdateForProject", + "updateTask", + "showSnack" + ], + "allowedHosts": ["api.todoist.com"], + "iFrame": true, + "isSkipMenuEntry": true, + "i18n": { + "languages": ["en"] + }, + "icon": "icon.svg" +} diff --git a/packages/plugin-dev/todoist-import/src/map/plan-import.spec.ts b/packages/plugin-dev/todoist-import/src/map/plan-import.spec.ts new file mode 100644 index 0000000000..cb3f0b4062 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/map/plan-import.spec.ts @@ -0,0 +1,308 @@ +import { BatchTaskCreate } from '@super-productivity/plugin-api'; +import { TodoistImportModel, TodoistTask } from '../parse/normalized-model'; +import { BATCH_CHUNK_SIZE, planImport } from './plan-import'; + +const task = (overrides: Partial): TodoistTask => ({ + extId: 't1', + projectExtId: 'p1', + parentExtId: null, + title: 'task', + notes: '', + labels: [], + apiPriority: 1, + dueDay: null, + dueWithTime: null, + timeEstimate: null, + isRecurring: false, + wasDemoted: false, + isDayDurationSkipped: false, + hasAssignee: false, + attachmentCount: 0, + ...overrides, +}); + +const model = (overrides: Partial = {}): TodoistImportModel => ({ + projects: [ + { extId: 'p1', title: 'Work', parentExtId: null, isInbox: false, childOrder: 1 }, + ], + sections: [], + tasks: [], + ...overrides, +}); + +describe('planImport', () => { + it('creates temp- prefixed IDs and parent references', () => { + const plan = planImport( + model({ + tasks: [ + task({ extId: 'a' }), + task({ extId: 'b', parentExtId: 'a', title: 'sub' }), + ], + }), + { priorityMapping: 'none' }, + ); + const ops = plan.projects[0].batchChunks[0] as BatchTaskCreate[]; + expect(ops.map((o) => [o.tempId, o.data.parentId])).toEqual([ + ['temp-a', undefined], + ['temp-b', 'temp-a'], + ]); + }); + + it('chunks operations at the batch limit, keeping parents before children', () => { + const tasks: TodoistTask[] = []; + for (let i = 0; i < 60; i++) { + tasks.push(task({ extId: `root-${i}`, title: `t${i}` })); + tasks.push(task({ extId: `sub-${i}`, parentExtId: `root-${i}`, title: `s${i}` })); + } + const plan = planImport(model({ tasks }), { priorityMapping: 'none' }); + const chunks = plan.projects[0].batchChunks; + expect(chunks.length).toBe(Math.ceil(120 / BATCH_CHUNK_SIZE)); + chunks.forEach((chunk) => expect(chunk.length).toBeLessThanOrEqual(BATCH_CHUNK_SIZE)); + // a parent is always at a lower global index than its child + const flat = chunks.flat() as BatchTaskCreate[]; + const indexByTempId = new Map(flat.map((op, i) => [op.tempId, i])); + for (const op of flat) { + if (op.data.parentId) { + expect(indexByTempId.get(op.data.parentId)).toBeLessThan( + indexByTempId.get(op.tempId) as number, + ); + } + } + }); + + it('emits follow-ups only for tasks that need them, with due exclusivity', () => { + const plan = planImport( + model({ + tasks: [ + task({ extId: 'plain' }), + task({ extId: 'dated', dueDay: '2026-07-15' }), + task({ extId: 'timed', dueWithTime: 123456, dueDay: null }), + ], + }), + { priorityMapping: 'none' }, + ); + expect(plan.projects[0].followUps).toEqual([ + { tempId: 'temp-dated', dueDay: '2026-07-15' }, + { tempId: 'temp-timed', dueWithTime: 123456 }, + ]); + }); + + it('collects label tags for root tasks but never for sub-tasks', () => { + const plan = planImport( + model({ + tasks: [ + task({ extId: 'a', labels: ['errand'] }), + task({ extId: 'b', parentExtId: 'a', labels: ['dropped'] }), + ], + }), + { priorityMapping: 'none' }, + ); + expect(plan.tagTitles).toEqual(['errand']); + expect(plan.projects[0].followUps).toEqual([ + { tempId: 'temp-a', tagTitles: ['errand'] }, + ]); + }); + + describe('priority tags (opt-in)', () => { + it('maps API 4→p1, 3→p2, 2→p3 and never tags the default priority 1', () => { + const plan = planImport( + model({ + tasks: [ + task({ extId: 'highest', apiPriority: 4 }), + task({ extId: 'mid', apiPriority: 3 }), + task({ extId: 'low', apiPriority: 2 }), + task({ extId: 'default', apiPriority: 1 }), + ], + }), + { priorityMapping: 'priorityTags' }, + ); + expect(plan.tagTitles.sort()).toEqual(['p1', 'p2', 'p3']); + expect(plan.projects[0].followUps).toEqual([ + { tempId: 'temp-highest', tagTitles: ['p1'] }, + { tempId: 'temp-mid', tagTitles: ['p2'] }, + { tempId: 'temp-low', tagTitles: ['p3'] }, + ]); + }); + + it('is off by default', () => { + const plan = planImport(model({ tasks: [task({ extId: 'a', apiPriority: 4 })] }), { + priorityMapping: 'none', + }); + expect(plan.tagTitles).toEqual([]); + }); + }); + + describe('eisenhower priority mapping (opt-in)', () => { + it('maps 4→urgent+important, 3→important, 2→urgent, leaving default 1 untagged', () => { + const plan = planImport( + model({ + tasks: [ + task({ extId: 'p1', apiPriority: 4 }), + task({ extId: 'p2', apiPriority: 3 }), + task({ extId: 'p3', apiPriority: 2 }), + task({ extId: 'p4', apiPriority: 1 }), + ], + }), + { priorityMapping: 'eisenhower' }, + ); + // reused by title → lands in SP's existing EM_URGENT / EM_IMPORTANT tags + expect(plan.tagTitles.sort()).toEqual(['important', 'urgent']); + expect(plan.projects[0].followUps).toEqual([ + { tempId: 'temp-p1', tagTitles: ['urgent', 'important'] }, + { tempId: 'temp-p2', tagTitles: ['important'] }, + { tempId: 'temp-p3', tagTitles: ['urgent'] }, + ]); + }); + + it('merges the matrix tags after existing labels and never tags sub-tasks', () => { + const plan = planImport( + model({ + tasks: [ + task({ extId: 'root', apiPriority: 4, labels: ['errand'] }), + task({ extId: 'sub', parentExtId: 'root', apiPriority: 4, labels: ['x'] }), + ], + }), + { priorityMapping: 'eisenhower' }, + ); + expect(plan.projects[0].followUps).toEqual([ + { tempId: 'temp-root', tagTitles: ['errand', 'urgent', 'important'] }, + ]); + }); + + it('does not duplicate matrix tags already present as Todoist labels', () => { + const plan = planImport( + model({ + tasks: [ + task({ + extId: 'root', + apiPriority: 4, + labels: ['Urgent', 'important'], + }), + ], + }), + { priorityMapping: 'eisenhower' }, + ); + + expect(plan.projects[0].followUps).toEqual([ + { tempId: 'temp-root', tagTitles: ['Urgent', 'important'] }, + ]); + }); + }); + + describe('project selection and titles', () => { + it('only plans selected projects', () => { + const plan = planImport( + model({ + projects: [ + { extId: 'p1', title: 'A', parentExtId: null, isInbox: false, childOrder: 1 }, + { extId: 'p2', title: 'B', parentExtId: null, isInbox: false, childOrder: 2 }, + ], + tasks: [task({ extId: 'a', projectExtId: 'p2' })], + }), + { priorityMapping: 'none', selectedProjectExtIds: new Set(['p2']) }, + ); + expect(plan.projects.map((p) => p.extId)).toEqual(['p2']); + }); + + it('renames the inbox and disambiguates colliding nested titles', () => { + const plan = planImport( + model({ + projects: [ + { + extId: 'inbox', + title: 'Inbox', + parentExtId: null, + isInbox: true, + childOrder: 0, + }, + { + extId: 'work', + title: 'Work', + parentExtId: null, + isInbox: false, + childOrder: 1, + }, + { + extId: 'misc1', + title: 'Misc', + parentExtId: 'work', + isInbox: false, + childOrder: 2, + }, + { + extId: 'home', + title: 'Home', + parentExtId: null, + isInbox: false, + childOrder: 3, + }, + { + extId: 'misc2', + title: 'Misc', + parentExtId: 'home', + isInbox: false, + childOrder: 4, + }, + ], + }), + { priorityMapping: 'none' }, + ); + expect(plan.projects.map((p) => p.title)).toEqual([ + 'Inbox (Todoist)', + 'Work', + 'Work / Misc', + 'Home', + 'Home / Misc', + ]); + }); + + it('suffixes titles that still collide after prefixing', () => { + const plan = planImport( + model({ + projects: [ + { extId: 'a', title: 'X', parentExtId: null, isInbox: false, childOrder: 1 }, + { extId: 'b', title: 'X', parentExtId: null, isInbox: false, childOrder: 2 }, + ], + }), + { priorityMapping: 'none' }, + ); + expect(plan.projects.map((p) => p.title)).toEqual(['X', 'X (2)']); + }); + + it('never generates a suffix that collides with another project title', () => { + const plan = planImport( + model({ + projects: [ + { extId: 'a', title: 'X', parentExtId: null, isInbox: false, childOrder: 1 }, + { extId: 'b', title: 'X', parentExtId: null, isInbox: false, childOrder: 2 }, + { + extId: 'c', + title: 'X (2)', + parentExtId: null, + isInbox: false, + childOrder: 3, + }, + ], + }), + { priorityMapping: 'none' }, + ); + + expect(plan.projects.map((p) => p.title)).toEqual(['X', 'X (3)', 'X (2)']); + }); + }); + + it('reports task and sub-task counts per project', () => { + const plan = planImport( + model({ + tasks: [ + task({ extId: 'a' }), + task({ extId: 'b', parentExtId: 'a' }), + task({ extId: 'c' }), + ], + }), + { priorityMapping: 'none' }, + ); + expect(plan.projects[0].taskCount).toBe(2); + expect(plan.projects[0].subTaskCount).toBe(1); + }); +}); diff --git a/packages/plugin-dev/todoist-import/src/map/plan-import.ts b/packages/plugin-dev/todoist-import/src/map/plan-import.ts new file mode 100644 index 0000000000..79372730ca --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/map/plan-import.ts @@ -0,0 +1,232 @@ +import { BatchOperation } from '@super-productivity/plugin-api'; +import { TodoistImportModel, TodoistTask } from '../parse/normalized-model'; + +/** + * Batch chunk size. Must stay ≤ the host's MAX_BATCH_OPERATIONS_SIZE (50): + * the plugin chunks its own `batchUpdateForProject` calls and awaits each one, + * so every call is a single dispatched action in its own tick (sync rule #6); + * the bridge's internal chunking would fire all chunks in one tick. + */ +export const BATCH_CHUNK_SIZE = 50; + +/** Temp IDs MUST be `temp-`-prefixed — the batch reducer only resolves parent + * references with this prefix; anything else orphans (= deletes) sub-tasks. */ +const tempId = (extId: string): string => `temp-${extId}`; + +/** Todoist API priority (4 = highest) → SP tag title. API 1 = the default on + * every task and is deliberately never tagged. */ +const PRIORITY_TAG_BY_API_VALUE: Record = { + 4: 'p1', + 3: 'p2', + 2: 'p3', +}; + +/** + * Opt-in alternative to the p1–p3 tags: map Todoist's single priority axis onto + * Super Productivity's built-in Eisenhower-matrix tags. The two tags are reused + * by title (`ensureTags` matches case-insensitively), so imported tasks land in + * the existing `EM_URGENT`/`EM_IMPORTANT` quadrants instead of spawning new + * tags. Collapsing one axis onto the 2-D matrix is inherently opinionated; this + * is the conventional split and only ever applies when the user explicitly + * chooses it. API 1 (default p4) stays untagged. + */ +const EISENHOWER_TAGS_BY_API_VALUE: Record = { + 4: ['urgent', 'important'], + 3: ['important'], + 2: ['urgent'], +}; + +export interface TaskFollowUp { + tempId: string; + dueDay?: string; + dueWithTime?: number; + /** resolved to tag IDs at run time (existing tags are reused by title) */ + tagTitles?: string[]; +} + +export interface ProjectImportPlan { + extId: string; + title: string; + taskCount: number; + subTaskCount: number; + batchChunks: BatchOperation[][]; + followUps: TaskFollowUp[]; +} + +export interface ImportPlan { + projects: ProjectImportPlan[]; + /** all tag titles the import needs (used labels + opt-in priority tags) */ + tagTitles: string[]; +} + +/** + * How Todoist task priority is carried over — mutually exclusive (one UI + * control): `none` leaves priority off, `priorityTags` adds the p1–p3 tags, + * `eisenhower` adds SP's built-in urgent/important tags. + */ +export type PriorityMapping = 'none' | 'priorityTags' | 'eisenhower'; + +export interface PlanImportOptions { + priorityMapping: PriorityMapping; + /** omit to import everything */ + selectedProjectExtIds?: ReadonlySet; +} + +export const groupTasksByProject = ( + model: TodoistImportModel, +): Map => { + const byProject = new Map(); + for (const t of model.tasks) { + const list = byProject.get(t.projectExtId) || []; + list.push(t); + byProject.set(t.projectExtId, list); + } + return byProject; +}; + +const taskTagTitles = (task: TodoistTask, priorityMapping: PriorityMapping): string[] => { + // SP sub-tasks cannot hold tags (host model) — the plugin must enforce this + if (task.parentExtId) { + return []; + } + const titles = [...task.labels]; + if (priorityMapping === 'priorityTags') { + const priorityTag = PRIORITY_TAG_BY_API_VALUE[task.apiPriority]; + if (priorityTag) { + titles.push(priorityTag); + } + } else if (priorityMapping === 'eisenhower') { + const emTags = EISENHOWER_TAGS_BY_API_VALUE[task.apiPriority]; + if (emTags) { + titles.push(...emTags); + } + } + const seen = new Set(); + return titles.filter((title) => { + const key = title.toLowerCase(); + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +}; + +/** + * Flattened project titles must stay unique enough to review: nested projects + * keep their plain name unless it collides, then `Parent / Child`; remaining + * duplicates get a numeric suffix. The Todoist Inbox becomes `Inbox (Todoist)` + * so it never shadows SP's own Inbox. + * + * Exported so the preview shows (and collision-checks) exactly the titles the + * import will create. + */ +export const buildProjectTitles = (model: TodoistImportModel): Map => { + const byExtId = new Map(model.projects.map((p) => [p.extId, p])); + const preferredTitles = new Map(); + const counts = new Map(); + for (const p of model.projects) { + const base = p.isInbox ? 'Inbox (Todoist)' : p.title; + const key = base.toLowerCase(); + counts.set(key, (counts.get(key) || 0) + 1); + preferredTitles.set(p.extId, base); + } + for (const p of model.projects) { + let title = preferredTitles.get(p.extId) as string; + if ((counts.get(title.toLowerCase()) || 0) > 1 && p.parentExtId) { + const parent = byExtId.get(p.parentExtId); + if (parent) { + title = `${parent.title} / ${title}`; + } + } + preferredTitles.set(p.extId, title); + } + + const reserved = new Set( + [...preferredTitles.values()].map((title) => title.toLowerCase()), + ); + const used = new Set(); + const titles = new Map(); + for (const p of model.projects) { + const base = preferredTitles.get(p.extId) as string; + let title = base; + let suffix = 2; + while (used.has(title.toLowerCase())) { + do { + title = `${base} (${suffix++})`; + } while (reserved.has(title.toLowerCase()) || used.has(title.toLowerCase())); + } + used.add(title.toLowerCase()); + titles.set(p.extId, title); + } + return titles; +}; + +/** + * Normalized model → executable plan. Pure; unit-tested. Operations are + * ordered parent-before-child (guaranteed by the model's task order), which + * keeps chunk boundaries safe. + */ +export const planImport = ( + model: TodoistImportModel, + options: PlanImportOptions, +): ImportPlan => { + const titles = buildProjectTitles(model); + const tagTitles = new Set(); + const projects: ProjectImportPlan[] = []; + const tasksByProject = groupTasksByProject(model); + + for (const project of model.projects) { + if ( + options.selectedProjectExtIds && + !options.selectedProjectExtIds.has(project.extId) + ) { + continue; + } + const tasks = tasksByProject.get(project.extId) || []; + const operations: BatchOperation[] = tasks.map((t) => ({ + type: 'create', + tempId: tempId(t.extId), + data: { + title: t.title, + notes: t.notes || undefined, + parentId: t.parentExtId ? tempId(t.parentExtId) : undefined, + timeEstimate: t.timeEstimate ?? undefined, + }, + })); + + const batchChunks: BatchOperation[][] = []; + for (let i = 0; i < operations.length; i += BATCH_CHUNK_SIZE) { + batchChunks.push(operations.slice(i, i + BATCH_CHUNK_SIZE)); + } + + const followUps: TaskFollowUp[] = []; + for (const t of tasks) { + const followUp: TaskFollowUp = { tempId: tempId(t.extId) }; + if (t.dueDay) { + followUp.dueDay = t.dueDay; + } else if (t.dueWithTime) { + followUp.dueWithTime = t.dueWithTime; + } + const titlesForTask = taskTagTitles(t, options.priorityMapping); + if (titlesForTask.length) { + followUp.tagTitles = titlesForTask; + titlesForTask.forEach((title) => tagTitles.add(title)); + } + if (followUp.dueDay || followUp.dueWithTime || followUp.tagTitles) { + followUps.push(followUp); + } + } + + projects.push({ + extId: project.extId, + title: titles.get(project.extId) as string, + taskCount: tasks.filter((t) => !t.parentExtId).length, + subTaskCount: tasks.filter((t) => !!t.parentExtId).length, + batchChunks, + followUps, + }); + } + + return { projects, tagTitles: [...tagTitles] }; +}; diff --git a/packages/plugin-dev/todoist-import/src/map/run-import.spec.ts b/packages/plugin-dev/todoist-import/src/map/run-import.spec.ts new file mode 100644 index 0000000000..b58ce89013 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/map/run-import.spec.ts @@ -0,0 +1,234 @@ +import { + BatchTaskCreate, + BatchUpdateRequest, + Tag, + Task, +} from '@super-productivity/plugin-api'; +import { TodoistImportModel, TodoistTask } from '../parse/normalized-model'; +import { planImport } from './plan-import'; +import { runImport } from './run-import'; + +const task = (overrides: Partial): TodoistTask => ({ + extId: 't1', + projectExtId: 'p1', + parentExtId: null, + title: 'task', + notes: '', + labels: [], + apiPriority: 1, + dueDay: null, + dueWithTime: null, + timeEstimate: null, + isRecurring: false, + wasDemoted: false, + isDayDurationSkipped: false, + hasAssignee: false, + attachmentCount: 0, + ...overrides, +}); + +const model = (tasks: TodoistTask[]): TodoistImportModel => ({ + projects: [ + { extId: 'p1', title: 'Work', parentExtId: null, isInbox: false, childOrder: 1 }, + ], + sections: [], + tasks, +}); + +/** In-memory fake of the PluginAPI subset the executor uses. */ +const createFakeApi = ( + opts: { + existingTags?: Partial[]; + failNthBatch?: number; + failNthTag?: number; + failGetTasks?: boolean; + } = {}, +): { + api: Parameters[0]; + sentBatches: BatchUpdateRequest[]; + updates: { taskId: string; changes: Partial }[]; + createdTags: string[]; +} => { + const sentBatches: BatchUpdateRequest[] = []; + const updates: { taskId: string; changes: Partial }[] = []; + const createdTags: string[] = []; + const createdTasks: Task[] = []; + let idCounter = 0; + let batchCounter = 0; + let tagCounter = 0; + + const api: Parameters[0] = { + getAllTags: async () => (opts.existingTags || []) as Tag[], + addTag: async (tagData) => { + tagCounter++; + if (opts.failNthTag === tagCounter) { + throw new Error('tag failed'); + } + createdTags.push(tagData.title as string); + return `tag-${createdTags.length}`; + }, + addProject: async () => `project-${++idCounter}`, + batchUpdateForProject: async (request) => { + batchCounter++; + if (opts.failNthBatch === batchCounter) { + throw new Error('batch failed'); + } + sentBatches.push(request); + const createdTaskIds: Record = {}; + for (const op of request.operations) { + if (op.type === 'create') { + const realId = `real-${op.tempId}`; + createdTaskIds[op.tempId] = realId; + const parentId = op.data.parentId || null; + createdTasks.push({ + id: realId, + title: op.data.title, + projectId: request.projectId, + // mirror the host: an unresolved temp- parent would orphan-delete + parentId: parentId && parentId.startsWith('temp-') ? undefined : parentId, + tagIds: [], + subTaskIds: [], + timeEstimate: 0, + timeSpent: 0, + isDone: false, + created: 1, + } as Task); + } + } + return { success: true, createdTaskIds }; + }, + updateTask: async (taskId, changes) => { + updates.push({ taskId, changes }); + }, + getTasks: async () => { + if (opts.failGetTasks) { + throw new Error('getTasks failed'); + } + return createdTasks; + }, + }; + return { api, sentBatches, updates, createdTags }; +}; + +describe('runImport', () => { + it('rewrites temp- parent refs to real IDs when a family straddles a chunk boundary', async () => { + // 49 filler roots + 1 root at index 49 whose 3 subtasks land in chunk 2 + const tasks: TodoistTask[] = []; + for (let i = 0; i < 49; i++) { + tasks.push(task({ extId: `filler-${i}` })); + } + tasks.push(task({ extId: 'family-root' })); + for (let i = 0; i < 3; i++) { + tasks.push(task({ extId: `family-sub-${i}`, parentExtId: 'family-root' })); + } + const plan = planImport(model(tasks), { priorityMapping: 'none' }); + expect(plan.projects[0].batchChunks.length).toBe(2); + + const { api, sentBatches } = createFakeApi(); + const result = await runImport(api, plan, () => {}); + + // the second SENT chunk must reference the parent's REAL id, not temp- + const secondChunkCreates = sentBatches[1].operations as BatchTaskCreate[]; + for (const op of secondChunkCreates) { + expect(op.data.parentId).toBe('real-temp-family-root'); + } + // and nothing was lost: 53 planned, 53 landed + expect(result.imported[0].landedTaskCount).toBe(50); + expect(result.imported[0].landedSubTaskCount).toBe(3); + expect(result.errorMessage).toBeNull(); + }); + + it('keeps temp- parent refs within the same chunk (bridge resolves those)', async () => { + const plan = planImport( + model([task({ extId: 'a' }), task({ extId: 'b', parentExtId: 'a' })]), + { priorityMapping: 'none' }, + ); + const { api, sentBatches } = createFakeApi(); + await runImport(api, plan, () => {}); + const ops = sentBatches[0].operations as BatchTaskCreate[]; + expect(ops[1].data.parentId).toBe('temp-a'); + }); + + it('never maps a label onto the virtual TODAY tag', async () => { + const plan = planImport(model([task({ extId: 'a', labels: ['Today'] })]), { + priorityMapping: 'none', + }); + const { api, updates, createdTags } = createFakeApi({ + existingTags: [{ id: 'TODAY', title: 'Today' }], + }); + await runImport(api, plan, () => {}); + expect(createdTags).toEqual(['Today']); + expect(updates[0].changes.tagIds).toEqual(['tag-1']); + }); + + it('reuses existing tags by title, case-insensitively', async () => { + const plan = planImport(model([task({ extId: 'a', labels: ['Errand'] })]), { + priorityMapping: 'none', + }); + const { api, updates, createdTags } = createFakeApi({ + existingTags: [{ id: 'tag-existing', title: 'errand' }], + }); + await runImport(api, plan, () => {}); + expect(createdTags).toEqual([]); + expect(updates[0].changes.tagIds).toEqual(['tag-existing']); + }); + + it('reports tags created before a later tag creation fails', async () => { + const plan = planImport(model([task({ extId: 'a', labels: ['first', 'second'] })]), { + priorityMapping: 'none', + }); + const { api } = createFakeApi({ failNthTag: 2 }); + + const result = await runImport(api, plan, () => {}); + + expect(result.createdTagTitles).toEqual(['first']); + expect(result.errorMessage).toBe('tag failed'); + }); + + it('records the failed project as partial and keeps earlier projects', async () => { + const m: TodoistImportModel = { + projects: [ + { extId: 'p1', title: 'A', parentExtId: null, isInbox: false, childOrder: 1 }, + { extId: 'p2', title: 'B', parentExtId: null, isInbox: false, childOrder: 2 }, + ], + sections: [], + tasks: [ + task({ extId: 'a', projectExtId: 'p1' }), + task({ extId: 'b', projectExtId: 'p2' }), + ], + }; + const plan = planImport(m, { priorityMapping: 'none' }); + const { api } = createFakeApi({ failNthBatch: 2 }); + const result = await runImport(api, plan, () => {}); + expect(result.imported.map((p) => p.title)).toEqual(['A']); + expect(result.failedProjectTitle).toBe('B'); + expect(result.errorMessage).toBe('batch failed'); + }); + + it('does not let a failed recount mask a successful import', async () => { + const plan = planImport(model([task({ extId: 'a' })]), { + priorityMapping: 'none', + }); + const { api } = createFakeApi({ failGetTasks: true }); + const result = await runImport(api, plan, () => {}); + expect(result.errorMessage).toBeNull(); + expect(result.isCountUnverified).toBe(true); + expect(result.imported.length).toBe(1); + }); + + it('reports detail progress during follow-ups', async () => { + const tasks: TodoistTask[] = []; + for (let i = 0; i < 30; i++) { + tasks.push(task({ extId: `t-${i}`, dueDay: '2026-07-15' })); + } + const plan = planImport(model(tasks), { priorityMapping: 'none' }); + const { api } = createFakeApi(); + const detailReports: (number | undefined)[] = []; + await runImport(api, plan, (p) => { + if (p.phase === 'details') { + detailReports.push(p.detailIndex); + } + }); + expect(detailReports).toEqual([0, 25]); + }); +}); diff --git a/packages/plugin-dev/todoist-import/src/map/run-import.ts b/packages/plugin-dev/todoist-import/src/map/run-import.ts new file mode 100644 index 0000000000..6e1c058cc1 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/map/run-import.ts @@ -0,0 +1,225 @@ +import { BatchOperation, PluginAPI, Task } from '@super-productivity/plugin-api'; +import { ImportPlan, ProjectImportPlan } from './plan-import'; + +/** SP's virtual Today tag — must never land in task.tagIds (sync rule #5). */ +const TODAY_TAG_ID = 'TODAY'; + +/** Report follow-up progress in steps of this many tasks. */ +const DETAIL_PROGRESS_STEP = 25; + +export interface ImportProgress { + projectTitle: string; + projectIndex: number; + totalProjects: number; + phase: 'project' | 'tasks' | 'details'; + /** follow-up progress within the 'details' phase */ + detailIndex?: number; + detailTotal?: number; +} + +export interface ImportedProjectResult { + title: string; + projectId: string; + /** planned counts, for an honest landed-vs-planned comparison */ + plannedTaskCount: number; + plannedSubTaskCount: number; + /** counted from re-read state — the batch API is fire-and-forget */ + landedTaskCount: number; + landedSubTaskCount: number; +} + +export interface ImportResult { + imported: ImportedProjectResult[]; + createdTagTitles: string[]; + /** set when the import aborted mid-way; that project exists PARTIALLY — + * the user should delete it before re-running */ + failedProjectTitle: string | null; + errorMessage: string | null; + /** the post-import recount failed; landed counts are unknown, not zero */ + isCountUnverified: boolean; +} + +type ImportApi = Pick< + PluginAPI, + | 'getAllTags' + | 'addTag' + | 'addProject' + | 'batchUpdateForProject' + | 'updateTask' + | 'getTasks' +>; + +const ensureTags = async ( + api: ImportApi, + tagTitles: string[], + createdTagTitles: string[], +): Promise> => { + const tagIdByTitle = new Map(); + const existing = await api.getAllTags(); + for (const tag of existing) { + // never map a label onto the virtual Today tag — a Todoist label named + // "Today" gets a real tag of its own instead + if (tag.id === TODAY_TAG_ID) { + continue; + } + tagIdByTitle.set(tag.title.toLowerCase(), tag.id); + } + for (const title of tagTitles) { + const key = title.toLowerCase(); + if (!tagIdByTitle.has(key)) { + tagIdByTitle.set(key, await api.addTag({ title })); + createdTagTitles.push(title); + } + } + return tagIdByTitle; +}; + +/** + * The bridge builds its temp-ID map PER CALL — a chunk sent in a later call + * cannot resolve a `temp-` parent created by an earlier call (the reducer + * would store the dangling ref and the consistency pass would orphan-DELETE + * the child). Real task IDs are supported in `parentId`, so rewrite every + * already-known temp parent to its real ID before sending. + */ +const resolveKnownParents = ( + chunk: BatchOperation[], + idByTempId: Record, +): BatchOperation[] => + chunk.map((op) => + op.type === 'create' && op.data.parentId && idByTempId[op.data.parentId] + ? { ...op, data: { ...op.data, parentId: idByTempId[op.data.parentId] } } + : op, + ); + +const importProject = async ( + api: ImportApi, + projectPlan: ProjectImportPlan, + tagIdByTitle: Map, + onProgress: ( + detail: Partial & { phase: ImportProgress['phase'] }, + ) => void, +): Promise => { + onProgress({ phase: 'project' }); + const projectId = await api.addProject({ title: projectPlan.title }); + + onProgress({ phase: 'tasks' }); + const idByTempId: Record = {}; + for (const chunk of projectPlan.batchChunks) { + // sequential awaits: one dispatched action per tick (sync rule #6) + const result = await api.batchUpdateForProject({ + projectId, + operations: resolveKnownParents(chunk, idByTempId), + }); + Object.assign(idByTempId, result.createdTaskIds); + } + + const followUps = projectPlan.followUps; + for (let i = 0; i < followUps.length; i++) { + if (i % DETAIL_PROGRESS_STEP === 0) { + onProgress({ phase: 'details', detailIndex: i, detailTotal: followUps.length }); + } + const followUp = followUps[i]; + const taskId = idByTempId[followUp.tempId]; + if (!taskId) { + continue; + } + const updates: Partial = {}; + if (followUp.dueDay) { + updates.dueDay = followUp.dueDay; + } else if (followUp.dueWithTime) { + updates.dueWithTime = followUp.dueWithTime; + } + if (followUp.tagTitles?.length) { + const tagIds = followUp.tagTitles + .map((t) => tagIdByTitle.get(t.toLowerCase())) + .filter((id): id is string => !!id); + if (tagIds.length) { + updates.tagIds = tagIds; + } + } + if (Object.keys(updates).length) { + await api.updateTask(taskId, updates); + } + } + return projectId; +}; + +const countLanded = async (api: ImportApi, result: ImportResult): Promise => { + const allTasks = await api.getTasks(); + const rootsByProject = new Map(); + const subsByProject = new Map(); + for (const task of allTasks) { + if (!task.projectId) { + continue; + } + const target = task.parentId ? subsByProject : rootsByProject; + target.set(task.projectId, (target.get(task.projectId) || 0) + 1); + } + for (const imported of result.imported) { + imported.landedTaskCount = rootsByProject.get(imported.projectId) || 0; + imported.landedSubTaskCount = subsByProject.get(imported.projectId) || 0; + } +}; + +/** + * Executes the plan project-by-project so an abort leaves at most one partial + * project (named in the result), and counts what actually landed by re-reading + * state (`batchUpdateForProject` always reports success and silently skips + * invalid operations). + */ +export const runImport = async ( + api: ImportApi, + plan: ImportPlan, + onProgress: (progress: ImportProgress) => void, +): Promise => { + const result: ImportResult = { + imported: [], + createdTagTitles: [], + failedProjectTitle: null, + errorMessage: null, + isCountUnverified: false, + }; + + try { + const tagIdByTitle = await ensureTags(api, plan.tagTitles, result.createdTagTitles); + + for (let i = 0; i < plan.projects.length; i++) { + const projectPlan = plan.projects[i]; + const report: Parameters[3] = (detail) => + onProgress({ + projectTitle: projectPlan.title, + projectIndex: i, + totalProjects: plan.projects.length, + ...detail, + }); + try { + const projectId = await importProject(api, projectPlan, tagIdByTitle, report); + result.imported.push({ + title: projectPlan.title, + projectId, + plannedTaskCount: projectPlan.taskCount, + plannedSubTaskCount: projectPlan.subTaskCount, + landedTaskCount: 0, + landedSubTaskCount: 0, + }); + } catch (e) { + result.failedProjectTitle = projectPlan.title; + result.errorMessage = e instanceof Error ? e.message : String(e); + break; + } + } + } catch (e) { + result.errorMessage = e instanceof Error ? e.message : String(e); + } + + if (result.imported.length) { + // a failed recount must not mask a successful import (or the real error) + try { + await countLanded(api, result); + } catch { + result.isCountUnverified = true; + } + } + + return result; +}; diff --git a/packages/plugin-dev/todoist-import/src/parse/from-api.spec.ts b/packages/plugin-dev/todoist-import/src/parse/from-api.spec.ts new file mode 100644 index 0000000000..1c5ea36cfb --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/parse/from-api.spec.ts @@ -0,0 +1,463 @@ +import { mergeSyncResponses, parseSyncResponse, RawSyncResponse } from './from-api'; + +const baseFixture = (): RawSyncResponse => ({ + projects: [ + { id: 'p1', name: 'Work', child_order: 1 }, + { id: 'p2', name: 'Home', child_order: 2 }, + ], + items: [], + sections: [], + notes: [], +}); + +describe('mergeSyncResponses', () => { + it('applies incremental additions, replacements, and tombstones to a full sync', () => { + const full: RawSyncResponse = { + sync_token: 'full-token', + projects: [ + { id: 'p1', name: 'Old name' }, + { id: 'p2', name: 'Deleted later' }, + { id: 'p4', name: 'Unchanged project' }, + ], + items: [{ id: 't1', project_id: 'p1', content: 'Old task title' }], + sections: [], + notes: [], + }; + const incremental: RawSyncResponse = { + sync_token: 'incremental-token', + projects: [ + { id: 'p1', name: 'New name' }, + { id: 'p2', is_deleted: true }, + { id: 'p3', name: 'New project' }, + ], + items: [{ id: 't1', project_id: 'p1', content: 'New task title' }], + }; + + const merged = mergeSyncResponses(full, incremental); + + expect(merged.sync_token).toBe('incremental-token'); + expect(merged.projects).toEqual([ + ...incremental.projects!, + { id: 'p4', name: 'Unchanged project' }, + ]); + expect(merged.items).toEqual(incremental.items); + expect(parseSyncResponse(merged).projects.map((project) => project.extId)).toEqual([ + 'p1', + 'p3', + 'p4', + ]); + expect(parseSyncResponse(merged).tasks[0].title).toBe('New task title'); + }); +}); + +describe('parseSyncResponse', () => { + it('skips archived and deleted projects and their items', () => { + const raw = baseFixture(); + raw.projects!.push( + { id: 'p3', name: 'Old', is_archived: true }, + { id: 'p4', name: 'Gone', is_deleted: 1 }, + ); + raw.items = [ + { id: 't1', project_id: 'p3', content: 'in archived' }, + { id: 't2', project_id: 'p4', content: 'in deleted' }, + { id: 't3', project_id: 'p1', content: 'kept' }, + ]; + const model = parseSyncResponse(raw); + expect(model.projects.map((p) => p.extId)).toEqual(['p1', 'p2']); + expect(model.tasks.map((t) => t.extId)).toEqual(['t3']); + }); + + it('skips completed and deleted items', () => { + const raw = baseFixture(); + raw.items = [ + { id: 't1', project_id: 'p1', content: 'done', checked: true }, + { id: 't2', project_id: 'p1', content: 'deleted', is_deleted: true }, + { id: 't3', project_id: 'p1', content: 'open' }, + ]; + const model = parseSyncResponse(raw); + expect(model.tasks.map((t) => t.extId)).toEqual(['t3']); + }); + + it('marks the inbox project', () => { + const raw = baseFixture(); + raw.projects!.unshift({ + id: 'p0', + name: 'Inbox', + inbox_project: true, + child_order: 0, + }); + const model = parseSyncResponse(raw); + expect(model.projects[0]).toEqual( + expect.objectContaining({ extId: 'p0', isInbox: true }), + ); + }); + + describe('due dates', () => { + it('parses all-day dates as dueDay', () => { + const raw = baseFixture(); + raw.items = [ + { id: 't1', project_id: 'p1', content: 'a', due: { date: '2026-07-15' } }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.dueDay).toBe('2026-07-15'); + expect(task.dueWithTime).toBeNull(); + }); + + it('parses floating datetimes as local time', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'a', + due: { date: '2026-07-15T09:30:00' }, + }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.dueDay).toBeNull(); + expect(task.dueWithTime).toBe(new Date(2026, 6, 15, 9, 30, 0).getTime()); + }); + + it('parses fixed-timezone datetimes (trailing Z) as UTC instants', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'a', + due: { date: '2026-07-15T09:30:00Z', timezone: 'Europe/Berlin' }, + }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.dueWithTime).toBe(Date.UTC(2026, 6, 15, 9, 30, 0)); + }); + + it('uses the deadline as dueDay when there is no due date', () => { + const raw = baseFixture(); + raw.items = [ + { id: 't1', project_id: 'p1', content: 'a', deadline: { date: '2026-08-01' } }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.dueDay).toBe('2026-08-01'); + expect(task.notes).not.toContain('Deadline:'); + }); + + it('keeps the deadline as a notes line when a due date exists', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'a', + due: { date: '2026-07-15' }, + deadline: { date: '2026-08-01' }, + }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.dueDay).toBe('2026-07-15'); + expect(task.notes).toContain('Deadline: 2026-08-01'); + }); + }); + + describe('recurring', () => { + it('flags recurring tasks and appends the verbatim rule to notes', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'water plants', + description: 'living room only', + due: { date: '2026-07-15', is_recurring: true, string: 'every! 3 days' }, + }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.isRecurring).toBe(true); + expect(task.notes).toBe('living room only\n\nRepeats: every! 3 days'); + }); + }); + + describe('durations', () => { + it('converts minute durations to a ms time estimate', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'a', + duration: { amount: 45, unit: 'minute' }, + }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.timeEstimate).toBe(45 * 60_000); + expect(task.isDayDurationSkipped).toBe(false); + }); + + it('skips day durations and flags them', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'a', + duration: { amount: 1, unit: 'day' }, + }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.timeEstimate).toBeNull(); + expect(task.isDayDurationSkipped).toBe(true); + }); + }); + + describe('nesting', () => { + it('keeps level-1 sub-tasks under their parent', () => { + const raw = baseFixture(); + raw.items = [ + { id: 'root', project_id: 'p1', content: 'root', child_order: 1 }, + { id: 'sub', project_id: 'p1', content: 'sub', parent_id: 'root' }, + ]; + const model = parseSyncResponse(raw); + expect(model.tasks.map((t) => [t.extId, t.parentExtId, t.wasDemoted])).toEqual([ + ['root', null, false], + ['sub', 'root', false], + ]); + }); + + it('re-parents depth ≥ 2 to the root ancestor in DFS order and flags demotion', () => { + const raw = baseFixture(); + raw.items = [ + { id: 'a', project_id: 'p1', content: 'a', child_order: 1 }, + { id: 'b', project_id: 'p1', content: 'b', parent_id: 'a', child_order: 1 }, + { id: 'c', project_id: 'p1', content: 'c', parent_id: 'b', child_order: 1 }, + { id: 'd', project_id: 'p1', content: 'd', parent_id: 'c', child_order: 1 }, + { id: 'b2', project_id: 'p1', content: 'b2', parent_id: 'a', child_order: 2 }, + ]; + const model = parseSyncResponse(raw); + expect(model.tasks.map((t) => [t.extId, t.parentExtId, t.wasDemoted])).toEqual([ + ['a', null, false], + ['b', 'a', false], + ['c', 'a', true], + ['d', 'a', true], + ['b2', 'a', false], + ]); + }); + + it('treats items with a missing parent as roots', () => { + const raw = baseFixture(); + raw.items = [ + { id: 'orphan', project_id: 'p1', content: 'x', parent_id: 'completed-parent' }, + ]; + const model = parseSyncResponse(raw); + expect(model.tasks[0].parentExtId).toBeNull(); + }); + }); + + describe('ordering', () => { + it('orders roots by section order then child order; section-less first', () => { + const raw = baseFixture(); + raw.sections = [ + { id: 's2', project_id: 'p1', name: 'Later', section_order: 2 }, + { id: 's1', project_id: 'p1', name: 'Now', section_order: 1 }, + ]; + raw.items = [ + { id: 'in-s2', project_id: 'p1', section_id: 's2', content: 'x', child_order: 1 }, + { id: 'no-sec-2', project_id: 'p1', content: 'x', child_order: 2 }, + { id: 'in-s1', project_id: 'p1', section_id: 's1', content: 'x', child_order: 1 }, + { id: 'no-sec-1', project_id: 'p1', content: 'x', child_order: 1 }, + ]; + const model = parseSyncResponse(raw); + expect(model.tasks.map((t) => t.extId)).toEqual([ + 'no-sec-1', + 'no-sec-2', + 'in-s1', + 'in-s2', + ]); + }); + + it('groups tasks by project in project order', () => { + const raw = baseFixture(); + raw.items = [ + { id: 'h1', project_id: 'p2', content: 'x', child_order: 1 }, + { id: 'w1', project_id: 'p1', content: 'x', child_order: 1 }, + ]; + const model = parseSyncResponse(raw); + expect(model.tasks.map((t) => t.extId)).toEqual(['w1', 'h1']); + }); + }); + + describe('comments', () => { + it('appends comments to notes and counts attachments', () => { + const raw = baseFixture(); + raw.items = [{ id: 't1', project_id: 'p1', content: 'a', description: 'desc' }]; + raw.notes = [ + { id: 'n1', item_id: 't1', content: 'first comment' }, + { + id: 'n2', + item_id: 't1', + content: 'see file', + file_attachment: { file_name: 'report.pdf', file_url: 'https://x/report.pdf' }, + }, + { id: 'n3', item_id: 't1', content: 'deleted', is_deleted: true }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.notes).toBe( + 'desc\n\nComments:\n- first comment\n- see file report.pdf: https://x/report.pdf', + ); + expect(task.attachmentCount).toBe(1); + }); + + it('uses caller-provided strings for text added to imported notes', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: '', + due: { date: '2026-07-15', is_recurring: true, string: 'every day' }, + deadline: { date: '2026-08-01' }, + }, + ]; + raw.notes = [ + { + id: 'n1', + item_id: 't1', + content: 'attachment', + file_attachment: { file_url: 'https://x/file' }, + }, + ]; + + const [taskParsed] = parseSyncResponse(raw, { + untitledProject: 'Projekt ohne Titel', + untitledTask: 'Aufgabe ohne Titel', + repeats: (rule) => `Wiederholt: ${rule}`, + deadline: (date) => `Frist: ${date}`, + comments: 'Kommentare:', + file: 'Datei', + }).tasks; + + expect(taskParsed.title).toBe('Aufgabe ohne Titel'); + expect(taskParsed.notes).toBe( + 'Wiederholt: every day\nFrist: 2026-08-01\n\nKommentare:\n- attachment Datei: https://x/file', + ); + }); + }); + + describe('hostile / malformed payloads', () => { + it('rejects shape-valid but impossible calendar dates', () => { + const raw = baseFixture(); + raw.items = [ + { id: 't1', project_id: 'p1', content: 'a', due: { date: '2026-99-99' } }, + { id: 't2', project_id: 'p1', content: 'b', deadline: { date: '0000-00-00' } }, + ]; + const [t1, t2] = parseSyncResponse(raw).tasks; + expect(t1.dueDay).toBeNull(); + expect(t2.dueDay).toBeNull(); + }); + + it('treats a parent in another project as missing (child becomes root)', () => { + const raw = baseFixture(); + raw.items = [ + { id: 'other', project_id: 'p2', content: 'x' }, + { id: 'child', project_id: 'p1', content: 'y', parent_id: 'other' }, + ]; + const child = parseSyncResponse(raw).tasks.find((t) => t.extId === 'child'); + expect(child?.parentExtId).toBeNull(); + }); + + it('drops unsafe or malformed attachment URLs from notes', () => { + const raw = baseFixture(); + raw.items = [{ id: 't1', project_id: 'p1', content: 'a' }]; + raw.notes = [ + { + id: 'n1', + item_id: 't1', + content: 'evil', + file_attachment: { file_name: 'x', file_url: 'javascript:alert(1)' }, + }, + { + id: 'n2', + item_id: 't1', + content: 'malformed', + file_attachment: { + file_name: 'x', + file_url: 'https://files.example/x\n[bad](javascript:alert(1))', + }, + }, + ]; + const [taskParsed] = parseSyncResponse(raw).tasks; + expect(taskParsed.notes).toBe('Comments:\n- evil\n- malformed'); + expect(taskParsed.attachmentCount).toBe(0); + }); + + it('rejects non-finite or absurd durations', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'a', + duration: { amount: Infinity, unit: 'minute' }, + }, + { + id: 't2', + project_id: 'p1', + content: 'b', + duration: { amount: 9_999_999, unit: 'minute' }, + }, + ]; + const [t1, t2] = parseSyncResponse(raw).tasks; + expect(t1.timeEstimate).toBeNull(); + expect(t2.timeEstimate).toBeNull(); + }); + + it('clamps oversized titles and notes', () => { + const raw = baseFixture(); + raw.projects![0].name = 'p'.repeat(5000); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'x'.repeat(5000), + description: 'y'.repeat(100_000), + }, + ]; + const parsed = parseSyncResponse(raw); + const [taskParsed] = parsed.tasks; + expect(parsed.projects[0].truncatedFieldCount).toBe(1); + expect(taskParsed.title.length).toBeLessThanOrEqual(1001); + expect(taskParsed.notes.length).toBeLessThanOrEqual(50_001); + expect(taskParsed.truncatedFieldCount).toBe(2); + }); + }); + + it('orders nested projects parent-first (children directly after their parent)', () => { + const raw = baseFixture(); + // child_order is per-parent in Todoist: a flat sort would interleave + raw.projects = [ + { id: 'a', name: 'A', child_order: 1 }, + { id: 'b', name: 'B', child_order: 2 }, + { id: 'a1', name: 'A1', parent_id: 'a', child_order: 1 }, + { id: 'b1', name: 'B1', parent_id: 'b', child_order: 1 }, + ]; + const model = parseSyncResponse(raw); + expect(model.projects.map((p) => p.extId)).toEqual(['a', 'a1', 'b', 'b1']); + }); + + it('captures labels and assignee flag', () => { + const raw = baseFixture(); + raw.items = [ + { + id: 't1', + project_id: 'p1', + content: 'a', + labels: ['errand', 'urgent'], + responsible_uid: 'user-2', + priority: 4, + }, + ]; + const [task] = parseSyncResponse(raw).tasks; + expect(task.labels).toEqual(['errand', 'urgent']); + expect(task.hasAssignee).toBe(true); + expect(task.apiPriority).toBe(4); + }); +}); diff --git a/packages/plugin-dev/todoist-import/src/parse/from-api.ts b/packages/plugin-dev/todoist-import/src/parse/from-api.ts new file mode 100644 index 0000000000..7d70b4346a --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/parse/from-api.ts @@ -0,0 +1,482 @@ +import { + TodoistImportModel, + TodoistProject, + TodoistSection, + TodoistTask, +} from './normalized-model'; + +/** + * Raw shapes from `POST https://api.todoist.com/api/v1/sync` (unified v1). + * Only the fields we read; everything is optional so unknown/missing fields + * from API drift degrade gracefully instead of crashing the import. + */ +export interface RawSyncResponse { + sync_token?: string; + full_sync?: boolean; + full_sync_date_utc?: string; + projects?: RawProject[]; + items?: RawItem[]; + sections?: RawSection[]; + notes?: RawNote[]; +} + +interface RawProject { + id?: string | number; + name?: string; + parent_id?: string | number | null; + child_order?: number; + inbox_project?: boolean; + is_archived?: boolean | number; + is_deleted?: boolean | number; +} + +interface RawSection { + id?: string | number; + project_id?: string | number; + name?: string; + section_order?: number; + is_deleted?: boolean | number; + is_archived?: boolean | number; +} + +interface RawDue { + /** YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS (floating/local) | …Z (fixed, UTC instant) */ + date?: string; + timezone?: string | null; + is_recurring?: boolean; + string?: string; +} + +interface RawItem { + id?: string | number; + project_id?: string | number; + section_id?: string | number | null; + parent_id?: string | number | null; + content?: string; + description?: string; + /** 4 = UI p1 (highest) … 1 = default */ + priority?: number; + labels?: string[]; + due?: RawDue | null; + deadline?: { date?: string } | null; + duration?: { amount?: number; unit?: string } | null; + checked?: boolean | number; + is_deleted?: boolean | number; + child_order?: number; + responsible_uid?: string | number | null; +} + +interface RawNote { + id?: string | number; + item_id?: string | number; + content?: string; + is_deleted?: boolean | number; + file_attachment?: { file_name?: string; file_url?: string } | null; +} + +const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/; + +// Everything below lands in synced state that every client must replay — +// clamp remote-controlled sizes and reject nonsense dates instead of +// trusting the payload (shared-project collaborators control much of it). +const MAX_TITLE_LEN = 1000; +const MAX_LABEL_LEN = 200; +const MAX_NOTES_LEN = 50_000; +const MIN_DUE_MS = 0; // 1970 +const MAX_DUE_MS = 32_503_680_000_000; // year 3000 + +export interface ParseStrings { + untitledProject: string; + untitledTask: string; + repeats: (rule: string) => string; + deadline: (date: string) => string; + comments: string; + file: string; +} + +const DEFAULT_PARSE_STRINGS: ParseStrings = { + untitledProject: 'Untitled project', + untitledTask: 'Untitled task', + repeats: (rule) => `Repeats: ${rule}`, + deadline: (date) => `Deadline: ${date}`, + comments: 'Comments:', + file: 'file', +}; + +const asId = (v: string | number | null | undefined): string | null => + v === null || v === undefined || v === '' ? null : String(v); + +const mergeResources = ( + full: T[] | undefined, + incremental: T[] | undefined, +): T[] | undefined => { + if (!incremental?.length) { + return full; + } + const changedIds = new Set( + incremental.map((resource) => asId(resource.id)).filter((id): id is string => !!id), + ); + return [ + ...incremental, + ...(full || []).filter((resource) => { + const id = asId(resource.id); + return !id || !changedIds.has(id); + }), + ]; +}; + +/** + * Apply the delta returned by a Todoist incremental sync to its initial full + * snapshot. Incremental tombstones intentionally replace the old resource so + * the normal parser filters deletions instead of resurrecting stale data. + */ +export const mergeSyncResponses = ( + full: RawSyncResponse, + incremental: RawSyncResponse, +): RawSyncResponse => ({ + ...full, + ...incremental, + projects: mergeResources(full.projects, incremental.projects), + items: mergeResources(full.items, incremental.items), + sections: mergeResources(full.sections, incremental.sections), + notes: mergeResources(full.notes, incremental.notes), +}); + +const asStr = (v: unknown): string => (typeof v === 'string' ? v : ''); + +const isSupportedAttachmentUrl = (url: unknown): url is string => { + if (typeof url !== 'string' || /[\s\u0000-\u001f\u007f]/u.test(url)) { + return false; + } + try { + const protocol = new URL(url).protocol; + return protocol === 'http:' || protocol === 'https:'; + } catch { + return false; + } +}; + +const isTruthyFlag = (v: boolean | number | undefined): boolean => !!v; + +const clamp = (s: string, maxLen: number): string => + s.length > maxLen ? `${s.slice(0, maxLen)}…` : s; + +/** Shape AND calendar validity (rejects e.g. 2026-99-99). */ +const isValidDayStr = (s: unknown): s is string => { + if (typeof s !== 'string' || !DATE_ONLY_RE.test(s)) { + return false; + } + const d = new Date(`${s}T00:00:00Z`); + return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === s; +}; + +/** + * `new Date('YYYY-MM-DDTHH:MM:SS')` is local time per spec; a trailing `Z` + * makes it a UTC instant — exactly Todoist's floating vs fixed semantics. + */ +const parseDue = ( + due: RawDue | null | undefined, +): { dueDay: string | null; dueWithTime: number | null } => { + const date = due?.date; + if (!date || typeof date !== 'string') { + return { dueDay: null, dueWithTime: null }; + } + if (DATE_ONLY_RE.test(date)) { + return isValidDayStr(date) + ? { dueDay: date, dueWithTime: null } + : { dueDay: null, dueWithTime: null }; + } + const ts = new Date(date).getTime(); + return Number.isNaN(ts) || ts < MIN_DUE_MS || ts > MAX_DUE_MS + ? { dueDay: null, dueWithTime: null } + : { dueDay: null, dueWithTime: ts }; +}; + +const buildNotes = ( + item: RawItem, + comments: RawNote[], + deadlineNoted: string | null, + strings: ParseStrings, +): { notes: string; truncatedFieldCount: number } => { + const parts: string[] = []; + const description = asStr(item.description).trim(); + if (description) { + parts.push(description); + } + const extras: string[] = []; + if (item.due?.is_recurring && asStr(item.due.string)) { + extras.push(strings.repeats(asStr(item.due.string))); + } + if (deadlineNoted) { + extras.push(strings.deadline(deadlineNoted)); + } + if (extras.length) { + parts.push(extras.join('\n')); + } + if (comments.length) { + const lines = comments.map((c) => { + const content = asStr(c.content).trim(); + const att = c.file_attachment; + // scheme-filter remote URLs before they land in markdown-rendered notes + const url = asStr(att?.file_url); + const attLine = isSupportedAttachmentUrl(url) + ? ` ${asStr(att?.file_name) || strings.file}: ${url}` + : ''; + return `- ${content}${attLine}`.trimEnd(); + }); + parts.push(`${strings.comments}\n${lines.join('\n')}`); + } + const notes = parts.join('\n\n'); + return { + notes: clamp(notes, MAX_NOTES_LEN), + truncatedFieldCount: notes.length > MAX_NOTES_LEN ? 1 : 0, + }; +}; + +/** + * Sync payload → normalized model. Pure; safe to unit-test with fixtures. + * + * - completed (`checked`) and deleted items are skipped, as are archived / + * deleted projects (and their items), + * - the item tree is flattened to SP's 2 levels: any task deeper than one + * level is re-parented to its root ancestor in DFS (reading) order, + * - items whose parent is missing (completed/deleted) are treated as roots. + */ +export const parseSyncResponse = ( + raw: RawSyncResponse, + strings: ParseStrings = DEFAULT_PARSE_STRINGS, +): TodoistImportModel => { + const projects: TodoistProject[] = []; + const keptProjectIds = new Set(); + + for (const p of raw.projects || []) { + const extId = asId(p.id); + if (!extId || isTruthyFlag(p.is_archived) || isTruthyFlag(p.is_deleted)) { + continue; + } + const title = asStr(p.name).trim(); + keptProjectIds.add(extId); + projects.push({ + extId, + title: clamp(title, MAX_TITLE_LEN) || strings.untitledProject, + parentExtId: asId(p.parent_id), + isInbox: !!p.inbox_project, + childOrder: p.child_order ?? 0, + truncatedFieldCount: title.length > MAX_TITLE_LEN ? 1 : 0, + }); + } + // parent-first DFS: `child_order` is per-parent in Todoist, so a flat sort + // would interleave unrelated siblings; this keeps children next to their + // (flattened-away) parent in the SP sidebar + const orderedProjects: TodoistProject[] = []; + const projectChildren = new Map(); + for (const p of projects) { + const parentKey = + p.parentExtId && keptProjectIds.has(p.parentExtId) ? p.parentExtId : null; + const list = projectChildren.get(parentKey) || []; + list.push(p); + projectChildren.set(parentKey, list); + } + projectChildren.forEach((list) => list.sort((a, b) => a.childOrder - b.childOrder)); + const visitedProjects = new Set(); + const visitProject = (p: TodoistProject): void => { + if (visitedProjects.has(p.extId)) { + return; + } + visitedProjects.add(p.extId); + orderedProjects.push(p); + (projectChildren.get(p.extId) || []).forEach(visitProject); + }; + (projectChildren.get(null) || []).forEach(visitProject); + // a parent-cycle in a hostile payload would skip its members entirely — + // append anything unvisited so no project is silently lost + for (const p of projects) { + visitProject(p); + } + + const sections: TodoistSection[] = []; + const sectionOrderById = new Map(); + for (const s of raw.sections || []) { + const extId = asId(s.id); + const projectExtId = asId(s.project_id); + if ( + !extId || + !projectExtId || + !keptProjectIds.has(projectExtId) || + isTruthyFlag(s.is_deleted) || + isTruthyFlag(s.is_archived) + ) { + continue; + } + sectionOrderById.set(extId, s.section_order ?? 0); + sections.push({ extId, projectExtId, title: asStr(s.name).trim() }); + } + + const commentsByItemId = new Map(); + for (const n of raw.notes || []) { + const itemId = asId(n.item_id); + if (!itemId || isTruthyFlag(n.is_deleted)) { + continue; + } + const list = commentsByItemId.get(itemId) || []; + list.push(n); + commentsByItemId.set(itemId, list); + } + + const keptItems: RawItem[] = []; + const keptItemIds = new Set(); + for (const item of raw.items || []) { + const extId = asId(item.id); + const projectExtId = asId(item.project_id); + if ( + !extId || + !projectExtId || + !keptProjectIds.has(projectExtId) || + isTruthyFlag(item.checked) || + isTruthyFlag(item.is_deleted) + ) { + continue; + } + keptItems.push(item); + keptItemIds.add(extId); + } + + // parent missing (completed/deleted/filtered) OR in another project (a + // shape Todoist shouldn't produce — its temp ID could never resolve in this + // project's batch) → treat as root + const projectByItemId = new Map( + keptItems.map((i) => [asId(i.id) as string, asId(i.project_id) as string]), + ); + const childrenByParent = new Map(); + const roots: RawItem[] = []; + for (const item of keptItems) { + const parentId = asId(item.parent_id); + if ( + parentId && + keptItemIds.has(parentId) && + projectByItemId.get(parentId) === asId(item.project_id) + ) { + const list = childrenByParent.get(parentId) || []; + list.push(item); + childrenByParent.set(parentId, list); + } else { + roots.push(item); + } + } + childrenByParent.forEach((list) => + list.sort((a, b) => (a.child_order ?? 0) - (b.child_order ?? 0)), + ); + + const projectOrder = new Map(orderedProjects.map((p, i) => [p.extId, i])); + const rootSortKey = (item: RawItem): [number, number, number] => { + const sectionId = asId(item.section_id); + // section-less tasks come first in Todoist, like sections with order -1 + const sectionOrder = sectionId ? (sectionOrderById.get(sectionId) ?? 0) : -1; + return [ + projectOrder.get(asId(item.project_id) as string) ?? 0, + sectionOrder, + item.child_order ?? 0, + ]; + }; + roots.sort((a, b) => { + const [pa, sa, ca] = rootSortKey(a); + const [pb, sb, cb] = rootSortKey(b); + return pa - pb || sa - sb || ca - cb; + }); + + const toTask = ( + item: RawItem, + rootExtId: string | null, + depth: number, + ): TodoistTask => { + const extId = asId(item.id) as string; + const { dueDay: parsedDueDay, dueWithTime } = parseDue(item.due); + const deadlineDay = isValidDayStr(item.deadline?.date) ? item.deadline!.date! : null; + const hasDue = !!parsedDueDay || !!dueWithTime; + // deadline fills in as dueDay when there is no due date; otherwise it is + // preserved as a notes line so nothing is silently dropped + const dueDay = parsedDueDay ?? (!hasDue ? deadlineDay : null); + const deadlineNoted = hasDue && deadlineDay ? deadlineDay : null; + + const duration = item.duration; + // Number.isFinite guards a hostile `1e999` → Infinity, which would diverge + // across clients (Infinity JSON-serializes to null in the op-log) + const isMinuteDuration = + !!duration && + duration.unit === 'minute' && + Number.isFinite(duration.amount) && + (duration.amount as number) > 0 && + (duration.amount as number) <= 525_600; // 1 year in minutes + const isDayDurationSkipped = !!duration && duration.unit === 'day'; + + const title = asStr(item.content).trim(); + const labels = (item.labels || []).filter( + (label): label is string => typeof label === 'string' && label.trim() !== '', + ); + const builtNotes = buildNotes( + item, + commentsByItemId.get(extId) || [], + deadlineNoted, + strings, + ); + + return { + extId, + projectExtId: asId(item.project_id) as string, + parentExtId: rootExtId, + title: clamp(title, MAX_TITLE_LEN) || strings.untitledTask, + notes: builtNotes.notes, + labels: labels.map((label) => clamp(label, MAX_LABEL_LEN)), + apiPriority: item.priority ?? 1, + dueDay, + dueWithTime, + timeEstimate: isMinuteDuration ? (duration.amount as number) * 60_000 : null, + isRecurring: !!item.due?.is_recurring, + wasDemoted: depth >= 2, + isDayDurationSkipped, + hasAssignee: !!asId(item.responsible_uid), + attachmentCount: (commentsByItemId.get(extId) || []).filter((c) => + isSupportedAttachmentUrl(c.file_attachment?.file_url), + ).length, + truncatedFieldCount: + (title.length > MAX_TITLE_LEN ? 1 : 0) + + labels.filter((label) => label.length > MAX_LABEL_LEN).length + + builtNotes.truncatedFieldCount, + }; + }; + + const tasks: TodoistTask[] = []; + const emittedIds = new Set(); + const emitFamily = (root: RawItem): void => { + const rootExtId = asId(root.id) as string; + emittedIds.add(rootExtId); + tasks.push(toTask(root, null, 0)); + // all descendants become direct sub-tasks of the root, DFS keeps reading order + const stack = (childrenByParent.get(rootExtId) || []) + .map((item) => ({ item, depth: 1 })) + .reverse(); + while (stack.length) { + const { item, depth } = stack.pop() as { item: RawItem; depth: number }; + const itemId = asId(item.id) as string; + if (emittedIds.has(itemId)) { + continue; // parent-cycle guard in a hostile payload + } + emittedIds.add(itemId); + tasks.push(toTask(item, rootExtId, depth)); + const children = childrenByParent.get(itemId) || []; + for (let i = children.length - 1; i >= 0; i--) { + stack.push({ item: children[i], depth: depth + 1 }); + } + } + }; + roots.forEach(emitFamily); + // members of a parent-cycle have no root — emit them as roots so nothing + // in the payload is silently dropped + for (const item of keptItems) { + if (!emittedIds.has(asId(item.id) as string)) { + emitFamily(item); + } + } + + return { projects: orderedProjects, sections, tasks }; +}; diff --git a/packages/plugin-dev/todoist-import/src/parse/load-todoist-data.spec.ts b/packages/plugin-dev/todoist-import/src/parse/load-todoist-data.spec.ts new file mode 100644 index 0000000000..9c72372289 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/parse/load-todoist-data.spec.ts @@ -0,0 +1,53 @@ +import { PluginAPI } from '@super-productivity/plugin-api'; +import { loadTodoistData } from './load-todoist-data'; + +describe('loadTodoistData', () => { + it('follows the initial full sync with an incremental sync and merges the delta', async () => { + const request = jest + .fn() + .mockResolvedValueOnce({ + sync_token: 'full-token', + projects: [{ id: 'p1', name: 'Old name' }], + items: [], + sections: [], + notes: [], + }) + .mockResolvedValueOnce({ + sync_token: 'incremental-token', + projects: [ + { id: 'p1', name: 'New name' }, + { id: 'p2', name: 'New project' }, + ], + }); + + const result = await loadTodoistData( + { request } as unknown as Pick, + 'secret-token', + ); + + expect(request).toHaveBeenCalledTimes(2); + expect( + new URLSearchParams(request.mock.calls[0][1].body as string).get('sync_token'), + ).toBe('*'); + expect( + new URLSearchParams(request.mock.calls[1][1].body as string).get('sync_token'), + ).toBe('full-token'); + expect(result.sync_token).toBe('incremental-token'); + expect(result.projects?.map((project) => project.name)).toEqual([ + 'New name', + 'New project', + ]); + }); + + it('fails closed when Todoist omits the incremental sync token', async () => { + const request = jest.fn().mockResolvedValue({ projects: [] }); + + await expect( + loadTodoistData( + { request } as unknown as Pick, + 'secret-token', + ), + ).rejects.toThrow('sync token'); + expect(request).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/plugin-dev/todoist-import/src/parse/load-todoist-data.ts b/packages/plugin-dev/todoist-import/src/parse/load-todoist-data.ts new file mode 100644 index 0000000000..c6c9ab7e92 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/parse/load-todoist-data.ts @@ -0,0 +1,43 @@ +import { PluginAPI } from '@super-productivity/plugin-api'; +import { mergeSyncResponses, RawSyncResponse } from './from-api'; + +const SYNC_URL = 'https://api.todoist.com/api/v1/sync'; +const RESOURCE_TYPES = ['projects', 'items', 'sections', 'notes']; + +type TodoistRequestApi = Pick; + +const requestSync = ( + api: TodoistRequestApi, + token: string, + syncToken: string, +): Promise => + api.request(SYNC_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + sync_token: syncToken, + resource_types: JSON.stringify(RESOURCE_TYPES), + }).toString(), + }); + +/** + * Todoist can serve delayed full snapshots for large accounts. Always apply the + * immediately-following incremental delta before previewing data so recent + * creates, updates, and tombstones are represented in the import. + * + * @see https://developer.todoist.com/api/v1/#tag/Sync/Incremental-sync + */ +export const loadTodoistData = async ( + api: TodoistRequestApi, + token: string, +): Promise => { + const full = await requestSync(api, token, '*'); + if (!full.sync_token) { + throw new Error('Todoist full sync did not return an incremental sync token.'); + } + const incremental = await requestSync(api, token, full.sync_token); + return mergeSyncResponses(full, incremental); +}; diff --git a/packages/plugin-dev/todoist-import/src/parse/normalized-model.ts b/packages/plugin-dev/todoist-import/src/parse/normalized-model.ts new file mode 100644 index 0000000000..444b1d2966 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/parse/normalized-model.ts @@ -0,0 +1,60 @@ +/** + * Normalized intermediate shape between the Todoist sync payload and the + * Super Productivity import plan. Everything lossy is flagged per task so the + * preview/summary UI can honestly report what will be / was dropped. + */ + +export interface TodoistProject { + extId: string; + title: string; + parentExtId: string | null; + isInbox: boolean; + childOrder: number; + /** number of imported values shortened to the parser's safe limits */ + truncatedFieldCount?: number; +} + +export interface TodoistSection { + extId: string; + projectExtId: string; + title: string; +} + +export interface TodoistTask { + extId: string; + projectExtId: string; + /** null = root task; set = direct sub-task after depth-flattening (SP nests 2 levels) */ + parentExtId: string | null; + title: string; + notes: string; + labels: string[]; + /** Raw Todoist API value: 4 = UI p1 (highest) … 1 = default/none */ + apiPriority: number; + /** YYYY-MM-DD — mutually exclusive with dueWithTime */ + dueDay: string | null; + /** unix ms — mutually exclusive with dueDay */ + dueWithTime: number | null; + /** ms, from minute-based durations only */ + timeEstimate: number | null; + isRecurring: boolean; + /** original depth was ≥ 2 and the task was re-parented to its root ancestor */ + wasDemoted: boolean; + /** had a day-based duration that is deliberately not imported */ + isDayDurationSkipped: boolean; + hasAssignee: boolean; + /** comment file attachments (URLs kept in notes, files not imported) */ + attachmentCount: number; + /** number of imported values shortened to the parser's safe limits */ + truncatedFieldCount?: number; +} + +export interface TodoistImportModel { + projects: TodoistProject[]; + sections: TodoistSection[]; + /** + * In final creation order: per project, root tasks (sorted by section order, + * then child order) each immediately followed by their sub-tasks in DFS + * order — a parent always precedes its children. + */ + tasks: TodoistTask[]; +} diff --git a/packages/plugin-dev/todoist-import/src/plugin.js b/packages/plugin-dev/todoist-import/src/plugin.js new file mode 100644 index 0000000000..94a9edc51a --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/plugin.js @@ -0,0 +1,4 @@ +// Todoist Import plugin — all logic lives in the iframe UI (index.html). +// The plugin is opened via the Import/Export settings screen; it registers no +// hooks, shortcuts or menu entries (isSkipMenuEntry) so it adds zero standing +// weight when idle. diff --git a/packages/plugin-dev/todoist-import/src/ui/build-lossy-notes.spec.ts b/packages/plugin-dev/todoist-import/src/ui/build-lossy-notes.spec.ts new file mode 100644 index 0000000000..7909ef2c35 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/ui/build-lossy-notes.spec.ts @@ -0,0 +1,100 @@ +import { PriorityMapping } from '../map/plan-import'; +import { TodoistImportModel, TodoistTask } from '../parse/normalized-model'; +import { buildLossyNotes } from './build-lossy-notes'; + +const task = (overrides: Partial): TodoistTask => ({ + extId: 'task', + projectExtId: 'selected', + parentExtId: null, + title: 'Task', + notes: '', + labels: [], + apiPriority: 1, + dueDay: null, + dueWithTime: null, + timeEstimate: null, + isRecurring: false, + wasDemoted: false, + isDayDurationSkipped: false, + hasAssignee: false, + attachmentCount: 0, + ...overrides, +}); + +const model = (): TodoistImportModel => ({ + projects: [ + { + extId: 'selected', + title: 'Child', + parentExtId: 'parent', + isInbox: false, + childOrder: 0, + truncatedFieldCount: 1, + }, + { + extId: 'other', + title: 'Other', + parentExtId: null, + isInbox: false, + childOrder: 1, + }, + ], + sections: [ + { extId: 'section-selected', projectExtId: 'selected', title: 'Section' }, + { extId: 'section-other', projectExtId: 'other', title: 'Other section' }, + ], + tasks: [ + task({ + extId: 'selected-root', + isRecurring: true, + isDayDurationSkipped: true, + hasAssignee: true, + attachmentCount: 2, + truncatedFieldCount: 2, + }), + task({ + extId: 'selected-sub', + parentExtId: 'selected-root', + labels: ['label'], + apiPriority: 4, + wasDemoted: true, + }), + task({ + extId: 'other-root', + projectExtId: 'other', + isRecurring: true, + attachmentCount: 4, + }), + ], +}); + +const keysFor = (priorityMapping: PriorityMapping): string[] => + buildLossyNotes(model(), new Set(['selected']), priorityMapping).map( + (note) => note.key, + ); + +describe('buildLossyNotes', () => { + it('only reports losses for selected projects', () => { + const notes = buildLossyNotes(model(), new Set(['selected']), 'none'); + + expect(notes).toEqual( + expect.arrayContaining([ + { key: 'LOSS.NESTED_PROJECTS', params: { count: 1 } }, + { key: 'LOSS.SECTIONS', params: { count: 1 } }, + { key: 'LOSS.DEMOTED_SUBTASKS', params: { count: 1 } }, + { key: 'LOSS.RECURRING', params: { count: 1 } }, + { key: 'LOSS.DAY_DURATIONS', params: { count: 1 } }, + { key: 'LOSS.SUBTASK_LABELS', params: { count: 1 } }, + { key: 'LOSS.ASSIGNEES', params: { count: 1 } }, + { key: 'LOSS.ATTACHMENTS', params: { count: 2 } }, + { key: 'LOSS.TRUNCATED_FIELDS', params: { count: 3 } }, + ]), + ); + }); + + it('reports prioritized sub-tasks only when priority mapping is enabled', () => { + expect(keysFor('none')).not.toContain('LOSS.SUBTASK_PRIORITIES'); + expect(keysFor('priorityTags')).toContain('LOSS.SUBTASK_PRIORITIES'); + expect(keysFor('eisenhower')).toContain('LOSS.SUBTASK_PRIORITIES'); + }); +}); diff --git a/packages/plugin-dev/todoist-import/src/ui/build-lossy-notes.ts b/packages/plugin-dev/todoist-import/src/ui/build-lossy-notes.ts new file mode 100644 index 0000000000..eb0f37db2a --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/ui/build-lossy-notes.ts @@ -0,0 +1,61 @@ +import { PriorityMapping } from '../map/plan-import'; +import { TodoistImportModel } from '../parse/normalized-model'; + +export interface LossNote { + key: string; + params?: Record; +} + +export const buildLossyNotes = ( + model: TodoistImportModel, + selected: ReadonlySet, + priorityMapping: PriorityMapping, +): LossNote[] => { + const projects = model.projects.filter((project) => selected.has(project.extId)); + const tasks = model.tasks.filter((task) => selected.has(task.projectExtId)); + const notes: LossNote[] = []; + const addCount = (key: string, count: number): void => { + if (count) { + notes.push({ key, params: { count } }); + } + }; + + addCount( + 'LOSS.NESTED_PROJECTS', + projects.filter((project) => !!project.parentExtId).length, + ); + addCount( + 'LOSS.SECTIONS', + model.sections.filter((section) => selected.has(section.projectExtId)).length, + ); + addCount('LOSS.DEMOTED_SUBTASKS', tasks.filter((task) => task.wasDemoted).length); + addCount('LOSS.RECURRING', tasks.filter((task) => task.isRecurring).length); + addCount( + 'LOSS.DAY_DURATIONS', + tasks.filter((task) => task.isDayDurationSkipped).length, + ); + addCount( + 'LOSS.SUBTASK_LABELS', + tasks.filter((task) => task.parentExtId && task.labels.length).length, + ); + if (priorityMapping !== 'none') { + addCount( + 'LOSS.SUBTASK_PRIORITIES', + tasks.filter((task) => task.parentExtId && task.apiPriority > 1).length, + ); + } + addCount('LOSS.ASSIGNEES', tasks.filter((task) => task.hasAssignee).length); + addCount( + 'LOSS.ATTACHMENTS', + tasks.reduce((count, task) => count + task.attachmentCount, 0), + ); + addCount( + 'LOSS.TRUNCATED_FIELDS', + [...projects, ...tasks].reduce( + (count, item) => count + (item.truncatedFieldCount || 0), + 0, + ), + ); + notes.push({ key: 'LOSS.COMPLETED_AND_REMINDERS' }); + return notes; +}; diff --git a/packages/plugin-dev/todoist-import/src/ui/i18n.spec.ts b/packages/plugin-dev/todoist-import/src/ui/i18n.spec.ts new file mode 100644 index 0000000000..f08133e110 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/ui/i18n.spec.ts @@ -0,0 +1,22 @@ +import { loadTranslations, t } from './i18n'; + +describe('iframe translations', () => { + it('awaits the iframe translation bridge and interpolates dynamic values locally', async () => { + const translate = jest.fn((key: string) => + Promise.resolve(key === 'BUTTON.LOAD_PREVIEW' ? 'Vorschau laden' : key), + ); + + await loadTranslations({ translate }); + + expect(t('BUTTON.LOAD_PREVIEW')).toBe('Vorschau laden'); + expect(t('ERROR.LOAD_FAILED', { error: '401' })).toContain('401'); + }); + + it('falls back to bundled English if the host translation call fails', async () => { + await loadTranslations({ + translate: () => Promise.reject(new Error('bridge unavailable')), + }); + + expect(t('BUTTON.IMPORT')).toBe('Import'); + }); +}); diff --git a/packages/plugin-dev/todoist-import/src/ui/i18n.ts b/packages/plugin-dev/todoist-import/src/ui/i18n.ts new file mode 100644 index 0000000000..d39134830c --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/ui/i18n.ts @@ -0,0 +1,47 @@ +import english from '../../i18n/en.json'; + +type TranslationParams = Record; + +interface TranslationApi { + translate: (key: string, params?: TranslationParams) => string | Promise; +} + +const flatten = (value: Record, prefix = ''): Record => { + const result: Record = {}; + for (const [key, nestedValue] of Object.entries(value)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof nestedValue === 'string') { + result[fullKey] = nestedValue; + } else if (nestedValue && typeof nestedValue === 'object') { + Object.assign(result, flatten(nestedValue as Record, fullKey)); + } + } + return result; +}; + +const englishByKey = flatten(english); +let translatedByKey: Record = { ...englishByKey }; + +export const loadTranslations = async (api: TranslationApi): Promise => { + translatedByKey = { ...englishByKey }; + await Promise.all( + Object.keys(englishByKey).map(async (key) => { + try { + const translated = await Promise.resolve(api.translate(key)); + if (translated && translated !== key) { + translatedByKey[key] = translated; + } + } catch { + // Bundled English remains available if the iframe bridge is unavailable. + } + }), + ); +}; + +export const t = (key: string, params: TranslationParams = {}): string => { + let value = translatedByKey[key] || englishByKey[key] || key; + for (const [name, replacement] of Object.entries(params)) { + value = value.split(`{{${name}}}`).join(String(replacement)); + } + return value; +}; diff --git a/packages/plugin-dev/todoist-import/src/ui/index.html b/packages/plugin-dev/todoist-import/src/ui/index.html new file mode 100644 index 0000000000..1202698d06 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/ui/index.html @@ -0,0 +1,53 @@ + + + + + Todoist Import + + + + +
+ + + diff --git a/packages/plugin-dev/todoist-import/src/ui/main.ts b/packages/plugin-dev/todoist-import/src/ui/main.ts new file mode 100644 index 0000000000..a1786b2b49 --- /dev/null +++ b/packages/plugin-dev/todoist-import/src/ui/main.ts @@ -0,0 +1,355 @@ +import { PluginAPI as PluginApiType, Project } from '@super-productivity/plugin-api'; +import { parseSyncResponse, ParseStrings } from '../parse/from-api'; +import { loadTodoistData } from '../parse/load-todoist-data'; +import { TodoistImportModel } from '../parse/normalized-model'; +import { + buildProjectTitles, + groupTasksByProject, + ImportPlan, + planImport, + PriorityMapping, +} from '../map/plan-import'; +import { runImport, ImportResult } from '../map/run-import'; +import { buildLossyNotes, LossNote } from './build-lossy-notes'; +import { loadTranslations, t } from './i18n'; + +declare global { + interface Window { + PluginAPI: PluginApiType; + } +} + +const api = (): PluginApiType => window.PluginAPI; + +const el = ( + tag: K, + props: Partial & { text?: string } = {}, + children: (HTMLElement | string)[] = [], +): HTMLElementTagNameMap[K] => { + const node = document.createElement(tag); + const { text, ...rest } = props; + Object.assign(node, rest); + if (text !== undefined) { + node.textContent = text; + } + for (const child of children) { + node.append(child); + } + return node; +}; + +const app = (): HTMLElement => document.getElementById('app') as HTMLElement; + +const render = (...children: HTMLElement[]): void => { + const root = app(); + root.replaceChildren(...children); +}; + +const parseStrings = (): ParseStrings => ({ + untitledProject: t('PARSE.UNTITLED_PROJECT'), + untitledTask: t('PARSE.UNTITLED_TASK'), + repeats: (rule) => t('PARSE.REPEATS', { rule }), + deadline: (date) => t('PARSE.DEADLINE', { date }), + comments: t('PARSE.COMMENTS'), + file: t('PARSE.FILE'), +}); + +// --------------------------------------------------------------------------- +// Step 1 — token input +// --------------------------------------------------------------------------- + +const renderTokenStep = (errorMsg?: string, tokenValue?: string): void => { + const tokenInputId = 'todoist-api-token'; + const tokenInput = el('input', { + id: tokenInputId, + type: 'password', + placeholder: t('TOKEN.PLACEHOLDER'), + autocomplete: 'off', + value: tokenValue || '', + }); + const fetchBtn = el('button', { text: t('BUTTON.LOAD_PREVIEW') }); + const errorLine = errorMsg + ? [el('p', { className: 'error', role: 'alert', text: errorMsg })] + : []; + + const submit = async (): Promise => { + const token = tokenInput.value.trim(); + if (!token) { + renderTokenStep(t('TOKEN.REQUIRED')); + return; + } + render( + el('h1', { text: t('TITLE.IMPORT') }), + el('p', { + text: t('TOKEN.LOADING'), + role: 'status', + ariaLive: 'polite', + }), + ); + try { + const raw = await loadTodoistData(api(), token); + const model = parseSyncResponse(raw || {}, parseStrings()); + if (!model.projects.length) { + renderTokenStep(t('TOKEN.NO_PROJECTS'), token); + return; + } + const existingProjects = await api().getAllProjects(); + renderPreviewStep(model, existingProjects); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + renderTokenStep(t('ERROR.LOAD_FAILED', { error: msg }), tokenInput.value); + } + }; + fetchBtn.addEventListener('click', () => void submit()); + tokenInput.addEventListener('keydown', (ev) => { + if (ev.key === 'Enter') { + void submit(); + } + }); + + render( + el('h1', { text: t('TITLE.IMPORT') }), + el('p', { text: t('TOKEN.INTRO') }), + ...errorLine, + el('label', { htmlFor: tokenInputId, text: t('TOKEN.LABEL') }), + tokenInput, + el('p', { className: 'muted', text: t('TOKEN.HELP') }), + el('div', { className: 'actions' }, [fetchBtn]), + ); +}; + +// --------------------------------------------------------------------------- +// Step 2 — preview with per-project selection +// --------------------------------------------------------------------------- + +const renderPreviewStep = ( + model: TodoistImportModel, + existingProjects: Project[], +): void => { + const existingTitles = new Set(existingProjects.map((p) => p.title.toLowerCase())); + // the exact titles the import will create (Inbox rename, `Parent / Child` + // disambiguation, `(2)` suffixes) — preview and collision check must match + const titleByExtId = buildProjectTitles(model); + const tasksByProject = groupTasksByProject(model); + const checkboxByExtId = new Map(); + const lossyList = el('ul'); + // Priority → tags is a single mutually-exclusive choice (radios): off, the + // p1–p3 tags, or SP's built-in Eisenhower urgent/important tags. + const priorityRadio = (value: PriorityMapping, checked = false): HTMLInputElement => + el('input', { type: 'radio', name: 'todoist-priority-mapping', value, checked }); + const priorityNoneRadio = priorityRadio('none', true); + const priorityTagsRadio = priorityRadio('priorityTags'); + const priorityEisenhowerRadio = priorityRadio('eisenhower'); + const selectedPriorityMapping = (): PriorityMapping => + priorityTagsRadio.checked + ? 'priorityTags' + : priorityEisenhowerRadio.checked + ? 'eisenhower' + : 'none'; + + const selectedIds = (): Set => { + const ids = new Set(); + checkboxByExtId.forEach((box, extId) => { + if (box.checked) { + ids.add(extId); + } + }); + return ids; + }; + + const refreshLossyList = (): void => { + lossyList.replaceChildren( + ...buildLossyNotes(model, selectedIds(), selectedPriorityMapping()).map((note) => + el('li', { text: t(note.key, note.params) }), + ), + ); + }; + + const projectRows = model.projects.map((project) => { + const tasks = tasksByProject.get(project.extId) || []; + const rootCount = tasks.filter((t) => !t.parentExtId).length; + const subCount = tasks.length - rootCount; + const title = titleByExtId.get(project.extId) as string; + const collides = existingTitles.has(title.toLowerCase()); + const checkbox = el('input', { type: 'checkbox', checked: !collides }); + checkbox.addEventListener('change', refreshLossyList); + checkboxByExtId.set(project.extId, checkbox); + return el('label', {}, [ + checkbox, + ` ${t('PREVIEW.PROJECT_COUNTS', { + title, + taskCount: rootCount, + subTaskCount: subCount, + })}`, + ...(collides + ? [ + el('span', { + className: 'warn', + text: ` — ${t('PREVIEW.ALREADY_EXISTS')}`, + }), + ] + : []), + ]); + }); + + for (const radio of [priorityNoneRadio, priorityTagsRadio, priorityEisenhowerRadio]) { + radio.addEventListener('change', refreshLossyList); + } + + const importBtn = el('button', { text: t('BUTTON.IMPORT') }); + const backBtn = el('button', { text: t('BUTTON.BACK') }); + backBtn.addEventListener('click', () => renderTokenStep()); + importBtn.addEventListener('click', () => { + const selected = selectedIds(); + if (!selected.size) { + api().showSnack({ msg: t('PREVIEW.SELECT_PROJECT'), type: 'WARNING' }); + return; + } + const priorityMapping = selectedPriorityMapping(); + const plan = planImport(model, { + priorityMapping, + selectedProjectExtIds: selected, + }); + void executeImport(plan, buildLossyNotes(model, selected, priorityMapping)); + }); + + refreshLossyList(); + render( + el('h1', { text: t('TITLE.PREVIEW') }), + el('p', { text: t('PREVIEW.CHOOSE_PROJECTS') }), + el('div', {}, projectRows), + el('fieldset', {}, [ + el('legend', { text: t('PREVIEW.PRIORITY_LEGEND') }), + el('label', {}, [priorityNoneRadio, ` ${t('PREVIEW.PRIORITY_NONE')}`]), + el('label', {}, [priorityTagsRadio, ` ${t('PREVIEW.PRIORITY_TAGS')}`]), + el('label', {}, [priorityEisenhowerRadio, ` ${t('PREVIEW.PRIORITY_EISENHOWER')}`]), + ]), + el('h2', { text: t('PREVIEW.LOSS_HEADING') }), + lossyList, + el('div', { className: 'actions' }, [importBtn, backBtn]), + ); +}; + +// --------------------------------------------------------------------------- +// Step 3 + 4 — import progress and summary +// --------------------------------------------------------------------------- + +const executeImport = async (plan: ImportPlan, lossyNotes: LossNote[]): Promise => { + const progressLine = el('p', { + text: t('IMPORT.STARTING'), + role: 'status', + ariaLive: 'polite', + }); + render(el('h1', { text: t('TITLE.IMPORTING') }), progressLine); + + const result = await runImport(api(), plan, (progress) => { + const detail = + progress.phase === 'details' && progress.detailTotal + ? t('IMPORT.PHASE_DETAILS', { + current: Math.min((progress.detailIndex ?? 0) + 1, progress.detailTotal), + total: progress.detailTotal, + }) + : t(progress.phase === 'project' ? 'IMPORT.PHASE_PROJECT' : 'IMPORT.PHASE_TASKS'); + progressLine.textContent = t('IMPORT.PROGRESS', { + current: progress.projectIndex + 1, + total: progress.totalProjects, + title: progress.projectTitle, + detail, + }); + }); + renderSummaryStep(result, lossyNotes); +}; + +const projectSummaryLine = (p: ImportResult['imported'][number]): HTMLElement => { + const isShortfall = + p.landedTaskCount < p.plannedTaskCount || + p.landedSubTaskCount < p.plannedSubTaskCount; + return el('li', { + className: isShortfall ? 'warn' : '', + text: + t('SUMMARY.PROJECT_RESULT', { + title: p.title, + landedTasks: p.landedTaskCount, + plannedTasks: p.plannedTaskCount, + landedSubTasks: p.landedSubTaskCount, + plannedSubTasks: p.plannedSubTaskCount, + }) + (isShortfall ? ` — ${t('SUMMARY.SHORTFALL')}` : ''), + }); +}; + +const renderSummaryStep = (result: ImportResult, lossyNotes: LossNote[]): void => { + const items = result.imported.map(projectSummaryLine); + const failure = result.errorMessage + ? [ + el('p', { + className: 'error', + role: 'alert', + text: result.failedProjectTitle + ? t('ERROR.IMPORT_STOPPED', { + project: result.failedProjectTitle, + error: result.errorMessage, + }) + : t('ERROR.IMPORT_FAILED', { error: result.errorMessage }), + }), + ] + : []; + const unverified = result.isCountUnverified + ? [ + el('p', { + className: 'warn', + role: 'status', + text: t('SUMMARY.UNVERIFIED'), + }), + ] + : []; + const tagLine = result.createdTagTitles.length + ? [ + el('p', { + text: t('SUMMARY.CREATED_TAGS', { + tags: result.createdTagTitles.join(', '), + }), + }), + ] + : []; + const lossy = lossyNotes.length + ? [ + el('h2', { text: t('SUMMARY.NOT_CARRIED_OVER') }), + el( + 'ul', + {}, + lossyNotes.map((note) => el('li', { text: t(note.key, note.params) })), + ), + ] + : []; + + if (!result.errorMessage) { + api().showSnack({ msg: t('SUMMARY.SNACK_FINISHED'), type: 'SUCCESS' }); + } + render( + el('h1', { + text: t(result.errorMessage ? 'TITLE.INCOMPLETE' : 'TITLE.FINISHED'), + }), + el('ul', {}, items), + ...unverified, + ...tagLine, + ...failure, + ...lossy, + el('p', { + className: 'muted', + text: t('SUMMARY.UNDO'), + }), + ); +}; + +// The host injects the PluginAPI bridge script at the end of ; wait for +// DOM readiness so it is guaranteed to be defined before first use. +const start = async (): Promise => { + await loadTranslations(api()); + renderTokenStep(); +}; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => void start()); +} else { + void start(); +} diff --git a/packages/plugin-dev/todoist-import/tsconfig.json b/packages/plugin-dev/todoist-import/tsconfig.json new file mode 100644 index 0000000000..97f798a881 --- /dev/null +++ b/packages/plugin-dev/todoist-import/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "lib": ["ES2020", "DOM"], + "types": ["jest", "node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/app/imex/file-imex/file-imex.component.html b/src/app/imex/file-imex/file-imex.component.html index 0de602d610..6dd54644a8 100644 --- a/src/app/imex/file-imex/file-imex.component.html +++ b/src/app/imex/file-imex/file-imex.component.html @@ -56,3 +56,13 @@ compress {{ T.FILE_IMEX.BTN_COMPRESS_ARCHIVE | translate }} + + diff --git a/src/app/imex/file-imex/file-imex.component.spec.ts b/src/app/imex/file-imex/file-imex.component.spec.ts index 8efdab25c9..840c500683 100644 --- a/src/app/imex/file-imex/file-imex.component.spec.ts +++ b/src/app/imex/file-imex/file-imex.component.spec.ts @@ -18,6 +18,7 @@ import { ConfirmUrlImportDialogComponent } from '../dialog-confirm-url-import/di import { DialogImportFromUrlComponent } from '../dialog-import-from-url/dialog-import-from-url.component'; import { createAppDataCompleteMock } from '../../util/app-data-mock'; import { ImportEncryptionHandlerService } from '../sync/import-encryption-handler.service'; +import { PluginService } from '../../plugins/plugin.service'; describe('FileImexComponent', () => { let component: FileImexComponent; @@ -52,6 +53,13 @@ describe('FileImexComponent', () => { importEncryptionHandlerSpy.checkEncryptionStateChange.and.returnValue( Promise.resolve({ willChange: false }), ); + const pluginServiceSpy = jasmine.createSpyObj('PluginService', [ + 'isInitialized', + 'initializePlugins', + 'activatePlugin', + ]); + pluginServiceSpy.isInitialized.and.returnValue(true); + pluginServiceSpy.activatePlugin.and.returnValue(Promise.resolve(null)); importEncryptionHandlerSpy.handleImportEncryptionIfNeeded.and.returnValue( Promise.resolve(null), ); @@ -77,6 +85,7 @@ describe('FileImexComponent', () => { { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: MatDialog, useValue: matDialogSpy }, { provide: ImportEncryptionHandlerService, useValue: importEncryptionHandlerSpy }, + { provide: PluginService, useValue: pluginServiceSpy }, ], }).compileComponents(); diff --git a/src/app/imex/file-imex/file-imex.component.ts b/src/app/imex/file-imex/file-imex.component.ts index 2455cec664..91c25eda5f 100644 --- a/src/app/imex/file-imex/file-imex.component.ts +++ b/src/app/imex/file-imex/file-imex.component.ts @@ -43,6 +43,9 @@ import { Log } from '../../core/log'; import { DialogArchiveCompressionComponent } from '../../features/archive/dialog-archive-compression/dialog-archive-compression.component'; import { DataValidationFailedError } from '../../op-log/core/errors/sync-errors'; import { alertDialog } from '../../util/native-dialogs'; +import { PluginService } from '../../plugins/plugin.service'; + +const TODOIST_IMPORT_PLUGIN_ID = 'todoist-import'; @Component({ selector: 'file-imex', @@ -59,6 +62,7 @@ export class FileImexComponent implements OnInit { private _matDialog = inject(MatDialog); private _http = inject(HttpClient); private _importEncryptionHandler = inject(ImportEncryptionHandlerService); + private _pluginService = inject(PluginService); readonly fileInputRef = viewChild('fileInput'); T: typeof T = T; @@ -297,4 +301,28 @@ export class FileImexComponent implements OnInit { maxWidth: '90vw', }); } + + async openTodoistImport(): Promise { + try { + if (!this._pluginService.isInitialized()) { + await this._pluginService.initializePlugins(); + } + // In-memory activation only (not persisted): the importer is a one-time + // tool and should be dormant again after a restart. + const instance = await this._pluginService.activatePlugin( + TODOIST_IMPORT_PLUGIN_ID, + true, + ); + if (!instance) { + throw new Error('Plugin activation returned no instance'); + } + await this._router.navigate(['/plugins', TODOIST_IMPORT_PLUGIN_ID, 'index']); + } catch (e) { + Log.err('Failed to open Todoist importer', e); + this._snackService.open({ + type: 'ERROR', + msg: T.FILE_IMEX.S_ERR_TODOIST_IMPORT_OPEN, + }); + } + } } diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index 600031d912..7c96b32675 100644 --- a/src/app/plugins/plugin-bridge.service.ts +++ b/src/app/plugins/plugin-bridge.service.ts @@ -1221,6 +1221,7 @@ export class PluginBridgeService implements OnDestroy { // Chunk large operations to prevent oversized payloads const chunks = this._chunkOperations(request.operations); + const createdTaskTimestamp = Date.now(); if (chunks.length > 1) { PluginLog.log('PluginBridge: Chunking large batch operation', { @@ -1237,6 +1238,7 @@ export class PluginBridgeService implements OnDestroy { projectId: request.projectId, operations: chunk, createdTaskIds, // Same IDs mapping for all chunks + createdTaskTimestamp, }), ); }); diff --git a/src/app/plugins/plugin.service.ts b/src/app/plugins/plugin.service.ts index 4c2a08340b..a9755b47e4 100644 --- a/src/app/plugins/plugin.service.ts +++ b/src/app/plugins/plugin.service.ts @@ -61,6 +61,7 @@ const BUNDLED_PLUGIN_PATHS = [ 'assets/bundled-plugins/google-calendar-provider', 'assets/bundled-plugins/caldav-calendar-provider', 'assets/bundled-plugins/doc-mode', + 'assets/bundled-plugins/todoist-import', ] as const; // Reserved ids: an uploaded plugin may not reuse a bundled plugin's manifest id (it would @@ -85,6 +86,7 @@ const BUNDLED_PLUGIN_IDS = new Set([ 'linear-issue-provider', 'procrastination-buster', 'sync-md', + 'todoist-import', 'trello-issue-provider', 'voice-reminder', 'yesterday-tasks', diff --git a/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.spec.ts b/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.spec.ts index 79a46d886b..53bf1979a9 100644 --- a/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.spec.ts +++ b/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.spec.ts @@ -86,6 +86,27 @@ describe('taskBatchUpdateMetaReducer', () => { expect(task.isDone).toBe(false); }); + it('should use the timestamp captured in the persistent batch action', () => { + const action = TaskSharedActions.batchUpdateForProject({ + projectId: 'project1', + operations: [ + { + type: 'create', + tempId: 'temp-stable-time', + data: { title: 'Replay-safe task' }, + } as BatchTaskCreate, + ], + createdTaskIds: { 'temp-stable-time': 'stable-time-task' }, + createdTaskTimestamp: 1_752_124_200_000, + }); + + const result = metaReducer(baseState, action); + + expect(result[TASK_FEATURE_NAME].entities['stable-time-task']?.created).toBe( + 1_752_124_200_000, + ); + }); + it('should skip task creation if title is empty', () => { const operations: BatchOperation[] = [ { diff --git a/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.ts b/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.ts index d234d90ee8..6da2c7fd1c 100644 --- a/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.ts +++ b/src/app/root-store/meta/task-shared-meta-reducers/task-batch-update.reducer.ts @@ -42,9 +42,8 @@ export const taskBatchUpdateMetaReducer = = RootSt ): ActionReducer => { return (state: T | undefined, action: Action) => { if (action.type === TaskSharedActions.batchUpdateForProject.type) { - const { projectId, operations, createdTaskIds } = action as ReturnType< - typeof TaskSharedActions.batchUpdateForProject - >; + const { projectId, operations, createdTaskIds, createdTaskTimestamp } = + action as ReturnType; // Ensure state has required properties const rootState = state as unknown as RootState; @@ -144,7 +143,9 @@ export const taskBatchUpdateMetaReducer = = RootSt notes: createOp.data.notes || '', parentId, timeEstimate: createOp.data.timeEstimate || 0, - created: Date.now(), + // New actions capture this before dispatch so replay is deterministic. + // The fallback keeps legacy operations and direct test callers valid. + created: createdTaskTimestamp ?? Date.now(), }; tasksToAdd.push(newTask); diff --git a/src/app/root-store/meta/task-shared.actions.ts b/src/app/root-store/meta/task-shared.actions.ts index 7c542a48ad..3e2a3bd280 100644 --- a/src/app/root-store/meta/task-shared.actions.ts +++ b/src/app/root-store/meta/task-shared.actions.ts @@ -381,6 +381,8 @@ export const TaskSharedActions = createActionGroup({ projectId: string; operations: BatchOperation[]; createdTaskIds: { [tempId: string]: string }; + /** Captured before dispatch so replay creates identical task state. */ + createdTaskTimestamp?: number; }) => ({ ...taskProps, meta: { diff --git a/src/app/t.const.ts b/src/app/t.const.ts index 2677f69717..08e933e3b6 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -2288,6 +2288,7 @@ const T = { }, EXPORT_DATA: 'FILE_IMEX.EXPORT_DATA', IMPORT_FROM_FILE: 'FILE_IMEX.IMPORT_FROM_FILE', + IMPORT_FROM_TODOIST: 'FILE_IMEX.IMPORT_FROM_TODOIST', IMPORT_FROM_URL: 'FILE_IMEX.IMPORT_FROM_URL', IMPORT_FROM_URL_DIALOG_DESCRIPTION: 'FILE_IMEX.IMPORT_FROM_URL_DIALOG_DESCRIPTION', IMPORT_FROM_URL_DIALOG_TITLE: 'FILE_IMEX.IMPORT_FROM_URL_DIALOG_TITLE', @@ -2299,6 +2300,7 @@ const T = { S_ERR_INVALID_DATA: 'FILE_IMEX.S_ERR_INVALID_DATA', S_ERR_INVALID_URL: 'FILE_IMEX.S_ERR_INVALID_URL', S_ERR_NETWORK: 'FILE_IMEX.S_ERR_NETWORK', + S_ERR_TODOIST_IMPORT_OPEN: 'FILE_IMEX.S_ERR_TODOIST_IMPORT_OPEN', S_IMPORT_FROM_URL_ERR_DECODE: 'FILE_IMEX.S_IMPORT_FROM_URL_ERR_DECODE', URL_PLACEHOLDER: 'FILE_IMEX.URL_PLACEHOLDER', }, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index eadb43d24c..239aa0c847 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2228,6 +2228,7 @@ }, "EXPORT_DATA": "Export Data", "IMPORT_FROM_FILE": "Import from File", + "IMPORT_FROM_TODOIST": "Import from Todoist", "IMPORT_FROM_URL": "Import from URL", "IMPORT_FROM_URL_DIALOG_DESCRIPTION": "Please enter the full URL of the Super Productivity backup JSON file to import.", "IMPORT_FROM_URL_DIALOG_TITLE": "Import from URL", @@ -2239,6 +2240,7 @@ "S_ERR_INVALID_DATA": "Import failed: Invalid JSON", "S_ERR_INVALID_URL": "Import failed: Invalid URL provided", "S_ERR_NETWORK": "Import failed: Network error while fetching data from URL", + "S_ERR_TODOIST_IMPORT_OPEN": "Could not open the Todoist importer", "S_IMPORT_FROM_URL_ERR_DECODE": "Error: Could not decode the URL parameter for import. Please ensure it is correctly formatted.", "URL_PLACEHOLDER": "Enter URL to import from" },