From 71cb406d48b8f64de653c2c1cd0f941ac307ffb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:27:28 -0400 Subject: [PATCH 01/17] chore: propagate main into development (#33) Co-authored-by: Wikid82 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 453ca4c..1adaf63 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.2" + ".": "1.5.3" } diff --git a/CHANGELOG.md b/CHANGELOG.md index df62f18..8ef69c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.5.3](https://github.com/Wikid82/Workspace-File-Bookmarks/compare/workspace-file-bookmarks-v1.5.2...workspace-file-bookmarks-v1.5.3) (2026-08-20) + + +### Bug Fixes + +* **scripts:** use built-in npm audit instead of missing audit:ci script ([f281cab](https://github.com/Wikid82/Workspace-File-Bookmarks/commit/f281cab4a11c7483b741f20fb711808e36d4333b)) + ## [1.5.2](https://github.com/Wikid82/Workspace-File-Bookmarks/compare/workspace-file-bookmarks-v1.5.1...workspace-file-bookmarks-v1.5.2) (2026-08-20) diff --git a/package-lock.json b/package-lock.json index 343c9c0..d5878f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "workspace-file-bookmarks", - "version": "1.5.2", + "version": "1.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "workspace-file-bookmarks", - "version": "1.5.2", + "version": "1.5.3", "devDependencies": { "@types/node": "^26.2.0", "@types/vscode": "^1.134.0", diff --git a/package.json b/package.json index f2beadd..236ab63 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Bookmark files across multi-repo VS Code workspaces for quick access.", "publisher": "Wikid82", "private": false, - "version": "1.5.2", + "version": "1.5.3", "icon": "media/icon.png", "categories": [ "Other" From 44a13f9d84a5d70d3de3c98f0c8990e5db81e82e Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Thu, 20 Aug 2026 11:33:42 -0400 Subject: [PATCH 02/17] test: add vitest harness covering BookmarkStore and describeBookmark Extension had zero test coverage prior to this. Adds a minimal 'vscode' module mock (just the EventEmitter/window/TreeItem/ThemeIcon surface BookmarkStore and its class hierarchy touch at import time) so the store's add/dedupe/remove/move/folder-cascade logic and the label-formatting helper can run outside a VS Code host. Wires npm run test into CI and the local dep-update sweep. --- .github/workflows/ci.yml | 3 + .vscodeignore | 2 + extension.ts | 8 +- package-lock.json | 1256 +++++++++++++++++++++++++++++++++++- package.json | 6 +- scripts/wfb_dep_update.sh | 1 + test/bookmarkStore.test.ts | 111 ++++ test/fakeContext.ts | 17 + test/vscode-mock.ts | 36 ++ vitest.config.mts | 10 + 10 files changed, 1423 insertions(+), 27 deletions(-) create mode 100644 test/bookmarkStore.test.ts create mode 100644 test/fakeContext.ts create mode 100644 test/vscode-mock.ts create mode 100644 vitest.config.mts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 990de52..36b8457 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,5 +27,8 @@ jobs: - name: Lint run: npm run lint + - name: Test + run: npm run test + - name: Build run: npm run build diff --git a/.vscodeignore b/.vscodeignore index e9e1eac..afce244 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -2,9 +2,11 @@ .github/** node_modules/** scripts/** +test/** extension.ts extension.ts.map tsconfig.json +vitest.config.mts .gitignore .vscodeignore **/*.map diff --git a/extension.ts b/extension.ts index b6958fd..9e8bf45 100644 --- a/extension.ts +++ b/extension.ts @@ -7,7 +7,7 @@ const VIEW_MODE_CONTEXT_KEY = 'workspace-file-bookmarks.viewMode'; type ViewMode = 'tree' | 'list'; -interface Bookmark { +export interface Bookmark { id: string; uri: string; label: string; @@ -17,7 +17,7 @@ interface Bookmark { createdAt: number; } -interface BookmarkFolder { +export interface BookmarkFolder { id: string; name: string; createdAt: number; @@ -53,7 +53,7 @@ export function activate(context: vscode.ExtensionContext) { export function deactivate() {} -class BookmarkStore { +export class BookmarkStore { private readonly _onDidChange = new vscode.EventEmitter(); readonly onDidChange = this._onDidChange.event; @@ -207,7 +207,7 @@ class BookmarksTreeProvider implements vscode.TreeDataProvider { } } -function describeBookmark(bookmark: Bookmark, isMultiRoot: boolean, folderName: string | null): string[] { +export function describeBookmark(bookmark: Bookmark, isMultiRoot: boolean, folderName: string | null): string[] { const parts: string[] = []; if (folderName) { parts.push(folderName); diff --git a/package-lock.json b/package-lock.json index d5878f5..8df5107 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@types/vscode": "^1.134.0", "@vscode/vsce": "^3.9.2", "esbuild": "^0.28.2", - "typescript": "~6.0.3" + "typescript": "~6.0.3", + "vitest": "^4.1.11" }, "engines": { "vscode": "^1.134.0" @@ -681,6 +682,13 @@ "node": ">=18" } }, + "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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -719,6 +727,296 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -906,6 +1204,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@textlint/ast-node-types": { "version": "15.8.0", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", @@ -992,6 +1297,31 @@ "@textlint/ast-node-types": "15.8.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", @@ -1038,6 +1368,119 @@ "node": ">=22.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vscode/vsce": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", @@ -1309,6 +1752,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1528,6 +1981,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1650,6 +2113,13 @@ "node": ">=18" } }, + "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/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -1785,7 +2255,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -1969,6 +2438,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -2040,6 +2516,16 @@ "@esbuild/win32-x64": "0.28.2" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2051,6 +2537,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2102,6 +2598,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2155,6 +2669,21 @@ "node": ">=14.14" } }, + "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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2706,33 +3235,306 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "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/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keytar": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, - "dependencies": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1" + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/linkify-it": { @@ -2831,6 +3633,16 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-it": { "version": "14.3.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", @@ -3009,6 +3821,25 @@ "dev": true, "license": "ISC" }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -3114,6 +3945,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3288,6 +4133,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -3325,6 +4177,35 @@ "node": ">=4" } }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -3527,6 +4408,40 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -3713,6 +4628,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3793,6 +4715,16 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -3829,6 +4761,20 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4057,6 +5003,63 @@ "url": "https://bevry.me/fund" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/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/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -4243,6 +5246,200 @@ "url": "https://bevry.me/fund" } }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/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/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/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/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -4267,6 +5464,23 @@ "node": ">=18" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 236ab63..39477b4 100644 --- a/package.json +++ b/package.json @@ -208,13 +208,15 @@ "vscode:prepublish": "npm run build", "package": "bash scripts/rebuild.sh", "clean": "rm -rf dist", - "lint": "tsc --noEmit" + "lint": "tsc --noEmit", + "test": "vitest run" }, "devDependencies": { "@types/node": "^26.2.0", "@types/vscode": "^1.134.0", "@vscode/vsce": "^3.9.2", "esbuild": "^0.28.2", - "typescript": "~6.0.3" + "typescript": "~6.0.3", + "vitest": "^4.1.11" } } diff --git a/scripts/wfb_dep_update.sh b/scripts/wfb_dep_update.sh index e3ed0b9..4f12fc2 100755 --- a/scripts/wfb_dep_update.sh +++ b/scripts/wfb_dep_update.sh @@ -62,6 +62,7 @@ for MODULE in "${NPM_MODULES[@]}"; do npm dedupe --legacy-peer-deps npm run build npm run lint + npm run test # Fails on high/critical findings; moderate/low are allowed through. npm audit --audit-level=high npm audit fix --legacy-peer-deps || true diff --git a/test/bookmarkStore.test.ts b/test/bookmarkStore.test.ts new file mode 100644 index 0000000..a23b1bc --- /dev/null +++ b/test/bookmarkStore.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from 'vitest'; +import { BookmarkStore, describeBookmark, type Bookmark } from '../extension'; +import { createFakeContext } from './fakeContext'; + +function makeBookmark(overrides: Partial = {}): Bookmark { + return { + id: 'id-1', + uri: 'file:///repo/src/a.ts', + label: 'a.ts', + relativePath: 'src/a.ts', + workspaceFolderName: 'repo', + folderId: null, + createdAt: 0, + ...overrides + }; +} + +function newStore() { + // BookmarkStore only touches context.workspaceState; the fake covers that. + return new BookmarkStore(createFakeContext() as any); +} + +describe('BookmarkStore', () => { + it('starts empty', () => { + const store = newStore(); + expect(store.getAllBookmarks()).toEqual([]); + expect(store.getAllFolders()).toEqual([]); + }); + + it('adds a bookmark', () => { + const store = newStore(); + store.addBookmark(makeBookmark()); + expect(store.getAllBookmarks()).toHaveLength(1); + }); + + it('does not add a duplicate bookmark for the same uri', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a' })); + store.addBookmark(makeBookmark({ id: 'b' })); + const all = store.getAllBookmarks(); + expect(all).toHaveLength(1); + expect(all[0].id).toBe('a'); + }); + + it('removes a bookmark by id', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a' })); + store.removeBookmark('a'); + expect(store.getAllBookmarks()).toEqual([]); + }); + + it('moves a bookmark to a folder', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a', folderId: null })); + store.moveBookmarkToFolder('a', 'folder-1'); + expect(store.getAllBookmarks()[0].folderId).toBe('folder-1'); + }); + + it('creates, renames, and deletes a folder', () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + expect(store.getAllFolders()).toEqual([folder]); + + store.renameFolder(folder.id, 'API'); + expect(store.getAllFolders()[0].name).toBe('API'); + + store.deleteFolder(folder.id); + expect(store.getAllFolders()).toEqual([]); + }); + + it('unsets folderId on bookmarks when their folder is deleted', () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + store.addBookmark(makeBookmark({ id: 'a', folderId: folder.id })); + + store.deleteFolder(folder.id); + + expect(store.getAllBookmarks()[0].folderId).toBeNull(); + }); + + it('fires onDidChange when bookmarks or folders change', () => { + const store = newStore(); + let fireCount = 0; + store.onDidChange(() => fireCount++); + + store.addBookmark(makeBookmark()); + store.createFolder('Backend'); + + expect(fireCount).toBe(2); + }); +}); + +describe('describeBookmark', () => { + const bookmark = makeBookmark({ workspaceFolderName: 'repo-a', relativePath: 'src/a.ts' }); + + it('is just the relative path in a single-root workspace with no folder', () => { + expect(describeBookmark(bookmark, false, null)).toEqual(['src/a.ts']); + }); + + it('prefixes the workspace folder name in a multi-root workspace', () => { + expect(describeBookmark(bookmark, true, null)).toEqual(['repo-a', 'src/a.ts']); + }); + + it('prefixes the bookmark folder name when set', () => { + expect(describeBookmark(bookmark, false, 'Backend')).toEqual(['Backend', 'src/a.ts']); + }); + + it('shows folder name and workspace folder name together', () => { + expect(describeBookmark(bookmark, true, 'Backend')).toEqual(['Backend', 'repo-a', 'src/a.ts']); + }); +}); diff --git a/test/fakeContext.ts b/test/fakeContext.ts new file mode 100644 index 0000000..2acc47c --- /dev/null +++ b/test/fakeContext.ts @@ -0,0 +1,17 @@ +// Fakes the slice of vscode.ExtensionContext that BookmarkStore reads/writes: +// workspaceState.get/update backed by a plain in-memory map. + +export function createFakeContext() { + const state = new Map(); + return { + workspaceState: { + get(key: string, defaultValue: T): T { + return state.has(key) ? (state.get(key) as T) : defaultValue; + }, + update(key: string, value: unknown) { + state.set(key, value); + return Promise.resolve(); + } + } + }; +} diff --git a/test/vscode-mock.ts b/test/vscode-mock.ts new file mode 100644 index 0000000..80bf6c4 --- /dev/null +++ b/test/vscode-mock.ts @@ -0,0 +1,36 @@ +// Minimal stand-in for the 'vscode' module, covering only the surface that +// BookmarkStore (the code under test) actually touches at runtime. Not a +// general-purpose vscode mock — extend it if tests start exercising more. + +export class EventEmitter { + private listeners: Array<(e: T) => void> = []; + + event = (listener: (e: T) => void) => { + this.listeners.push(listener); + return { dispose: () => {} }; + }; + + fire(data: T) { + for (const listener of this.listeners) { + listener(data); + } + } +} + +export const window = { + showInformationMessage: (..._args: unknown[]) => {}, + showWarningMessage: (..._args: unknown[]) => {} +}; + +// Referenced only at class-definition time (extends/enum access) when the +// whole extension.ts module loads for testing — never actually exercised by +// BookmarkStore/describeBookmark, so these stay stubs. +export const TreeItemCollapsibleState = { None: 0, Collapsed: 1, Expanded: 2 }; + +export class TreeItem { + constructor(public label?: unknown, public collapsibleState?: unknown) {} +} + +export class ThemeIcon { + constructor(public id: string) {} +} diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..ffe237c --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + resolve: { + alias: { + vscode: path.resolve(import.meta.dirname, 'test/vscode-mock.ts') + } + } +}); From da843b99af2b5b2662b1845b9fb5bd24ea02e562 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Thu, 20 Aug 2026 11:44:33 -0400 Subject: [PATCH 03/17] test: expand coverage to 85%+ and wire up Codecov patch/project gates Exports the remaining internal pieces of extension.ts (BookmarksTreeProvider, FolderGroupItem, BookmarkTreeItem, toBookmark, and the command handler functions) and adds tests for all of them: the tree/list view-mode grouping and sorting, folder pick/create/rename/delete flows, the active-file and multi-select bookmark commands, and activate()'s command registration. Coverage: 100% statements/functions/lines, 93.75% branches. Adds codecov.yml (project + patch, both 85% target/1% threshold, mirroring Charon/Hestia) and a CI upload step. Adds scripts/local-patch-report.sh, a single-package port of Hestia's script, so patch coverage can be checked locally before a CI round-trip. Adds CLAUDE.md with a Definition of Done section documenting the gate. --- .github/workflows/ci.yml | 11 +- .vscodeignore | 2 + CLAUDE.md | 35 ++++ codecov.yml | 38 ++++ extension.ts | 28 +-- package-lock.json | 202 +++++++++++++++++++++- package.json | 4 +- scripts/local-patch-report.sh | 162 +++++++++++++++++ test/activate.test.ts | 50 ++++++ test/commands.test.ts | 315 ++++++++++++++++++++++++++++++++++ test/setup.ts | 8 + test/toBookmark.test.ts | 30 ++++ test/treeProvider.test.ts | 122 +++++++++++++ test/vscode-mock.ts | 56 +++++- vitest.config.mts | 15 ++ 15 files changed, 1049 insertions(+), 29 deletions(-) create mode 100644 CLAUDE.md create mode 100644 codecov.yml create mode 100755 scripts/local-patch-report.sh create mode 100644 test/activate.test.ts create mode 100644 test/commands.test.ts create mode 100644 test/setup.ts create mode 100644 test/toBookmark.test.ts create mode 100644 test/treeProvider.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36b8457..a9bfb47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,15 @@ jobs: - name: Lint run: npm run lint - - name: Test - run: npm run test + - name: Test with coverage + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + fail_ci_if_error: false - name: Build run: npm run build diff --git a/.vscodeignore b/.vscodeignore index afce244..008da00 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -7,6 +7,8 @@ extension.ts extension.ts.map tsconfig.json vitest.config.mts +codecov.yml +CLAUDE.md .gitignore .vscodeignore **/*.map diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ab98a2a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,35 @@ +# Workspace File Bookmarks + +A VS Code extension (single-file `extension.ts`, esbuild-bundled) that lets +users bookmark files across a multi-root/multi-repo workspace. + +## Workflow + +`development` batches feature work and dependency updates so a merge into +`main` doesn't trigger a release per commit. `main` is only touched directly +for CI fixes or hotfixes, which then propagate back down to `development` via +the `propagate-main-to-development` workflow. `promote-dev-to-main` opens a +weekly PR the other direction once `development`'s CI is green. release-please +drives versioning and publishing off Conventional Commit prefixes on `main`. + +## Definition of Done + +Every change is expected to clear this bar before it's mergeable: + +- **CI clean.** `npm run lint` (`tsc --noEmit`), `npm run test`, and + `npm run build` all pass — see `.github/workflows/ci.yml`. +- **Unit coverage ≥85%, both patch and project.** Codecov enforces this on + every PR (`codecov.yml`: `project` and `patch`, both `target: 85%, + threshold: 1%`), reported from `npm run test:coverage` (vitest + v8, + `vitest.config.mts` mirrors the same 85% thresholds so a local run fails + the same way CI's will). Before pushing, run + `scripts/local-patch-report.sh` to check the *patch* number locally — it + diffs your branch against `origin/development`, regenerates a fresh + coverage profile, and reports the same changed-lines coverage number + Codecov's patch gate computes, so a real gap shows up before a CI + round-trip instead of after. Write real tests that close gaps for real — + no padding, no vacuous assertions just to move a number. +- **Security scanning clean.** CodeQL runs on every push/PR + (`.github/workflows/codeql.yml`); `npm audit --audit-level=high` (via + `scripts/wfb_dep_update.sh`) gates dependency updates on high/critical + findings. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..9312bbc --- /dev/null +++ b/codecov.yml @@ -0,0 +1,38 @@ +# ============================================================================= +# Codecov Configuration +# Require 85% overall (project) and 85% patch (changed-lines) coverage — +# matches the same bar used on this repo's other projects (Charon, Hestia). +# See CLAUDE.md's Definition of Done section. +# ============================================================================= + +coverage: + status: + project: + default: + target: 85% + threshold: 1% + patch: + default: + target: 85% + threshold: 1% + +require_ci_to_pass: yes + +comment: + require_changes: false + require_base: false + require_head: true + layout: "reach, diff, flags, files" + behavior: default + +# Keep in sync with scripts/local-patch-report.sh's own exclude list. +ignore: + - "test/**" + - "**/*.test.ts" + - "**/*.d.ts" + - "scripts/**" + - "*.md" + - ".github/**" + - "vitest.config.mts" + - "dist/**" + - "coverage/**" diff --git a/extension.ts b/extension.ts index 9e8bf45..ffd8d7b 100644 --- a/extension.ts +++ b/extension.ts @@ -114,7 +114,7 @@ export class BookmarkStore { } } -class FolderGroupItem extends vscode.TreeItem { +export class FolderGroupItem extends vscode.TreeItem { constructor(public readonly folder: BookmarkFolder, public readonly bookmarks: Bookmark[]) { super(folder.name, vscode.TreeItemCollapsibleState.Expanded); this.contextValue = 'bookmarkFolder'; @@ -123,7 +123,7 @@ class FolderGroupItem extends vscode.TreeItem { } } -class BookmarkTreeItem extends vscode.TreeItem { +export class BookmarkTreeItem extends vscode.TreeItem { constructor(public readonly bookmark: Bookmark, descriptionParts: string[]) { super(bookmark.label, vscode.TreeItemCollapsibleState.None); this.description = descriptionParts.join(' • '); @@ -140,7 +140,7 @@ class BookmarkTreeItem extends vscode.TreeItem { type TreeNode = FolderGroupItem | BookmarkTreeItem; -class BookmarksTreeProvider implements vscode.TreeDataProvider { +export class BookmarksTreeProvider implements vscode.TreeDataProvider { private readonly _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; @@ -219,7 +219,7 @@ export function describeBookmark(bookmark: Bookmark, isMultiRoot: boolean, folde return parts; } -function toBookmark(uri: vscode.Uri): Bookmark { +export function toBookmark(uri: vscode.Uri): Bookmark { const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); const workspaceFolderName = workspaceFolder?.name ?? 'Workspace'; const relativePath = workspaceFolder ? vscode.workspace.asRelativePath(uri, false) : uri.fsPath; @@ -236,7 +236,7 @@ function toBookmark(uri: vscode.Uri): Bookmark { }; } -function addActiveFileBookmark(store: BookmarkStore) { +export function addActiveFileBookmark(store: BookmarkStore) { const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showWarningMessage('Open a file to bookmark it.'); @@ -245,14 +245,14 @@ function addActiveFileBookmark(store: BookmarkStore) { store.addBookmark(toBookmark(editor.document.uri)); } -function addBookmarksForUris(store: BookmarkStore, uri: vscode.Uri | undefined, uris: vscode.Uri[] | undefined) { +export function addBookmarksForUris(store: BookmarkStore, uri: vscode.Uri | undefined, uris: vscode.Uri[] | undefined) { const targets = uris && uris.length > 0 ? uris : uri ? [uri] : []; for (const target of targets) { store.addBookmark(toBookmark(target)); } } -async function openBookmark(bookmark: Bookmark) { +export async function openBookmark(bookmark: Bookmark) { try { const uri = vscode.Uri.parse(bookmark.uri); const document = await vscode.workspace.openTextDocument(uri); @@ -262,7 +262,7 @@ async function openBookmark(bookmark: Bookmark) { } } -async function openAllInFolder(store: BookmarkStore, item: FolderGroupItem) { +export async function openAllInFolder(store: BookmarkStore, item: FolderGroupItem) { if (item.bookmarks.length === 0) { vscode.window.showInformationMessage(`"${item.folder.name}" has no bookmarks.`); return; @@ -272,7 +272,7 @@ async function openAllInFolder(store: BookmarkStore, item: FolderGroupItem) { } } -async function createFolder(store: BookmarkStore) { +export async function createFolder(store: BookmarkStore) { const name = await vscode.window.showInputBox({ prompt: 'New bookmark folder name', placeHolder: 'e.g. Backend, In Review, TODO', @@ -283,7 +283,7 @@ async function createFolder(store: BookmarkStore) { } } -async function renameFolder(store: BookmarkStore, item: FolderGroupItem) { +export async function renameFolder(store: BookmarkStore, item: FolderGroupItem) { const name = await vscode.window.showInputBox({ prompt: 'Rename bookmark folder', value: item.folder.name, @@ -294,7 +294,7 @@ async function renameFolder(store: BookmarkStore, item: FolderGroupItem) { } } -async function deleteFolder(store: BookmarkStore, item: FolderGroupItem) { +export async function deleteFolder(store: BookmarkStore, item: FolderGroupItem) { if (item.bookmarks.length > 0) { const confirm = await vscode.window.showWarningMessage( `Delete folder "${item.folder.name}"? ${item.bookmarks.length} bookmark(s) will move to the root list.`, @@ -312,7 +312,7 @@ const NEW_FOLDER_PICK = '$(new-folder) New Folder...'; const NO_FOLDER_PICK = '$(circle-slash) No Folder (root)'; /** Prompts the user to choose a bookmark folder. Returns `undefined` if cancelled, `null` for root. */ -async function pickFolder(store: BookmarkStore, placeHolder: string): Promise { +export async function pickFolder(store: BookmarkStore, placeHolder: string): Promise { const folders = store.getAllFolders(); const picked = await vscode.window.showQuickPick( [NO_FOLDER_PICK, ...folders.map(f => f.name), NEW_FOLDER_PICK], @@ -340,7 +340,7 @@ async function pickFolder(store: BookmarkStore, placeHolder: string): Promise f.name === picked)?.id ?? undefined; } -async function moveToFolder(store: BookmarkStore, item: BookmarkTreeItem) { +export async function moveToFolder(store: BookmarkStore, item: BookmarkTreeItem) { const folderId = await pickFolder(store, `Move "${item.bookmark.label}" to...`); if (folderId === undefined) { return; @@ -348,7 +348,7 @@ async function moveToFolder(store: BookmarkStore, item: BookmarkTreeItem) { store.moveBookmarkToFolder(item.bookmark.id, folderId); } -async function addBookmarksToFolder(store: BookmarkStore, uri: vscode.Uri | undefined, uris: vscode.Uri[] | undefined) { +export async function addBookmarksToFolder(store: BookmarkStore, uri: vscode.Uri | undefined, uris: vscode.Uri[] | undefined) { const targets = uris && uris.length > 0 ? uris : uri ? [uri] : []; if (targets.length === 0) { return; diff --git a/package-lock.json b/package-lock.json index 8df5107..a807b76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@types/node": "^26.2.0", "@types/vscode": "^1.134.0", + "@vitest/coverage-v8": "^4.1.11", "@vscode/vsce": "^3.9.2", "esbuild": "^0.28.2", "typescript": "~6.0.3", @@ -230,6 +231,23 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/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/@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", @@ -240,6 +258,46 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -682,6 +740,16 @@ "node": ">=18" } }, + "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", @@ -689,6 +757,17 @@ "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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1368,6 +1447,37 @@ "node": ">=22.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.11", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", @@ -1762,6 +1872,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -2878,6 +3000,13 @@ "node": ">=10" } }, + "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/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -3107,6 +3236,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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-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-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/istextorbinary": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", @@ -3126,9 +3294,9 @@ } }, "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==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -3643,6 +3811,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "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/markdown-it": { "version": "14.3.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", diff --git a/package.json b/package.json index 39477b4..ad5ebc8 100644 --- a/package.json +++ b/package.json @@ -209,11 +209,13 @@ "package": "bash scripts/rebuild.sh", "clean": "rm -rf dist", "lint": "tsc --noEmit", - "test": "vitest run" + "test": "vitest run", + "test:coverage": "vitest run --coverage" }, "devDependencies": { "@types/node": "^26.2.0", "@types/vscode": "^1.134.0", + "@vitest/coverage-v8": "^4.1.11", "@vscode/vsce": "^3.9.2", "esbuild": "^0.28.2", "typescript": "~6.0.3", diff --git a/scripts/local-patch-report.sh b/scripts/local-patch-report.sh new file mode 100755 index 0000000..a314b92 --- /dev/null +++ b/scripts/local-patch-report.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Local patch-coverage preflight. Reports the same "patch coverage" number +# Codecov's `patch` gate computes (coverage.status.patch in codecov.yml) — +# coverage of only the lines changed in your diff, not the whole project — +# so a real gap shows up before a CI round-trip instead of after. Ported +# from Hestia's scripts/local-patch-report.sh, simplified for a single- +# package TS project: one lcov profile, no backend/frontend split, no path +# prefix translation (vitest's lcov.info already uses repo-root-relative +# paths). See CLAUDE.md's Definition of Done section. +# +# Usage: scripts/local-patch-report.sh [--base ] +# Env: WFB_MIN_COVERAGE (default 85) — minimum patch coverage percent. +# WFB_PATCH_ADVISORY=1 — report only, don't fail on a gap below the +# minimum (still prints PASS/FAIL either way). + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT_DIR" + +BASE_REF="origin/development" +if [ "${1:-}" = "--base" ]; then + BASE_REF="$2" +fi + +MIN_COVERAGE="${WFB_MIN_COVERAGE:-85}" +ADVISORY="${WFB_PATCH_ADVISORY:-0}" + +MERGE_BASE="$(git merge-base "$BASE_REF" HEAD)" +echo "Diffing HEAD against ${BASE_REF} (merge-base ${MERGE_BASE:0:12})" +echo "" + +# Same exclusions as codecov.yml's ignore: list — keep the two in sync +# manually. +DIFF_FILE="$(mktemp)" +trap 'rm -f "$DIFF_FILE"' EXIT + +git diff --unified=0 "$MERGE_BASE"...HEAD -- \ + '*.ts' \ + ':(exclude)test/**' \ + ':(exclude)**/*.test.ts' \ + ':(exclude)**/*.d.ts' \ + ':(exclude)vitest.config.mts' \ + >"$DIFF_FILE" + +if [ ! -s "$DIFF_FILE" ]; then + echo "No coverable changed lines in the diff — nothing to report." + exit 0 +fi + +echo "--- Regenerating coverage profile ---" +# The project-wide threshold gate in vitest.config.mts may fail here (that's +# a different, stricter question than patch coverage) — don't let it abort +# this script; only a real test failure (no lcov produced at all) should. +npm run test:coverage >/dev/null || true +if [ ! -f "$ROOT_DIR/coverage/lcov.info" ]; then + echo "FAIL: tests did not produce a coverage profile" >&2 + exit 1 +fi + +echo "" +echo "--- Patch coverage ---" + +python3 - "$DIFF_FILE" "$ROOT_DIR" "$MIN_COVERAGE" "$ADVISORY" <<'PY' +import re +import sys + +diff_file, root_dir, min_coverage_raw, advisory_raw = sys.argv[1:5] +min_coverage = float(min_coverage_raw) +advisory = advisory_raw == "1" + + +def parse_diff_changed_lines(path): + """Return {file_path: set(added_line_numbers)} from a unified=0 diff.""" + changed = {} + current_file = None + current_line = None + hunk_re = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") + + with open(path) as f: + for line in f: + if line.startswith("+++ b/"): + current_file = line[6:].rstrip("\n") + changed.setdefault(current_file, set()) + current_line = None + continue + if line.startswith("--- ") or line.startswith("diff --git"): + continue + m = hunk_re.match(line) + if m: + current_line = int(m.group(1)) + continue + if current_file is None or current_line is None: + continue + if line.startswith("+") and not line.startswith("+++"): + changed[current_file].add(current_line) + current_line += 1 + elif line.startswith("-") and not line.startswith("---"): + continue + else: + current_line += 1 + return {f: lines for f, lines in changed.items() if lines} + + +def parse_lcov(path): + """Return {file_path: {line: hit_count}} from an lcov.info file.""" + result = {} + current = None + try: + with open(path) as f: + for line in f: + line = line.strip() + if line.startswith("SF:"): + current = line[3:] + result.setdefault(current, {}) + elif line.startswith("DA:") and current is not None: + parts = line[3:].split(",") + ln, hits = int(parts[0]), int(parts[1]) + per_line = result[current] + per_line[ln] = per_line.get(ln, 0) or hits + elif line == "end_of_record": + current = None + except FileNotFoundError: + pass + return result + + +changed = parse_diff_changed_lines(diff_file) +lcov = parse_lcov(f"{root_dir}/coverage/lcov.info") + +total_coverable = 0 +total_covered = 0 +rows = [] + +for file_path, lines in sorted(changed.items()): + per_line = lcov.get(file_path, {}) + coverable = [ln for ln in sorted(lines) if ln in per_line] + if not coverable: + continue + covered = [ln for ln in coverable if per_line[ln] > 0] + total_coverable += len(coverable) + total_covered += len(covered) + pct = 100.0 * len(covered) / len(coverable) + rows.append((file_path, len(covered), len(coverable), pct)) + +for file_path, covered, coverable, pct in rows: + uncovered_note = "" if covered == coverable else " <-- gap" + print(f" {pct:6.1f}% ({covered}/{coverable}) {file_path}{uncovered_note}") + +if total_coverable == 0: + print("\nNo changed lines overlapped with coverage data (e.g. non-statement lines only).") + sys.exit(0) + +overall = 100.0 * total_covered / total_coverable +print(f"\nPatch coverage: {overall:.1f}% ({total_covered}/{total_coverable}) — minimum {min_coverage}%") + +if overall < min_coverage: + print("FAIL: patch coverage below minimum" + (" (advisory — not failing)" if advisory else "")) + sys.exit(0 if advisory else 1) + +print("PASS: patch coverage meets the minimum") +PY diff --git a/test/activate.test.ts b/test/activate.test.ts new file mode 100644 index 0000000..c9abfb5 --- /dev/null +++ b/test/activate.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { activate, deactivate } from '../extension'; +import { createFakeContext } from './fakeContext'; +import { commands, window } from './vscode-mock'; + +describe('activate', () => { + it('creates the tree view and registers every command', () => { + const context = createFakeContext(); + (context as any).subscriptions = []; + + activate(context as any); + + expect(window.createTreeView).toHaveBeenCalledOnce(); + expect(commands.registerCommand).toHaveBeenCalledTimes(12); + expect((context as any).subscriptions).toHaveLength(13); // tree view + 12 commands + }); + + it('exercises every registered command handler at least once', async () => { + const context = createFakeContext(); + (context as any).subscriptions = []; + + activate(context as any); + + const calls = commands.registerCommand.mock.calls as unknown as Array<[string, (...args: any[]) => unknown]>; + const handlers = new Map(calls); + const bookmark = { id: 'b', uri: 'file:///repo/a.ts', label: 'a.ts', relativePath: 'a.ts', workspaceFolderName: 'repo', folderId: null, createdAt: 0 }; + const folder = { id: 'f', name: 'Backend', createdAt: 0 }; + + await handlers.get('workspace-file-bookmarks.addBookmark')?.(); + await handlers.get('workspace-file-bookmarks.addBookmarkFromExplorer')?.(undefined, undefined); + await handlers.get('workspace-file-bookmarks.addBookmarkToFolder')?.(undefined, undefined); + await handlers.get('workspace-file-bookmarks.removeBookmark')?.({ bookmark }); + await handlers.get('workspace-file-bookmarks.openBookmark')?.(bookmark); + await handlers.get('workspace-file-bookmarks.createFolder')?.(); + await handlers.get('workspace-file-bookmarks.renameFolder')?.({ folder }); + await handlers.get('workspace-file-bookmarks.deleteFolder')?.({ folder, bookmarks: [] }); + await handlers.get('workspace-file-bookmarks.moveToFolder')?.({ bookmark }); + await handlers.get('workspace-file-bookmarks.openAllInFolder')?.({ folder, bookmarks: [] }); + await handlers.get('workspace-file-bookmarks.setViewModeList')?.(); + await handlers.get('workspace-file-bookmarks.setViewModeTree')?.(); + + expect(handlers.size).toBe(12); + }); +}); + +describe('deactivate', () => { + it('is a no-op', () => { + expect(() => deactivate()).not.toThrow(); + }); +}); diff --git a/test/commands.test.ts b/test/commands.test.ts new file mode 100644 index 0000000..baeaa6f --- /dev/null +++ b/test/commands.test.ts @@ -0,0 +1,315 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + BookmarkStore, + addActiveFileBookmark, + addBookmarksForUris, + addBookmarksToFolder, + createFolder, + deleteFolder, + moveToFolder, + openAllInFolder, + openBookmark, + pickFolder, + renameFolder, + type Bookmark, + type BookmarkFolder +} from '../extension'; +import { createFakeContext } from './fakeContext'; +import { Uri, window, workspace } from './vscode-mock'; + +function newStore() { + return new BookmarkStore(createFakeContext() as any); +} + +function makeBookmark(overrides: Partial = {}): Bookmark { + return { + id: 'id-1', + uri: 'file:///repo/src/a.ts', + label: 'a.ts', + relativePath: 'src/a.ts', + workspaceFolderName: 'repo', + folderId: null, + createdAt: 0, + ...overrides + }; +} + +function makeFolder(overrides: Partial = {}): BookmarkFolder { + return { id: 'folder-1', name: 'Backend', createdAt: 0, ...overrides }; +} + +describe('addActiveFileBookmark', () => { + it('bookmarks the active editor document', () => { + const store = newStore(); + workspace.getWorkspaceFolder.mockReturnValue(undefined); + window.activeTextEditor = { document: { uri: Uri.file('/repo/a.ts') } }; + + addActiveFileBookmark(store); + + expect(store.getAllBookmarks()).toHaveLength(1); + }); + + it('warns and does nothing when no file is open', () => { + const store = newStore(); + window.activeTextEditor = undefined; + + addActiveFileBookmark(store); + + expect(window.showWarningMessage).toHaveBeenCalledOnce(); + expect(store.getAllBookmarks()).toHaveLength(0); + }); +}); + +describe('addBookmarksForUris', () => { + it('bookmarks every uri in a multi-select', () => { + const store = newStore(); + workspace.getWorkspaceFolder.mockReturnValue(undefined); + + addBookmarksForUris(store, undefined, [Uri.file('/repo/a.ts') as any, Uri.file('/repo/b.ts') as any]); + + expect(store.getAllBookmarks()).toHaveLength(2); + }); + + it('falls back to the single uri when uris is empty', () => { + const store = newStore(); + workspace.getWorkspaceFolder.mockReturnValue(undefined); + + addBookmarksForUris(store, Uri.file('/repo/a.ts') as any, undefined); + + expect(store.getAllBookmarks()).toHaveLength(1); + }); + + it('does nothing when neither uri nor uris is provided', () => { + const store = newStore(); + + addBookmarksForUris(store, undefined, undefined); + + expect(store.getAllBookmarks()).toHaveLength(0); + }); +}); + +describe('openBookmark', () => { + it('opens the document in a non-preview editor', async () => { + const document = {}; + workspace.openTextDocument.mockResolvedValue(document); + + await openBookmark(makeBookmark()); + + expect(workspace.openTextDocument).toHaveBeenCalled(); + expect(window.showTextDocument).toHaveBeenCalledWith(document, { preview: false }); + }); + + it('shows an error when the file cannot be opened', async () => { + workspace.openTextDocument.mockRejectedValue(new Error('not found')); + + await openBookmark(makeBookmark()); + + expect(window.showErrorMessage).toHaveBeenCalledOnce(); + }); +}); + +describe('openAllInFolder', () => { + it('opens every bookmark in the folder', async () => { + const document = {}; + workspace.openTextDocument.mockResolvedValue(document); + const item = { folder: makeFolder(), bookmarks: [makeBookmark({ id: 'a' }), makeBookmark({ id: 'b' })] }; + + await openAllInFolder(newStore(), item as any); + + expect(workspace.openTextDocument).toHaveBeenCalledTimes(2); + }); + + it('shows an info message instead of opening when the folder is empty', async () => { + const item = { folder: makeFolder(), bookmarks: [] }; + + await openAllInFolder(newStore(), item as any); + + expect(window.showInformationMessage).toHaveBeenCalledOnce(); + expect(workspace.openTextDocument).not.toHaveBeenCalled(); + }); +}); + +describe('createFolder', () => { + it('creates a folder from trimmed input', async () => { + const store = newStore(); + window.showInputBox.mockResolvedValue(' Backend '); + + await createFolder(store); + + expect(store.getAllFolders()).toHaveLength(1); + expect(store.getAllFolders()[0].name).toBe('Backend'); + + const validateInput = window.showInputBox.mock.calls[0][0].validateInput; + expect(validateInput(' ')).toMatch(/cannot be empty/); + expect(validateInput('Backend')).toBeUndefined(); + }); + + it('does nothing when the input is cancelled', async () => { + const store = newStore(); + window.showInputBox.mockResolvedValue(undefined); + + await createFolder(store); + + expect(store.getAllFolders()).toHaveLength(0); + }); +}); + +describe('renameFolder', () => { + it('renames the folder from trimmed input', async () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + window.showInputBox.mockResolvedValue(' API '); + + await renameFolder(store, { folder } as any); + + expect(store.getAllFolders()[0].name).toBe('API'); + + const validateInput = window.showInputBox.mock.calls[0][0].validateInput; + expect(validateInput(' ')).toMatch(/cannot be empty/); + expect(validateInput('API')).toBeUndefined(); + }); + + it('does nothing when the input is cancelled', async () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + window.showInputBox.mockResolvedValue(undefined); + + await renameFolder(store, { folder } as any); + + expect(store.getAllFolders()[0].name).toBe('Backend'); + }); +}); + +describe('deleteFolder', () => { + it('deletes an empty folder without confirming', async () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + + await deleteFolder(store, { folder, bookmarks: [] } as any); + + expect(store.getAllFolders()).toHaveLength(0); + expect(window.showWarningMessage).not.toHaveBeenCalled(); + }); + + it('deletes a non-empty folder only after confirmation', async () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + window.showWarningMessage.mockResolvedValue('Delete'); + + await deleteFolder(store, { folder, bookmarks: [makeBookmark()] } as any); + + expect(store.getAllFolders()).toHaveLength(0); + }); + + it('keeps a non-empty folder when the confirmation is declined', async () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + window.showWarningMessage.mockResolvedValue(undefined); + + await deleteFolder(store, { folder, bookmarks: [makeBookmark()] } as any); + + expect(store.getAllFolders()).toHaveLength(1); + }); +}); + +describe('pickFolder', () => { + it('returns null for the root/no-folder pick', async () => { + const store = newStore(); + window.showQuickPick.mockResolvedValue('$(circle-slash) No Folder (root)'); + + await expect(pickFolder(store, 'placeholder')).resolves.toBeNull(); + }); + + it('returns undefined when the quick pick is cancelled', async () => { + const store = newStore(); + window.showQuickPick.mockResolvedValue(undefined); + + await expect(pickFolder(store, 'placeholder')).resolves.toBeUndefined(); + }); + + it('returns the matching folder id for an existing folder', async () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + window.showQuickPick.mockResolvedValue('Backend'); + + await expect(pickFolder(store, 'placeholder')).resolves.toBe(folder.id); + }); + + it('creates a new folder and returns its id', async () => { + const store = newStore(); + window.showQuickPick.mockResolvedValue('$(new-folder) New Folder...'); + window.showInputBox.mockResolvedValue('Frontend'); + + const id = await pickFolder(store, 'placeholder'); + + expect(store.getAllFolders().find(f => f.id === id)?.name).toBe('Frontend'); + + const validateInput = window.showInputBox.mock.calls[0][0].validateInput; + expect(validateInput(' ')).toMatch(/cannot be empty/); + expect(validateInput('Frontend')).toBeUndefined(); + }); + + it('returns undefined when creating a new folder is cancelled', async () => { + const store = newStore(); + window.showQuickPick.mockResolvedValue('$(new-folder) New Folder...'); + window.showInputBox.mockResolvedValue(undefined); + + await expect(pickFolder(store, 'placeholder')).resolves.toBeUndefined(); + }); +}); + +describe('moveToFolder', () => { + it('moves the bookmark to the picked folder', async () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a' })); + const folder = store.createFolder('Backend'); + window.showQuickPick.mockResolvedValue('Backend'); + + await moveToFolder(store, { bookmark: makeBookmark({ id: 'a' }) } as any); + + expect(store.getAllBookmarks()[0].folderId).toBe(folder.id); + }); + + it('does nothing when the picker is cancelled', async () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a', folderId: 'existing' })); + window.showQuickPick.mockResolvedValue(undefined); + + await moveToFolder(store, { bookmark: makeBookmark({ id: 'a' }) } as any); + + expect(store.getAllBookmarks()[0].folderId).toBe('existing'); + }); +}); + +describe('addBookmarksToFolder', () => { + it('adds every target uri into the picked folder', async () => { + const store = newStore(); + workspace.getWorkspaceFolder.mockReturnValue(undefined); + const folder = store.createFolder('Backend'); + window.showQuickPick.mockResolvedValue('Backend'); + + await addBookmarksToFolder(store, undefined, [Uri.file('/repo/a.ts') as any, Uri.file('/repo/b.ts') as any]); + + const bookmarks = store.getAllBookmarks(); + expect(bookmarks).toHaveLength(2); + expect(bookmarks.every(b => b.folderId === folder.id)).toBe(true); + }); + + it('does nothing when there are no targets', async () => { + const store = newStore(); + + await addBookmarksToFolder(store, undefined, undefined); + + expect(window.showQuickPick).not.toHaveBeenCalled(); + }); + + it('does nothing when the picker is cancelled', async () => { + const store = newStore(); + workspace.getWorkspaceFolder.mockReturnValue(undefined); + window.showQuickPick.mockResolvedValue(undefined); + + await addBookmarksToFolder(store, Uri.file('/repo/a.ts') as any, undefined); + + expect(store.getAllBookmarks()).toHaveLength(0); + }); +}); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..9dcffcc --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,8 @@ +import { afterEach, vi } from 'vitest'; +import { window, workspace } from './vscode-mock'; + +afterEach(() => { + vi.clearAllMocks(); + window.activeTextEditor = undefined; + workspace.workspaceFolders = undefined; +}); diff --git a/test/toBookmark.test.ts b/test/toBookmark.test.ts new file mode 100644 index 0000000..f635b2a --- /dev/null +++ b/test/toBookmark.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { toBookmark } from '../extension'; +import { Uri, workspace } from './vscode-mock'; + +describe('toBookmark', () => { + it('uses the workspace-relative path and folder name inside a workspace folder', () => { + const uri = Uri.file('/repo/src/a.ts'); + workspace.getWorkspaceFolder.mockReturnValue({ name: 'repo', uri: Uri.file('/repo') }); + workspace.asRelativePath.mockReturnValue('src/a.ts'); + + const bookmark = toBookmark(uri as any); + + expect(bookmark.relativePath).toBe('src/a.ts'); + expect(bookmark.workspaceFolderName).toBe('repo'); + expect(bookmark.label).toBe('a.ts'); + expect(bookmark.folderId).toBeNull(); + expect(bookmark.uri).toBe(uri.toString()); + }); + + it('falls back to the raw fsPath and "Workspace" when outside any workspace folder', () => { + const uri = Uri.file('/outside/b.ts'); + workspace.getWorkspaceFolder.mockReturnValue(undefined); + + const bookmark = toBookmark(uri as any); + + expect(bookmark.relativePath).toBe('/outside/b.ts'); + expect(bookmark.workspaceFolderName).toBe('Workspace'); + expect(bookmark.label).toBe('b.ts'); + }); +}); diff --git a/test/treeProvider.test.ts b/test/treeProvider.test.ts new file mode 100644 index 0000000..41d6ecd --- /dev/null +++ b/test/treeProvider.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect } from 'vitest'; +import { BookmarkStore, BookmarksTreeProvider, FolderGroupItem, BookmarkTreeItem, type Bookmark } from '../extension'; +import { createFakeContext } from './fakeContext'; +import { workspace, Uri } from './vscode-mock'; + +function makeBookmark(overrides: Partial = {}): Bookmark { + const id = overrides.id ?? 'id-1'; + return { + id, + uri: `file:///repo/src/${id}.ts`, + label: `${id}.ts`, + relativePath: `src/${id}.ts`, + workspaceFolderName: 'repo', + folderId: null, + createdAt: 0, + ...overrides + }; +} + +function newProvider() { + const context = createFakeContext(); + const store = new BookmarkStore(context as any); + const provider = new BookmarksTreeProvider(store, context as any); + return { store, provider }; +} + +describe('BookmarksTreeProvider', () => { + it('defaults to tree view mode', () => { + const { provider } = newProvider(); + expect(provider.getViewMode()).toBe('tree'); + }); + + it('switches view mode and persists it', () => { + const { provider } = newProvider(); + provider.setViewMode('list'); + expect(provider.getViewMode()).toBe('list'); + }); + + it('getTreeItem returns the element unchanged', () => { + const { provider } = newProvider(); + const item = new BookmarkTreeItem(makeBookmark(), ['src/a.ts']); + expect(provider.getTreeItem(item)).toBe(item); + }); + + describe('list mode', () => { + it('returns all bookmarks newest-first, ignoring folders', () => { + const { store, provider } = newProvider(); + provider.setViewMode('list'); + store.createFolder('Backend'); + store.addBookmark(makeBookmark({ id: 'old', createdAt: 1 })); + store.addBookmark(makeBookmark({ id: 'new', createdAt: 2 })); + + const children = provider.getChildren() as BookmarkTreeItem[]; + + expect(children.map(c => c.bookmark.id)).toEqual(['new', 'old']); + }); + + it('returns no children for any element (flat list)', () => { + const { provider } = newProvider(); + provider.setViewMode('list'); + const item = new BookmarkTreeItem(makeBookmark(), []); + + expect(provider.getChildren(item)).toEqual([]); + }); + }); + + describe('tree mode', () => { + it('groups bookmarks under their folder, alphabetized, and lists ungrouped ones after', () => { + const { store, provider } = newProvider(); + const zFolder = store.createFolder('Zeta'); + const aFolder = store.createFolder('Alpha'); + store.addBookmark(makeBookmark({ id: 'grouped', folderId: aFolder.id })); + store.addBookmark(makeBookmark({ id: 'ungrouped-old', folderId: null, createdAt: 1 })); + store.addBookmark(makeBookmark({ id: 'ungrouped-new', folderId: null, createdAt: 2 })); + + const children = provider.getChildren(); + + expect(children.filter((c): c is FolderGroupItem => c instanceof FolderGroupItem).map(f => f.folder.name)).toEqual([ + 'Alpha', + 'Zeta' + ]); + expect(children.filter((c): c is BookmarkTreeItem => c instanceof BookmarkTreeItem).map(b => b.bookmark.id)).toEqual([ + 'ungrouped-new', + 'ungrouped-old' + ]); + void zFolder; + }); + + it('returns the bookmarks inside a folder, newest first, when expanded', () => { + const { store, provider } = newProvider(); + const folder = store.createFolder('Backend'); + store.addBookmark(makeBookmark({ id: 'old', folderId: folder.id, createdAt: 1 })); + store.addBookmark(makeBookmark({ id: 'new', folderId: folder.id, createdAt: 2 })); + + const [folderNode] = provider.getChildren(); + const children = provider.getChildren(folderNode) as BookmarkTreeItem[]; + + expect(children.map(c => c.bookmark.id)).toEqual(['new', 'old']); + }); + + it('returns no children for a leaf bookmark node', () => { + const { store, provider } = newProvider(); + store.addBookmark(makeBookmark()); + const [bookmarkNode] = provider.getChildren(); + + expect(provider.getChildren(bookmarkNode)).toEqual([]); + }); + + it('includes the workspace folder name in a multi-root workspace', () => { + const { store, provider } = newProvider(); + workspace.workspaceFolders = [ + { name: 'repo-a', uri: Uri.file('/repo-a') }, + { name: 'repo-b', uri: Uri.file('/repo-b') } + ]; + store.addBookmark(makeBookmark()); + + const [bookmarkNode] = provider.getChildren() as BookmarkTreeItem[]; + + expect(bookmarkNode.description).toContain('repo'); + }); + }); +}); diff --git a/test/vscode-mock.ts b/test/vscode-mock.ts index 80bf6c4..e4489b5 100644 --- a/test/vscode-mock.ts +++ b/test/vscode-mock.ts @@ -1,6 +1,7 @@ // Minimal stand-in for the 'vscode' module, covering only the surface that -// BookmarkStore (the code under test) actually touches at runtime. Not a -// general-purpose vscode mock — extend it if tests start exercising more. +// extension.ts actually touches at runtime. Not a general-purpose vscode +// mock — extend it if tests start exercising more. +import { vi } from 'vitest'; export class EventEmitter { private listeners: Array<(e: T) => void> = []; @@ -17,20 +18,57 @@ export class EventEmitter { } } -export const window = { - showInformationMessage: (..._args: unknown[]) => {}, - showWarningMessage: (..._args: unknown[]) => {} -}; +export class Uri { + private constructor(public readonly fsPath: string, private readonly raw: string) {} + + static parse(value: string): Uri { + return new Uri(value.replace(/^file:\/\//, ''), value); + } + + static file(path: string): Uri { + return new Uri(path, `file://${path}`); + } + + toString() { + return this.raw; + } +} -// Referenced only at class-definition time (extends/enum access) when the -// whole extension.ts module loads for testing — never actually exercised by -// BookmarkStore/describeBookmark, so these stay stubs. export const TreeItemCollapsibleState = { None: 0, Collapsed: 1, Expanded: 2 }; export class TreeItem { + contextValue?: string; + iconPath?: unknown; + description?: string; + tooltip?: string; + command?: unknown; + constructor(public label?: unknown, public collapsibleState?: unknown) {} } export class ThemeIcon { constructor(public id: string) {} } + +export const window = { + activeTextEditor: undefined as { document: { uri: Uri } } | undefined, + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + showErrorMessage: vi.fn(), + showInputBox: vi.fn(), + showQuickPick: vi.fn(), + showTextDocument: vi.fn(), + createTreeView: vi.fn(() => ({ dispose: () => {} })) +}; + +export const workspace = { + workspaceFolders: undefined as Array<{ name: string; uri: Uri }> | undefined, + getWorkspaceFolder: vi.fn(), + asRelativePath: vi.fn((uri: Uri) => uri.fsPath), + openTextDocument: vi.fn() +}; + +export const commands = { + registerCommand: vi.fn(() => ({ dispose: () => {} })), + executeCommand: vi.fn() +}; diff --git a/vitest.config.mts b/vitest.config.mts index ffe237c..8f88dbc 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -6,5 +6,20 @@ export default defineConfig({ alias: { vscode: path.resolve(import.meta.dirname, 'test/vscode-mock.ts') } + }, + test: { + setupFiles: ['./test/setup.ts'], + clearMocks: true, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['extension.ts'], + thresholds: { + lines: 85, + statements: 85, + functions: 85, + branches: 85 + } + } } }); From a1100816de1ce59e34f95585debba4f8540295e7 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Thu, 20 Aug 2026 12:01:12 -0400 Subject: [PATCH 04/17] ci: trigger a run to verify Codecov upload with the new token From 9cb53b356c3103c99c747688a049e0d21ae2fb27 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:04:03 -0400 Subject: [PATCH 05/17] chore(deps): update codecov/codecov-action action to v7 (#35) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9bfb47..1daf733 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: run: npm run test:coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage/lcov.info From 481157bd952cf7374838d8019b5ea56d915991d1 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Thu, 20 Aug 2026 23:21:08 -0400 Subject: [PATCH 06/17] feat(ci): add secret scanning, dependency audit, SBOM and provenance attestation --- .github/workflows/ci.yml | 21 +++++++++++++++++++ .github/workflows/release-please.yml | 30 +++++++++++++++++++++------- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1daf733..3724f2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,21 @@ permissions: contents: read jobs: + secret-scan: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + + - name: Gitleaks + uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build: runs-on: ubuntu-latest steps: @@ -24,6 +39,12 @@ jobs: - name: Install dependencies run: npm ci + # SCA gate: fails on high/critical CVEs in the dependency tree. Kept in step + # with wfb_dep_update.sh's audit gate, but runs on every push/PR instead of + # only when that script is invoked manually. + - name: Audit dependencies + run: npm audit --audit-level=high + - name: Lint run: npm run lint diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 4d8d078..0104ebc 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -42,6 +42,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write + attestations: write env: VS_MARKETPLACE_TOKEN: ${{ secrets.VS_MARKETPLACE_TOKEN }} OPEN_VSX_TOKEN: ${{ secrets.OPEN_VSX_TOKEN }} @@ -84,6 +86,19 @@ jobs: - name: Package extension run: npx @vscode/vsce package + # CycloneDX SBOM covering every prod dependency in the packaged .vsix, for + # downstream vulnerability/license auditing without re-resolving the tree. + - name: Generate SBOM + run: npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --output-file sbom.cdx.json + + # Cryptographically binds the .vsix and its SBOM to this workflow run and + # commit (Sigstore-backed, keyless — no secrets to manage). Verify with: + # gh attestation verify -R ${{ github.repository }} + - name: Attest build provenance + uses: actions/attest-build-provenance@8beda2b7ed98355c0e97c0a63bec38ae472e66c4 # v4 + with: + subject-path: '*.vsix, sbom.cdx.json' + # release-please-config.json sets draft:true for this package specifically so this step can # run at all: GitHub's immutable-releases feature locks a release the instant it's published, # so assets can only be attached while it's still a draft. We un-draft it in the next step, @@ -99,19 +114,20 @@ jobs: # Only meaningful right after release-please creates the (draft) release. On a manual # re-run the release is already published and non-draft, so attaching again would just # 422 on the duplicate asset name — skip both steps in that case. - - name: Attach VSIX to GitHub Release + - name: Attach VSIX and SBOM to GitHub Release if: ${{ needs.release-please.outputs.release_created == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} UPLOAD_URL: ${{ steps.resolve.outputs.upload_url }} run: | - VSIX_FILE=$(ls *.vsix) BASE_URL="${UPLOAD_URL%%\{*}" - curl -sSf -X POST \ - -H "Authorization: token ${GH_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary @"${VSIX_FILE}" \ - "${BASE_URL}?name=${VSIX_FILE}" + for ASSET in *.vsix sbom.cdx.json; do + curl -sSf -X POST \ + -H "Authorization: token ${GH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"${ASSET}" \ + "${BASE_URL}?name=${ASSET}" + done - name: Publish GitHub Release if: ${{ needs.release-please.outputs.release_created == 'true' }} From 7f0ee6dde3a9e7e1791b89bce6013c9285ea7a21 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Thu, 20 Aug 2026 23:25:52 -0400 Subject: [PATCH 07/17] fix(scripts): use indexed loop and clarify shellcheck comment --- scripts/wfb_dep_update.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/wfb_dep_update.sh b/scripts/wfb_dep_update.sh index 4f12fc2..c05fd24 100755 --- a/scripts/wfb_dep_update.sh +++ b/scripts/wfb_dep_update.sh @@ -28,7 +28,8 @@ NPM_MODULES=( "$REPO_ROOT" ) -for MODULE in "${NPM_MODULES[@]}"; do +for (( i=0; i<${#NPM_MODULES[@]}; i++ )); do + MODULE="${NPM_MODULES[i]}" echo "============================================================================" echo "Updating: $MODULE" echo "============================================================================" @@ -46,8 +47,9 @@ for MODULE in "${NPM_MODULES[@]}"; do # ncu only touches dependency ranges, not engines.vscode, so keep the # minimum supported VS Code version in lockstep with @types/vscode here. # vsce refuses to package when engines.vscode is behind @types/vscode. - # shellcheck disable=SC2016 -- single-quoted on purpose: the ${...} below - # is a JS template literal for node to evaluate, not a shell expansion. + # Single-quoted on purpose: the ${...} below is a JS template literal + # for node to evaluate, not a shell expansion. + # shellcheck disable=SC2016 node -e ' const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); From 6b7c474637074782c8ee66fdc66a946d8b0ff094 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 00:38:09 -0400 Subject: [PATCH 08/17] chore: update default reason for manual dev-to-main promotion --- .github/workflows/promote-dev-to-main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/promote-dev-to-main.yml b/.github/workflows/promote-dev-to-main.yml index f3cd6ca..3e2d812 100644 --- a/.github/workflows/promote-dev-to-main.yml +++ b/.github/workflows/promote-dev-to-main.yml @@ -13,7 +13,7 @@ on: reason: description: 'Why are you running this manually?' required: true - default: 'Ad-hoc promotion request' + default: 'Feature Release' skip_health_check: description: 'Skip the CI health check on development?' required: false From 69888334af059a1b81677275095280c118307bd5 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 00:47:32 -0400 Subject: [PATCH 09/17] chore: shift dev-to-main promotion cron to 12:30 UTC Mondays --- .github/workflows/promote-dev-to-main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/promote-dev-to-main.yml b/.github/workflows/promote-dev-to-main.yml index 3e2d812..89a1a05 100644 --- a/.github/workflows/promote-dev-to-main.yml +++ b/.github/workflows/promote-dev-to-main.yml @@ -7,7 +7,7 @@ name: Promote development to main on: schedule: - - cron: '23 12 * * 1' # Mondays ~12:23 UTC, off the top of the hour + - cron: '30 12 * * 1' # Mondays ~12:30 UTC, off the top of the hour workflow_dispatch: inputs: reason: From 2f35967406233ea34129e36f6cad0649a37b3a5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:06:05 -0400 Subject: [PATCH 10/17] chore(deps): update actions/attest-build-provenance digest to 4d10147 (#36) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 0104ebc..261ce95 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -95,7 +95,7 @@ jobs: # commit (Sigstore-backed, keyless — no secrets to manage). Verify with: # gh attestation verify -R ${{ github.repository }} - name: Attest build provenance - uses: actions/attest-build-provenance@8beda2b7ed98355c0e97c0a63bec38ae472e66c4 # v4 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 with: subject-path: '*.vsix, sbom.cdx.json' From 12a53367cca1e34ba6ad325f3b2aa95b3a9d41db Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 12:59:42 -0400 Subject: [PATCH 11/17] chore: bump codeql-action/init to latest v4 commit --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d786dbe..1ae19da 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,7 +31,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 + - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 with: languages: javascript-typescript queries: security-and-quality From ab17af975d3e6a75dde449baa172547eef1f53c1 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 13:07:01 -0400 Subject: [PATCH 12/17] chore: bump codeql-action/analyze to latest v4 commit hash --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1ae19da..4426d4c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,6 +36,6 @@ jobs: languages: javascript-typescript queries: security-and-quality - - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 + - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 with: category: /language:javascript-typescript From b02679219c71b1ae92f029a7edf9c2bdd53402b5 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 16:18:16 -0400 Subject: [PATCH 13/17] feat: rename a bookmark's display label Adds a renameBookmark command (context menu on a bookmark item, mirroring renameFolder) that lets users override the filename-derived label so bookmarks with the same filename across repos can be told apart. Closes #37 --- extension.ts | 16 ++++++++++++++++ package.json | 10 ++++++++++ test/activate.test.ts | 7 ++++--- test/bookmarkStore.test.ts | 7 +++++++ test/commands.test.ts | 29 +++++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/extension.ts b/extension.ts index ffd8d7b..9596e0c 100644 --- a/extension.ts +++ b/extension.ts @@ -40,6 +40,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('workspace-file-bookmarks.addBookmarkFromExplorer', (uri: vscode.Uri | undefined, uris: vscode.Uri[] | undefined) => addBookmarksForUris(store, uri, uris)), vscode.commands.registerCommand('workspace-file-bookmarks.addBookmarkToFolder', (uri: vscode.Uri | undefined, uris: vscode.Uri[] | undefined) => addBookmarksToFolder(store, uri, uris)), vscode.commands.registerCommand('workspace-file-bookmarks.removeBookmark', (item: BookmarkTreeItem) => store.removeBookmark(item.bookmark.id)), + vscode.commands.registerCommand('workspace-file-bookmarks.renameBookmark', (item: BookmarkTreeItem) => renameBookmark(store, item)), vscode.commands.registerCommand('workspace-file-bookmarks.openBookmark', (bookmark: Bookmark) => openBookmark(bookmark)), vscode.commands.registerCommand('workspace-file-bookmarks.createFolder', () => createFolder(store)), vscode.commands.registerCommand('workspace-file-bookmarks.renameFolder', (item: FolderGroupItem) => renameFolder(store, item)), @@ -84,6 +85,10 @@ export class BookmarkStore { this.setBookmarks(this.getAllBookmarks().map(b => (b.id === id ? { ...b, folderId } : b))); } + renameBookmark(id: string, label: string) { + this.setBookmarks(this.getAllBookmarks().map(b => (b.id === id ? { ...b, label } : b))); + } + createFolder(name: string): BookmarkFolder { const folder: BookmarkFolder = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, @@ -294,6 +299,17 @@ export async function renameFolder(store: BookmarkStore, item: FolderGroupItem) } } +export async function renameBookmark(store: BookmarkStore, item: BookmarkTreeItem) { + const label = await vscode.window.showInputBox({ + prompt: 'Rename bookmark', + value: item.bookmark.label, + validateInput: value => (value.trim().length === 0 ? 'Bookmark label cannot be empty.' : undefined) + }); + if (label) { + store.renameBookmark(item.bookmark.id, label.trim()); + } +} + export async function deleteFolder(store: BookmarkStore, item: FolderGroupItem) { if (item.bookmarks.length > 0) { const confirm = await vscode.window.showWarningMessage( diff --git a/package.json b/package.json index ad5ebc8..c5f7025 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,11 @@ "category": "Workspace File Bookmarks", "icon": "$(trash)" }, + { + "command": "workspace-file-bookmarks.renameBookmark", + "title": "Rename Bookmark", + "category": "Workspace File Bookmarks" + }, { "command": "workspace-file-bookmarks.createFolder", "title": "New Bookmark Folder", @@ -155,6 +160,11 @@ "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", "group": "inline" }, + { + "command": "workspace-file-bookmarks.renameBookmark", + "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", + "group": "1_modification" + }, { "command": "workspace-file-bookmarks.moveToFolder", "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", diff --git a/test/activate.test.ts b/test/activate.test.ts index c9abfb5..aeeca56 100644 --- a/test/activate.test.ts +++ b/test/activate.test.ts @@ -11,8 +11,8 @@ describe('activate', () => { activate(context as any); expect(window.createTreeView).toHaveBeenCalledOnce(); - expect(commands.registerCommand).toHaveBeenCalledTimes(12); - expect((context as any).subscriptions).toHaveLength(13); // tree view + 12 commands + expect(commands.registerCommand).toHaveBeenCalledTimes(13); + expect((context as any).subscriptions).toHaveLength(14); // tree view + 13 commands }); it('exercises every registered command handler at least once', async () => { @@ -30,6 +30,7 @@ describe('activate', () => { await handlers.get('workspace-file-bookmarks.addBookmarkFromExplorer')?.(undefined, undefined); await handlers.get('workspace-file-bookmarks.addBookmarkToFolder')?.(undefined, undefined); await handlers.get('workspace-file-bookmarks.removeBookmark')?.({ bookmark }); + await handlers.get('workspace-file-bookmarks.renameBookmark')?.({ bookmark }); await handlers.get('workspace-file-bookmarks.openBookmark')?.(bookmark); await handlers.get('workspace-file-bookmarks.createFolder')?.(); await handlers.get('workspace-file-bookmarks.renameFolder')?.({ folder }); @@ -39,7 +40,7 @@ describe('activate', () => { await handlers.get('workspace-file-bookmarks.setViewModeList')?.(); await handlers.get('workspace-file-bookmarks.setViewModeTree')?.(); - expect(handlers.size).toBe(12); + expect(handlers.size).toBe(13); }); }); diff --git a/test/bookmarkStore.test.ts b/test/bookmarkStore.test.ts index a23b1bc..4a81a63 100644 --- a/test/bookmarkStore.test.ts +++ b/test/bookmarkStore.test.ts @@ -56,6 +56,13 @@ describe('BookmarkStore', () => { expect(store.getAllBookmarks()[0].folderId).toBe('folder-1'); }); + it('renames a bookmark label', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a', label: 'a.ts' })); + store.renameBookmark('a', 'My File'); + expect(store.getAllBookmarks()[0].label).toBe('My File'); + }); + it('creates, renames, and deletes a folder', () => { const store = newStore(); const folder = store.createFolder('Backend'); diff --git a/test/commands.test.ts b/test/commands.test.ts index baeaa6f..0122d60 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -10,6 +10,7 @@ import { openAllInFolder, openBookmark, pickFolder, + renameBookmark, renameFolder, type Bookmark, type BookmarkFolder @@ -180,6 +181,34 @@ describe('renameFolder', () => { }); }); +describe('renameBookmark', () => { + it('renames the bookmark from trimmed input', async () => { + const store = newStore(); + const bookmark = makeBookmark(); + store.addBookmark(bookmark); + window.showInputBox.mockResolvedValue(' My Bookmark '); + + await renameBookmark(store, { bookmark } as any); + + expect(store.getAllBookmarks()[0].label).toBe('My Bookmark'); + + const validateInput = window.showInputBox.mock.calls[0][0].validateInput; + expect(validateInput(' ')).toMatch(/cannot be empty/); + expect(validateInput('My Bookmark')).toBeUndefined(); + }); + + it('does nothing when the input is cancelled', async () => { + const store = newStore(); + const bookmark = makeBookmark(); + store.addBookmark(bookmark); + window.showInputBox.mockResolvedValue(undefined); + + await renameBookmark(store, { bookmark } as any); + + expect(store.getAllBookmarks()[0].label).toBe('a.ts'); + }); +}); + describe('deleteFolder', () => { it('deletes an empty folder without confirming', async () => { const store = newStore(); From 663a1646b3ed23cc76357c9e66290bc5812cdc9c Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 16:23:21 -0400 Subject: [PATCH 14/17] docs: clarify feature branch workflow in CLAUDE.md --- CLAUDE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index ab98a2a..036bf8b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,11 @@ users bookmark files across a multi-root/multi-repo workspace. ## Workflow -`development` batches feature work and dependency updates so a merge into +Feature work happens on `feature/**` branches, one PR per feature. A PR may +be split into multiple commits, but never merge a feature that isn't +complete — don't land it half-done. If a task spans multiple features, split +it into multiple PRs (one per feature) rather than combining them into one. +`development` batches dependency updates so a merge into `main` doesn't trigger a release per commit. `main` is only touched directly for CI fixes or hotfixes, which then propagate back down to `development` via the `propagate-main-to-development` workflow. `promote-dev-to-main` opens a From e9e625234f6a5cdcfa771ad4b93f97206c8e5ba2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:31:57 -0400 Subject: [PATCH 15/17] chore: propagate main into development (#58) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Wikid82 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> From b79b7595a0ac17d9ef653b4b9ec75297316e3f5e Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 16:41:41 -0400 Subject: [PATCH 16/17] feat: reorder bookmarks and folders via drag-and-drop Adds an explicit `order` field to Bookmark/BookmarkFolder and wires BookmarksTreeProvider up as a vscode.TreeDragAndDropController so users can drag to reorder bookmarks within a folder/list, reorder folders, and drag a bookmark onto a folder to move it. Items sort by `order` when set, falling back to the existing createdAt/name ordering for anything not yet touched by a drag. Closes #38 --- extension.ts | 170 ++++++++++++++++++--- test/bookmarkStore.test.ts | 26 ++++ test/dragAndDrop.test.ts | 293 +++++++++++++++++++++++++++++++++++++ test/vscode-mock.ts | 20 +++ 4 files changed, 491 insertions(+), 18 deletions(-) create mode 100644 test/dragAndDrop.test.ts diff --git a/extension.ts b/extension.ts index 9596e0c..2cb0149 100644 --- a/extension.ts +++ b/extension.ts @@ -15,21 +15,64 @@ export interface Bookmark { workspaceFolderName: string; folderId: string | null; createdAt: number; + order?: number; } export interface BookmarkFolder { id: string; name: string; createdAt: number; + order?: number; } +export const BOOKMARK_DND_MIME_TYPE = 'application/vnd.code.tree.workspace-file-bookmarks-view'; + +/** Sorts by explicit `order` when set (ascending), falling back for items without one. Items with an order always sort before items without. */ +export function sortByOrder(items: T[], fallbackCompare: (a: T, b: T) => number): T[] { + return items.slice().sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) { + return a.order - b.order; + } + if (a.order !== undefined) { + return -1; + } + if (b.order !== undefined) { + return 1; + } + return fallbackCompare(a, b); + }); +} + +/** Computes the ordered id list for a drag-and-drop scope: `draggedId` removed then reinserted before `beforeId` (or appended when `beforeId` is null). */ +export function computeReorderedIds( + scopeItems: T[], + draggedId: string, + beforeId: string | null, + fallbackCompare: (a: T, b: T) => number +): string[] { + const ids = sortByOrder(scopeItems, fallbackCompare) + .filter(item => item.id !== draggedId) + .map(item => item.id); + const insertAt = beforeId ? ids.indexOf(beforeId) : -1; + if (insertAt === -1) { + ids.push(draggedId); + } else { + ids.splice(insertAt, 0, draggedId); + } + return ids; +} + +const bookmarkFallbackCompare = (a: Bookmark, b: Bookmark) => b.createdAt - a.createdAt; +const folderFallbackCompare = (a: BookmarkFolder, b: BookmarkFolder) => a.name.localeCompare(b.name); + export function activate(context: vscode.ExtensionContext) { const store = new BookmarkStore(context); const provider = new BookmarksTreeProvider(store, context); const treeView = vscode.window.createTreeView('workspace-file-bookmarks-view', { treeDataProvider: provider, - showCollapseAll: true + showCollapseAll: true, + dragAndDropController: provider }); vscode.commands.executeCommand('setContext', VIEW_MODE_CONTEXT_KEY, provider.getViewMode()); @@ -89,6 +132,18 @@ export class BookmarkStore { this.setBookmarks(this.getAllBookmarks().map(b => (b.id === id ? { ...b, label } : b))); } + /** Assigns sequential `order` values to the bookmarks named in `orderedIds`; bookmarks outside that set are untouched. */ + reorderBookmarks(orderedIds: string[]) { + const orderById = new Map(orderedIds.map((id, index) => [id, index])); + this.setBookmarks(this.getAllBookmarks().map(b => (orderById.has(b.id) ? { ...b, order: orderById.get(b.id) } : b))); + } + + /** Assigns sequential `order` values to the folders named in `orderedIds`; folders outside that set are untouched. */ + reorderFolders(orderedIds: string[]) { + const orderById = new Map(orderedIds.map((id, index) => [id, index])); + this.setFolders(this.getAllFolders().map(f => (orderById.has(f.id) ? { ...f, order: orderById.get(f.id) } : f))); + } + createFolder(name: string): BookmarkFolder { const folder: BookmarkFolder = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, @@ -145,7 +200,7 @@ export class BookmarkTreeItem extends vscode.TreeItem { type TreeNode = FolderGroupItem | BookmarkTreeItem; -export class BookmarksTreeProvider implements vscode.TreeDataProvider { +export class BookmarksTreeProvider implements vscode.TreeDataProvider, vscode.TreeDragAndDropController { private readonly _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; @@ -181,35 +236,64 @@ export class BookmarksTreeProvider implements vscode.TreeDataProvider return []; } const folderById = new Map(folders.map(f => [f.id, f.name])); - return bookmarks - .slice() - .sort((a, b) => b.createdAt - a.createdAt) - .map(b => new BookmarkTreeItem(b, describeBookmark(b, isMultiRoot, folderById.get(b.folderId ?? '') ?? null))); + return sortByOrder(bookmarks, bookmarkFallbackCompare).map( + b => new BookmarkTreeItem(b, describeBookmark(b, isMultiRoot, folderById.get(b.folderId ?? '') ?? null)) + ); } if (element instanceof FolderGroupItem) { - return element.bookmarks - .slice() - .sort((a, b) => b.createdAt - a.createdAt) - .map(b => new BookmarkTreeItem(b, describeBookmark(b, isMultiRoot, null))); + return sortByOrder(element.bookmarks, bookmarkFallbackCompare).map( + b => new BookmarkTreeItem(b, describeBookmark(b, isMultiRoot, null)) + ); } if (element) { return []; } - const folderNodes = folders - .slice() - .sort((a, b) => a.name.localeCompare(b.name)) - .map(folder => new FolderGroupItem(folder, bookmarks.filter(b => b.folderId === folder.id))); + const folderNodes = sortByOrder(folders, folderFallbackCompare).map( + folder => new FolderGroupItem(folder, bookmarks.filter(b => b.folderId === folder.id)) + ); - const ungrouped = bookmarks - .filter(b => !b.folderId) - .sort((a, b) => b.createdAt - a.createdAt) - .map(b => new BookmarkTreeItem(b, describeBookmark(b, isMultiRoot, null))); + const ungrouped = sortByOrder( + bookmarks.filter(b => !b.folderId), + bookmarkFallbackCompare + ).map(b => new BookmarkTreeItem(b, describeBookmark(b, isMultiRoot, null))); return [...folderNodes, ...ungrouped]; } + + get dropMimeTypes(): string[] { + return [BOOKMARK_DND_MIME_TYPE]; + } + + get dragMimeTypes(): string[] { + return [BOOKMARK_DND_MIME_TYPE]; + } + + handleDrag(source: readonly TreeNode[], dataTransfer: vscode.DataTransfer): void { + const item = source[0]; + if (!item) { + return; + } + const payload = item instanceof BookmarkTreeItem + ? { kind: 'bookmark' as const, id: item.bookmark.id } + : { kind: 'folder' as const, id: item.folder.id }; + dataTransfer.set(BOOKMARK_DND_MIME_TYPE, new vscode.DataTransferItem(JSON.stringify(payload))); + } + + async handleDrop(target: TreeNode | undefined, dataTransfer: vscode.DataTransfer): Promise { + const transferItem = dataTransfer.get(BOOKMARK_DND_MIME_TYPE); + if (!transferItem) { + return; + } + const payload = JSON.parse(await transferItem.asString()) as { kind: 'bookmark' | 'folder'; id: string }; + if (payload.kind === 'folder') { + dropFolder(this.store, payload.id, target); + } else { + dropBookmark(this.store, payload.id, target, this.viewMode); + } + } } export function describeBookmark(bookmark: Bookmark, isMultiRoot: boolean, folderName: string | null): string[] { @@ -356,6 +440,56 @@ export async function pickFolder(store: BookmarkStore, placeHolder: string): Pro return folders.find(f => f.name === picked)?.id ?? undefined; } +/** Reorders folders by moving `draggedId` to just before the folder `target` represents (or to the end if dropped elsewhere). */ +export function dropFolder(store: BookmarkStore, draggedId: string, target: TreeNode | undefined) { + if (target instanceof BookmarkTreeItem) { + return; + } + const beforeId = target instanceof FolderGroupItem ? target.folder.id : null; + if (beforeId === draggedId) { + return; + } + store.reorderFolders(computeReorderedIds(store.getAllFolders(), draggedId, beforeId, folderFallbackCompare)); +} + +/** + * Handles a bookmark dropped onto `target`: dropping onto a folder moves the bookmark into it + * (appended at the end); dropping onto another bookmark reorders within that bookmark's scope, + * moving folders too in tree view (list view reorders in place without touching folderId); + * dropping on empty space appends to the end of the root/list scope. + */ +export function dropBookmark(store: BookmarkStore, draggedId: string, target: TreeNode | undefined, viewMode: ViewMode) { + const dragged = store.getAllBookmarks().find(b => b.id === draggedId); + if (!dragged) { + return; + } + + if (target instanceof FolderGroupItem) { + if (dragged.folderId !== target.folder.id) { + store.moveBookmarkToFolder(draggedId, target.folder.id); + } + const scope = store.getAllBookmarks().filter(b => b.folderId === target.folder.id); + store.reorderBookmarks(computeReorderedIds(scope, draggedId, null, bookmarkFallbackCompare)); + return; + } + + if (target instanceof BookmarkTreeItem && target.bookmark.id === draggedId) { + return; + } + + const beforeId = target instanceof BookmarkTreeItem ? target.bookmark.id : null; + // List view is a flat scope: reorder in place without ever reassigning folderId. + const destinationFolderId = + viewMode === 'list' ? dragged.folderId : target instanceof BookmarkTreeItem ? target.bookmark.folderId : null; + + if (destinationFolderId !== dragged.folderId) { + store.moveBookmarkToFolder(draggedId, destinationFolderId); + } + + const scope = store.getAllBookmarks().filter(b => (viewMode === 'list' ? true : b.folderId === destinationFolderId)); + store.reorderBookmarks(computeReorderedIds(scope, draggedId, beforeId, bookmarkFallbackCompare)); +} + export async function moveToFolder(store: BookmarkStore, item: BookmarkTreeItem) { const folderId = await pickFolder(store, `Move "${item.bookmark.label}" to...`); if (folderId === undefined) { diff --git a/test/bookmarkStore.test.ts b/test/bookmarkStore.test.ts index 4a81a63..15f86b1 100644 --- a/test/bookmarkStore.test.ts +++ b/test/bookmarkStore.test.ts @@ -63,6 +63,32 @@ describe('BookmarkStore', () => { expect(store.getAllBookmarks()[0].label).toBe('My File'); }); + it('assigns sequential order to the named bookmarks and leaves others untouched', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a', uri: 'file:///repo/src/a.ts' })); + store.addBookmark(makeBookmark({ id: 'b', uri: 'file:///repo/src/b.ts' })); + store.addBookmark(makeBookmark({ id: 'c', uri: 'file:///repo/src/c.ts' })); + + store.reorderBookmarks(['c', 'a']); + + const byId = new Map(store.getAllBookmarks().map(b => [b.id, b])); + expect(byId.get('c')?.order).toBe(0); + expect(byId.get('a')?.order).toBe(1); + expect(byId.get('b')?.order).toBeUndefined(); + }); + + it('assigns sequential order to the named folders and leaves others untouched', () => { + const store = newStore(); + const first = store.createFolder('First'); + const second = store.createFolder('Second'); + + store.reorderFolders([second.id, first.id]); + + const byId = new Map(store.getAllFolders().map(f => [f.id, f])); + expect(byId.get(second.id)?.order).toBe(0); + expect(byId.get(first.id)?.order).toBe(1); + }); + it('creates, renames, and deletes a folder', () => { const store = newStore(); const folder = store.createFolder('Backend'); diff --git a/test/dragAndDrop.test.ts b/test/dragAndDrop.test.ts new file mode 100644 index 0000000..0914d5a --- /dev/null +++ b/test/dragAndDrop.test.ts @@ -0,0 +1,293 @@ +import { describe, it, expect } from 'vitest'; +import { + BOOKMARK_DND_MIME_TYPE, + BookmarkStore, + BookmarksTreeProvider, + BookmarkTreeItem, + FolderGroupItem, + computeReorderedIds, + dropBookmark, + dropFolder, + sortByOrder, + type Bookmark +} from '../extension'; +import { createFakeContext } from './fakeContext'; +import { DataTransfer, DataTransferItem } from './vscode-mock'; + +function makeBookmark(overrides: Partial = {}): Bookmark { + const id = overrides.id ?? 'id-1'; + return { + id, + uri: `file:///repo/src/${id}.ts`, + label: `${id}.ts`, + relativePath: `src/${id}.ts`, + workspaceFolderName: 'repo', + folderId: null, + createdAt: 0, + ...overrides + }; +} + +function newStore() { + return new BookmarkStore(createFakeContext() as any); +} + +function newProvider() { + const context = createFakeContext(); + const store = new BookmarkStore(context as any); + const provider = new BookmarksTreeProvider(store, context as any); + return { store, provider }; +} + +describe('sortByOrder', () => { + it('sorts by order ascending when every item has one', () => { + const items = [{ id: 'b', order: 1 }, { id: 'a', order: 0 }]; + expect(sortByOrder(items, () => 0).map(i => i.id)).toEqual(['a', 'b']); + }); + + it('puts ordered items before unordered ones', () => { + const items = [{ id: 'unordered' }, { id: 'ordered', order: 0 }]; + expect(sortByOrder(items, () => 0).map(i => i.id)).toEqual(['ordered', 'unordered']); + }); + + it('puts ordered items before unordered ones regardless of comparison direction', () => { + const items = [{ id: 'ordered', order: 0 }, { id: 'unordered' }]; + expect(sortByOrder(items, () => 0).map(i => i.id)).toEqual(['ordered', 'unordered']); + }); + + it('falls back to the comparator when neither item has an order', () => { + const items: Array<{ id: string; createdAt: number; order?: number }> = [ + { id: 'a', createdAt: 1 }, + { id: 'b', createdAt: 2 } + ]; + expect(sortByOrder(items, (a, b) => b.createdAt - a.createdAt).map(i => i.id)).toEqual(['b', 'a']); + }); +}); + +describe('computeReorderedIds', () => { + it('appends the dragged id at the end when beforeId is null', () => { + const items = [{ id: 'a', order: 0 }, { id: 'b', order: 1 }]; + expect(computeReorderedIds(items, 'a', null, () => 0)).toEqual(['b', 'a']); + }); + + it('inserts the dragged id before beforeId', () => { + const items = [{ id: 'a', order: 0 }, { id: 'b', order: 1 }, { id: 'c', order: 2 }]; + expect(computeReorderedIds(items, 'c', 'a', () => 0)).toEqual(['c', 'a', 'b']); + }); +}); + +describe('dropFolder', () => { + it('reorders a folder to just before the target folder', () => { + const store = newStore(); + const a = store.createFolder('A'); + const b = store.createFolder('B'); + const c = store.createFolder('C'); + + dropFolder(store, c.id, new FolderGroupItem(a, [])); + + const byId = new Map(store.getAllFolders().map(f => [f.id, f])); + expect(byId.get(c.id)?.order).toBe(0); + expect(byId.get(a.id)?.order).toBe(1); + expect(byId.get(b.id)?.order).toBe(2); + }); + + it('moves a folder to the end when dropped on empty space', () => { + const store = newStore(); + const a = store.createFolder('A'); + const b = store.createFolder('B'); + + dropFolder(store, a.id, undefined); + + const byId = new Map(store.getAllFolders().map(f => [f.id, f])); + expect(byId.get(b.id)?.order).toBe(0); + expect(byId.get(a.id)?.order).toBe(1); + }); + + it('does nothing when dropped onto itself', () => { + const store = newStore(); + const a = store.createFolder('A'); + + dropFolder(store, a.id, new FolderGroupItem(a, [])); + + expect(store.getAllFolders()[0].order).toBeUndefined(); + }); + + it('ignores a drop onto a bookmark item', () => { + const store = newStore(); + const a = store.createFolder('A'); + store.addBookmark(makeBookmark({ id: 'bm' })); + + dropFolder(store, a.id, new BookmarkTreeItem(makeBookmark({ id: 'bm' }), [])); + + expect(store.getAllFolders()[0].order).toBeUndefined(); + }); +}); + +describe('dropBookmark', () => { + it('moves a bookmark into the target folder and appends it', () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + store.addBookmark(makeBookmark({ id: 'existing', folderId: folder.id })); + store.addBookmark(makeBookmark({ id: 'dragged', folderId: null })); + + dropBookmark(store, 'dragged', new FolderGroupItem(folder, []), 'tree'); + + const byId = new Map(store.getAllBookmarks().map(b => [b.id, b])); + expect(byId.get('dragged')?.folderId).toBe(folder.id); + expect(byId.get('existing')?.order).toBe(0); + expect(byId.get('dragged')?.order).toBe(1); + }); + + it('reorders within the same folder scope in tree view without changing folderId', () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + store.addBookmark(makeBookmark({ id: 'a', folderId: folder.id })); + store.addBookmark(makeBookmark({ id: 'b', folderId: folder.id })); + + dropBookmark(store, 'b', new BookmarkTreeItem(makeBookmark({ id: 'a', folderId: folder.id }), []), 'tree'); + + const byId = new Map(store.getAllBookmarks().map(bm => [bm.id, bm])); + expect(byId.get('b')?.folderId).toBe(folder.id); + expect(byId.get('b')?.order).toBe(0); + expect(byId.get('a')?.order).toBe(1); + }); + + it('moves folders when dropped onto a bookmark in a different folder in tree view', () => { + const store = newStore(); + const source = store.createFolder('Source'); + const dest = store.createFolder('Dest'); + store.addBookmark(makeBookmark({ id: 'dragged', folderId: source.id })); + store.addBookmark(makeBookmark({ id: 'target', folderId: dest.id })); + + dropBookmark(store, 'dragged', new BookmarkTreeItem(makeBookmark({ id: 'target', folderId: dest.id }), []), 'tree'); + + const byId = new Map(store.getAllBookmarks().map(bm => [bm.id, bm])); + expect(byId.get('dragged')?.folderId).toBe(dest.id); + expect(byId.get('dragged')?.order).toBe(0); + expect(byId.get('target')?.order).toBe(1); + }); + + it('reorders in place in list view without reassigning folderId', () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + store.addBookmark(makeBookmark({ id: 'grouped', folderId: folder.id })); + store.addBookmark(makeBookmark({ id: 'root', folderId: null })); + + dropBookmark(store, 'root', new BookmarkTreeItem(makeBookmark({ id: 'grouped', folderId: folder.id }), []), 'list'); + + const byId = new Map(store.getAllBookmarks().map(bm => [bm.id, bm])); + expect(byId.get('root')?.folderId).toBeNull(); + expect(byId.get('root')?.order).toBe(0); + expect(byId.get('grouped')?.order).toBe(1); + }); + + it('moves a bookmark to the root and appends it when dropped on empty space in tree view', () => { + const store = newStore(); + const folder = store.createFolder('Backend'); + store.addBookmark(makeBookmark({ id: 'root-existing', folderId: null })); + store.addBookmark(makeBookmark({ id: 'dragged', folderId: folder.id })); + + dropBookmark(store, 'dragged', undefined, 'tree'); + + const byId = new Map(store.getAllBookmarks().map(bm => [bm.id, bm])); + expect(byId.get('dragged')?.folderId).toBeNull(); + expect(byId.get('root-existing')?.order).toBe(0); + expect(byId.get('dragged')?.order).toBe(1); + }); + + it('appends to the end of the flat list when dropped on empty space in list view', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a' })); + store.addBookmark(makeBookmark({ id: 'dragged' })); + + dropBookmark(store, 'dragged', undefined, 'list'); + + const byId = new Map(store.getAllBookmarks().map(bm => [bm.id, bm])); + expect(byId.get('a')?.order).toBe(0); + expect(byId.get('dragged')?.order).toBe(1); + }); + + it('does nothing when the dragged bookmark no longer exists', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a' })); + + dropBookmark(store, 'missing', undefined, 'tree'); + + expect(store.getAllBookmarks()[0].order).toBeUndefined(); + }); + + it('does nothing when dropped onto itself', () => { + const store = newStore(); + store.addBookmark(makeBookmark({ id: 'a' })); + + dropBookmark(store, 'a', new BookmarkTreeItem(makeBookmark({ id: 'a' }), []), 'tree'); + + expect(store.getAllBookmarks()[0].order).toBeUndefined(); + }); +}); + +describe('BookmarksTreeProvider drag and drop', () => { + it('exposes the same mime type for drag and drop', () => { + const { provider } = newProvider(); + expect(provider.dragMimeTypes).toEqual([BOOKMARK_DND_MIME_TYPE]); + expect(provider.dropMimeTypes).toEqual([BOOKMARK_DND_MIME_TYPE]); + }); + + it('handleDrag stores the bookmark id under the mime type', async () => { + const { provider } = newProvider(); + const item = new BookmarkTreeItem(makeBookmark({ id: 'a' }), []); + const dataTransfer = new DataTransfer(); + + provider.handleDrag([item], dataTransfer as any); + + const stored = await dataTransfer.get(BOOKMARK_DND_MIME_TYPE)?.asString(); + expect(JSON.parse(stored ?? '{}')).toEqual({ kind: 'bookmark', id: 'a' }); + }); + + it('handleDrag does nothing when the source list is empty', () => { + const { provider } = newProvider(); + const dataTransfer = new DataTransfer(); + + provider.handleDrag([], dataTransfer as any); + + expect(dataTransfer.get(BOOKMARK_DND_MIME_TYPE)).toBeUndefined(); + }); + + it('handleDrop reorders folders end-to-end via the data transfer payload', async () => { + const { store, provider } = newProvider(); + const a = store.createFolder('A'); + const b = store.createFolder('B'); + const dataTransfer = new DataTransfer(); + dataTransfer.set(BOOKMARK_DND_MIME_TYPE, new DataTransferItem(JSON.stringify({ kind: 'folder', id: b.id }))); + + await provider.handleDrop(new FolderGroupItem(a, []), dataTransfer as any); + + const byId = new Map(store.getAllFolders().map(f => [f.id, f])); + expect(byId.get(b.id)?.order).toBe(0); + expect(byId.get(a.id)?.order).toBe(1); + }); + + it('handleDrop reorders bookmarks end-to-end via the data transfer payload', async () => { + const { store, provider } = newProvider(); + store.addBookmark(makeBookmark({ id: 'a' })); + store.addBookmark(makeBookmark({ id: 'b' })); + const dataTransfer = new DataTransfer(); + dataTransfer.set(BOOKMARK_DND_MIME_TYPE, new DataTransferItem(JSON.stringify({ kind: 'bookmark', id: 'b' }))); + + await provider.handleDrop(new BookmarkTreeItem(makeBookmark({ id: 'a' }), []), dataTransfer as any); + + const byId = new Map(store.getAllBookmarks().map(bm => [bm.id, bm])); + expect(byId.get('b')?.order).toBe(0); + expect(byId.get('a')?.order).toBe(1); + }); + + it('handleDrop does nothing when the data transfer has no matching payload', async () => { + const { store, provider } = newProvider(); + store.addBookmark(makeBookmark({ id: 'a' })); + const dataTransfer = new DataTransfer(); + + await provider.handleDrop(undefined, dataTransfer as any); + + expect(store.getAllBookmarks()[0].order).toBeUndefined(); + }); +}); diff --git a/test/vscode-mock.ts b/test/vscode-mock.ts index e4489b5..f22a211 100644 --- a/test/vscode-mock.ts +++ b/test/vscode-mock.ts @@ -50,6 +50,26 @@ export class ThemeIcon { constructor(public id: string) {} } +export class DataTransferItem { + constructor(private readonly value: string) {} + + asString(): Promise { + return Promise.resolve(this.value); + } +} + +export class DataTransfer { + private readonly items = new Map(); + + set(mimeType: string, value: DataTransferItem) { + this.items.set(mimeType, value); + } + + get(mimeType: string): DataTransferItem | undefined { + return this.items.get(mimeType); + } +} + export const window = { activeTextEditor: undefined as { document: { uri: Uri } } | undefined, showInformationMessage: vi.fn(), From c8a2079239d372017446e6293984ee5b36961b8f Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Fri, 21 Aug 2026 16:49:54 -0400 Subject: [PATCH 17/17] feat: add e2e testing with @vscode/test-cli and wire it into CI Adds a real integration-testing layer alongside the vitest unit suite: test/e2e/*.test.ts runs against the built extension inside an actual VS Code Extension Development Host (@vscode/test-cli + @vscode/test-electron), exercising command registration, the tree view, and workspaceState through the real vscode API instead of the vitest mock. activate() now returns a small { store, provider } API so e2e tests can inspect state the UI alone doesn't expose. Wires `npm run test:e2e` into a new CI job (xvfb-run on Linux), and updates the Definition of Done in CLAUDE.md to require e2e coverage for new user-facing commands/tree behavior alongside unit tests, not as a substitute for them. Also overrides mocha's stale diff/serialize-javascript transitive deps to close a high-severity npm audit finding pulled in by @vscode/test-cli. --- .github/workflows/ci.yml | 20 + .gitignore | 2 + .vscode-test.mjs | 10 + .vscodeignore | 3 + CLAUDE.md | 15 +- extension.ts | 10 +- package-lock.json | 1734 ++++++++++++++++++- package.json | 11 +- test/e2e/extension.e2e.test.ts | 106 ++ test/e2e/fixtures/workspace/src/sample-a.ts | 1 + test/e2e/fixtures/workspace/src/sample-b.ts | 1 + tsconfig.json | 2 +- vitest.config.mts | 1 + 13 files changed, 1832 insertions(+), 84 deletions(-) create mode 100644 .vscode-test.mjs create mode 100644 test/e2e/extension.e2e.test.ts create mode 100644 test/e2e/fixtures/workspace/src/sample-a.ts create mode 100644 test/e2e/fixtures/workspace/src/sample-b.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3724f2c..8e679fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,23 @@ jobs: - name: Build run: npm run build + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24.19.0 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Xvfb + run: sudo apt-get update && sudo apt-get install -y xvfb + + - name: Run e2e tests + run: xvfb-run -a npm run test:e2e diff --git a/.gitignore b/.gitignore index bbfb2cf..6970ce3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ node_modules/ dist/ coverage/ +out-e2e/ +.vscode-test/ .DS_Store *.log *.vsix diff --git a/.vscode-test.mjs b/.vscode-test.mjs new file mode 100644 index 0000000..e615cb8 --- /dev/null +++ b/.vscode-test.mjs @@ -0,0 +1,10 @@ +import { defineConfig } from '@vscode/test-cli'; + +export default defineConfig({ + files: 'out-e2e/**/*.test.js', + workspaceFolder: 'test/e2e/fixtures/workspace', + mocha: { + ui: 'bdd', + timeout: 20000 + } +}); diff --git a/.vscodeignore b/.vscodeignore index 008da00..cd26c45 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -3,10 +3,13 @@ node_modules/** scripts/** test/** +out-e2e/** +.vscode-test/** extension.ts extension.ts.map tsconfig.json vitest.config.mts +.vscode-test.mjs codecov.yml CLAUDE.md .gitignore diff --git a/CLAUDE.md b/CLAUDE.md index 036bf8b..dacdff3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,9 @@ drives versioning and publishing off Conventional Commit prefixes on `main`. Every change is expected to clear this bar before it's mergeable: -- **CI clean.** `npm run lint` (`tsc --noEmit`), `npm run test`, and - `npm run build` all pass — see `.github/workflows/ci.yml`. +- **CI clean.** `npm run lint` (`tsc --noEmit`), `npm run test`, + `npm run build`, and `npm run test:e2e` all pass — see + `.github/workflows/ci.yml`. - **Unit coverage ≥85%, both patch and project.** Codecov enforces this on every PR (`codecov.yml`: `project` and `patch`, both `target: 85%, threshold: 1%`), reported from `npm run test:coverage` (vitest + v8, @@ -33,6 +34,16 @@ Every change is expected to clear this bar before it's mergeable: Codecov's patch gate computes, so a real gap shows up before a CI round-trip instead of after. Write real tests that close gaps for real — no padding, no vacuous assertions just to move a number. +- **New features carry e2e coverage, not just unit tests.** Unit tests + (vitest, against `test/vscode-mock.ts`) cover logic in isolation; + `test/e2e/*.test.ts` (mocha, via `@vscode/test-cli` + `@vscode/test-electron`) + runs the real, built extension inside an actual VS Code Extension + Development Host — real command registration, real tree view, real + `workspaceState`. Any new user-facing command or tree behavior needs both: + a unit test for the logic and an e2e test exercising it end-to-end through + the real `vscode` API. Run locally with `npm run test:e2e` (builds the + extension, bundles `test/e2e/**` with esbuild, then launches the test + host — needs a display or `xvfb-run` on headless Linux). - **Security scanning clean.** CodeQL runs on every push/PR (`.github/workflows/codeql.yml`); `npm audit --audit-level=high` (via `scripts/wfb_dep_update.sh`) gates dependency updates on high/critical diff --git a/extension.ts b/extension.ts index 9596e0c..30d817a 100644 --- a/extension.ts +++ b/extension.ts @@ -23,7 +23,13 @@ export interface BookmarkFolder { createdAt: number; } -export function activate(context: vscode.ExtensionContext) { +/** The extension's public API, returned from `activate()` — used by e2e tests to inspect state that isn't reachable through the tree view UI alone. */ +export interface ExtensionApi { + store: BookmarkStore; + provider: BookmarksTreeProvider; +} + +export function activate(context: vscode.ExtensionContext): ExtensionApi { const store = new BookmarkStore(context); const provider = new BookmarksTreeProvider(store, context); @@ -50,6 +56,8 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('workspace-file-bookmarks.setViewModeList', () => provider.setViewMode('list')), vscode.commands.registerCommand('workspace-file-bookmarks.setViewModeTree', () => provider.setViewMode('tree')) ); + + return { store, provider }; } export function deactivate() {} diff --git a/package-lock.json b/package-lock.json index a807b76..8904193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,12 @@ "name": "workspace-file-bookmarks", "version": "1.5.3", "devDependencies": { + "@types/mocha": "^10.0.10", "@types/node": "^26.2.0", "@types/vscode": "^1.134.0", "@vitest/coverage-v8": "^4.1.11", + "@vscode/test-cli": "^0.0.15", + "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", "esbuild": "^0.28.2", "typescript": "~6.0.3", @@ -740,6 +743,90 @@ "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/@isaacs/cliui/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/@isaacs/cliui/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/@isaacs/cliui/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/@isaacs/cliui/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/@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/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -816,6 +903,17 @@ "url": "https://github.com/sponsors/Boshen" } }, + "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/@rolldown/binding-android-arm-eabi": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", @@ -1401,6 +1499,20 @@ "dev": true, "license": "MIT" }, + "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/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", @@ -1591,6 +1703,60 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vscode/test-cli": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.15.tgz", + "integrity": "sha512-nAxk2X79wuXS7aOhyFFhFcCqd7EBUoMesu7ZgsYE/4eFjyBMuyIweVE94BxdKH1RieN8eOz2SIrljrZt6Lk9fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.10", + "c8": "^11.0.0", + "chokidar": "^5.0.0", + "enhanced-resolve": "^5.24.0", + "glob": "^13.0.6", + "minimatch": "^10.2.5", + "mocha": "^11.7.6", + "supports-color": "^10.2.2", + "yargs": "^18.0.0" + }, + "bin": { + "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vscode/test-cli/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@vscode/test-electron": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", + "integrity": "sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@vscode/vsce": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", @@ -2013,6 +2179,13 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2072,6 +2245,115 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/c8": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", + "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^8.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/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/c8/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/c8/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/c8/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/c8/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/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2103,6 +2385,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2174,6 +2469,22 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -2182,6 +2493,75 @@ "license": "ISC", "optional": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -2242,6 +2622,28 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "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/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -2290,6 +2692,19 @@ } } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2381,6 +2796,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2455,6 +2880,13 @@ "node": ">= 0.4" } }, + "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/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2514,8 +2946,22 @@ "once": "^1.4.0" } }, - "node_modules/entities": { - "version": "4.5.0", + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, @@ -2638,6 +3084,29 @@ "@esbuild/win32-x64": "0.28.2" } }, + "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": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2751,6 +3220,50 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "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/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -2816,6 +3329,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2987,6 +3523,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -3113,6 +3659,13 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/index-to-position": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", @@ -3131,8 +3684,7 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", @@ -3210,6 +3762,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3220,6 +3785,39 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -3236,6 +3834,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "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", @@ -3293,6 +3905,22 @@ "url": "https://bevry.me/fund" } }, + "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/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -3386,6 +4014,52 @@ "npm": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -3432,6 +4106,16 @@ "node": ">=6" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -3725,6 +4409,22 @@ "uc.micro": "^2.0.0" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -3788,6 +4488,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3908,6 +4625,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/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/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3944,6 +4674,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -4003,45 +4746,295 @@ "license": "MIT", "optional": true }, - "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==", + "node_modules/mocha": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/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/mocha/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/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "readdirp": "^4.0.1" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "node_modules/mocha/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": "MIT", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/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/mocha/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/mocha/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/mocha/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/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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/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/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", "optional": true }, "node_modules/node-abi": { @@ -4166,6 +5159,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -4185,6 +5194,143 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", @@ -4198,6 +5344,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parse-json": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", @@ -4289,6 +5449,26 @@ "url": "https://github.com/fb55/entities?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-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": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -4351,13 +5531,13 @@ "license": "ISC" }, "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==", + "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": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -4431,6 +5611,13 @@ "node": ">=10" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -4583,6 +5770,30 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4593,6 +5804,23 @@ "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4748,6 +5976,46 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.0.tgz", + "integrity": "sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "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/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -4831,6 +6099,19 @@ "dev": true, "license": "ISC" }, + "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/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -4971,18 +6252,47 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", - "optional": true, + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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": { - "safe-buffer": "~5.2.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/string-width": { + "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==", @@ -4997,6 +6307,29 @@ "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/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/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -5036,6 +6369,30 @@ "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-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -5127,6 +6484,20 @@ "node": ">=8" } }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar-fs": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", @@ -5176,6 +6547,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5233,19 +6619,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/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/tinyrainbow": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", @@ -5415,8 +6788,22 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" + }, + "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/validate-npm-package-license": { "version": "3.0.4", @@ -5520,19 +6907,6 @@ } } }, - "node_modules/vite/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/vitest": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", @@ -5623,19 +6997,6 @@ } } }, - "node_modules/vitest/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/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -5660,6 +7021,22 @@ "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/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -5677,6 +7054,111 @@ "node": ">=8" } }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "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/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/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5725,6 +7207,16 @@ "node": ">=4.0" } }, + "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": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -5732,6 +7224,77 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "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-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/yauzl": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", @@ -5754,6 +7317,19 @@ "dependencies": { "buffer-crc32": "~0.2.3" } + }, + "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/package.json b/package.json index c5f7025..7803250 100644 --- a/package.json +++ b/package.json @@ -220,12 +220,21 @@ "clean": "rm -rf dist", "lint": "tsc --noEmit", "test": "vitest run", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "pretest:e2e": "npm run build && esbuild test/e2e/*.test.ts --bundle --outdir=out-e2e --platform=node --format=cjs --external:vscode --sourcemap", + "test:e2e": "vscode-test" + }, + "overrides": { + "diff": "^8.0.4", + "serialize-javascript": "^7.1.0" }, "devDependencies": { + "@types/mocha": "^10.0.10", "@types/node": "^26.2.0", "@types/vscode": "^1.134.0", "@vitest/coverage-v8": "^4.1.11", + "@vscode/test-cli": "^0.0.15", + "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", "esbuild": "^0.28.2", "typescript": "~6.0.3", diff --git a/test/e2e/extension.e2e.test.ts b/test/e2e/extension.e2e.test.ts new file mode 100644 index 0000000..5957403 --- /dev/null +++ b/test/e2e/extension.e2e.test.ts @@ -0,0 +1,106 @@ +import * as assert from 'node:assert/strict'; +import * as path from 'node:path'; +import * as vscode from 'vscode'; +import type { BookmarkTreeItem, ExtensionApi, FolderGroupItem } from '../../extension'; + +// The running extension is loaded from dist/extension.cjs by the test host, while this +// test file is compiled from a separate esbuild pass — so its classes are a *different* +// module instance and `instanceof` across the two would silently always fail. Duck-type +// tree nodes by shape instead of importing the classes as values. +function isBookmarkNode(node: BookmarkTreeItem | FolderGroupItem): node is BookmarkTreeItem { + return 'bookmark' in node; +} + +const EXTENSION_ID = 'Wikid82.workspace-file-bookmarks'; + +async function getApi(): Promise { + const extension = vscode.extensions.getExtension(EXTENSION_ID); + assert.ok(extension, `extension "${EXTENSION_ID}" is not installed in the test host`); + return extension.isActive ? extension.exports : await extension.activate(); +} + +function fixtureUri(relativePath: string): vscode.Uri { + const [workspaceFolder] = vscode.workspace.workspaceFolders ?? []; + assert.ok(workspaceFolder, 'expected the e2e fixture workspace to be open'); + return vscode.Uri.file(path.join(workspaceFolder.uri.fsPath, relativePath)); +} + +describe('Workspace File Bookmarks (e2e)', () => { + beforeEach(async () => { + const { store } = await getApi(); + for (const bookmark of store.getAllBookmarks()) { + store.removeBookmark(bookmark.id); + } + for (const folder of store.getAllFolders()) { + store.deleteFolder(folder.id); + } + }); + + it('activates and registers every contributed command', async () => { + await getApi(); + const commands = await vscode.commands.getCommands(true); + + for (const id of [ + 'workspace-file-bookmarks.addBookmark', + 'workspace-file-bookmarks.addBookmarkFromExplorer', + 'workspace-file-bookmarks.addBookmarkToFolder', + 'workspace-file-bookmarks.removeBookmark', + 'workspace-file-bookmarks.renameBookmark', + 'workspace-file-bookmarks.openBookmark', + 'workspace-file-bookmarks.createFolder', + 'workspace-file-bookmarks.renameFolder', + 'workspace-file-bookmarks.deleteFolder', + 'workspace-file-bookmarks.moveToFolder', + 'workspace-file-bookmarks.openAllInFolder', + 'workspace-file-bookmarks.setViewModeList', + 'workspace-file-bookmarks.setViewModeTree' + ]) { + assert.ok(commands.includes(id), `expected command "${id}" to be registered`); + } + }); + + it('bookmarks the active editor via the addBookmark command and lists it in the tree', async () => { + const { store, provider } = await getApi(); + const document = await vscode.workspace.openTextDocument(fixtureUri('src/sample-a.ts')); + await vscode.window.showTextDocument(document); + + await vscode.commands.executeCommand('workspace-file-bookmarks.addBookmark'); + + const bookmarks = store.getAllBookmarks(); + assert.equal(bookmarks.length, 1); + assert.equal(bookmarks[0].relativePath, 'src/sample-a.ts'); + + const children = provider.getChildren() as BookmarkTreeItem[]; + assert.equal(children.length, 1); + assert.equal(children[0].bookmark.relativePath, 'src/sample-a.ts'); + }); + + it('removes a bookmark via the removeBookmark command', async () => { + const { store, provider } = await getApi(); + const document = await vscode.workspace.openTextDocument(fixtureUri('src/sample-a.ts')); + await vscode.window.showTextDocument(document); + await vscode.commands.executeCommand('workspace-file-bookmarks.addBookmark'); + const [item] = provider.getChildren() as BookmarkTreeItem[]; + + await vscode.commands.executeCommand('workspace-file-bookmarks.removeBookmark', item); + + assert.deepEqual(store.getAllBookmarks(), []); + }); + + it('groups a bookmark under a folder created and assigned via commands', async () => { + const { store, provider } = await getApi(); + const folder = store.createFolder('Backend'); + const document = await vscode.workspace.openTextDocument(fixtureUri('src/sample-b.ts')); + await vscode.window.showTextDocument(document); + await vscode.commands.executeCommand('workspace-file-bookmarks.addBookmark'); + const [bookmarkItem] = (provider.getChildren() as (BookmarkTreeItem | FolderGroupItem)[]).filter(isBookmarkNode); + + store.moveBookmarkToFolder(bookmarkItem.bookmark.id, folder.id); + + const [folderNode] = provider.getChildren() as FolderGroupItem[]; + assert.equal(folderNode.folder.name, 'Backend'); + const folderChildren = provider.getChildren(folderNode) as BookmarkTreeItem[]; + assert.equal(folderChildren.length, 1); + assert.equal(folderChildren[0].bookmark.relativePath, 'src/sample-b.ts'); + }); +}); diff --git a/test/e2e/fixtures/workspace/src/sample-a.ts b/test/e2e/fixtures/workspace/src/sample-a.ts new file mode 100644 index 0000000..cc798ff --- /dev/null +++ b/test/e2e/fixtures/workspace/src/sample-a.ts @@ -0,0 +1 @@ +export const a = 1; diff --git a/test/e2e/fixtures/workspace/src/sample-b.ts b/test/e2e/fixtures/workspace/src/sample-b.ts new file mode 100644 index 0000000..2021030 --- /dev/null +++ b/test/e2e/fixtures/workspace/src/sample-b.ts @@ -0,0 +1 @@ +export const b = 2; diff --git a/tsconfig.json b/tsconfig.json index 7b410c3..402df11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "module": "preserve", "moduleResolution": "bundler", "lib": ["ES2022"], - "types": ["node", "vscode"], + "types": ["node", "vscode", "mocha"], "strict": true, "skipLibCheck": true, "noEmit": true diff --git a/vitest.config.mts b/vitest.config.mts index 8f88dbc..c76f7c3 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -8,6 +8,7 @@ export default defineConfig({ } }, test: { + exclude: ['**/node_modules/**', 'test/e2e/**', 'out-e2e/**', '.vscode-test/**'], setupFiles: ['./test/setup.ts'], clearMocks: true, coverage: {