From 2a074b0871bb03a69bba35610c93307189aae45c 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:28:11 -0400 Subject: [PATCH] chore: promote development to main (2026-08-21) (#57) 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> --- .github/workflows/ci.yml | 31 + .github/workflows/codeql.yml | 4 +- .github/workflows/promote-dev-to-main.yml | 4 +- .github/workflows/release-please.yml | 30 +- .vscodeignore | 4 + CLAUDE.md | 39 + codecov.yml | 38 + extension.ts | 52 +- package-lock.json | 1462 ++++++++++++++++++++- package.json | 18 +- scripts/local-patch-report.sh | 162 +++ scripts/wfb_dep_update.sh | 9 +- test/activate.test.ts | 51 + test/bookmarkStore.test.ts | 118 ++ test/commands.test.ts | 344 +++++ test/fakeContext.ts | 17 + test/setup.ts | 8 + test/toBookmark.test.ts | 30 + test/treeProvider.test.ts | 122 ++ test/vscode-mock.ts | 74 ++ vitest.config.mts | 25 + 21 files changed, 2582 insertions(+), 60 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/bookmarkStore.test.ts create mode 100644 test/commands.test.ts create mode 100644 test/fakeContext.ts create mode 100644 test/setup.ts create mode 100644 test/toBookmark.test.ts create mode 100644 test/treeProvider.test.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..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,8 +39,24 @@ 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 + - name: Test with coverage + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + fail_ci_if_error: false + - name: Build run: npm run build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d786dbe..4426d4c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,11 +31,11 @@ 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 - - uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 + - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 with: category: /language:javascript-typescript diff --git a/.github/workflows/promote-dev-to-main.yml b/.github/workflows/promote-dev-to-main.yml index f3cd6ca..89a1a05 100644 --- a/.github/workflows/promote-dev-to-main.yml +++ b/.github/workflows/promote-dev-to-main.yml @@ -7,13 +7,13 @@ 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: 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 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 4d8d078..261ce95 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@4d101475d8b20a2381f78447822ac1eab6504dd8 # 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' }} diff --git a/.vscodeignore b/.vscodeignore index e9e1eac..008da00 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -2,9 +2,13 @@ .github/** node_modules/** scripts/** +test/** 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..036bf8b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,39 @@ +# 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 + +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 +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 b6958fd..9596e0c 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; @@ -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)), @@ -53,7 +54,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; @@ -84,6 +85,10 @@ 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)}`, @@ -114,7 +119,7 @@ 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 +128,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 +145,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; @@ -207,7 +212,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); @@ -219,7 +224,7 @@ function describeBookmark(bookmark: Bookmark, isMultiRoot: boolean, folderName: 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 +241,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 +250,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 +267,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 +277,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 +288,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 +299,18 @@ async function renameFolder(store: BookmarkStore, item: FolderGroupItem) { } } -async function deleteFolder(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( `Delete folder "${item.folder.name}"? ${item.bookmarks.length} bookmark(s) will move to the root list.`, @@ -312,7 +328,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 +356,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 +364,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 d5878f5..a807b76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "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" + "typescript": "~6.0.3", + "vitest": "^4.1.11" }, "engines": { "vscode": "^1.134.0" @@ -229,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", @@ -239,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", @@ -681,6 +740,34 @@ "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", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -719,6 +806,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 +1283,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 +1376,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", @@ -1021,21 +1430,165 @@ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.134.0.tgz", "integrity": "sha512-NDEu0hg4sF7+vvFsADsktqUJ6f80LHSZvVK2Ovo1XiQ0/VHck1O3zst+ZZyVA/uvz6vo6LcuoqU2q48YMqOwWw==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.8.tgz", + "integrity": "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "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", + "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/@typespec/ts-http-runtime": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.8.tgz", - "integrity": "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==", + "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": { - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=22.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vscode/vsce": { @@ -1309,6 +1862,28 @@ "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/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", @@ -1528,6 +2103,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 +2235,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 +2377,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -1969,6 +2560,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 +2638,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 +2659,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 +2720,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 +2791,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", @@ -2349,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", @@ -2578,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", @@ -2597,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" }, @@ -2717,22 +3414,295 @@ "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, - "hasInstallScript": true, - "license": "MIT", + "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/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, + "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 +3801,44 @@ "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/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", @@ -3009,6 +4017,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 +4141,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 +4329,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 +4373,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 +4604,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 +4824,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 +4911,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 +4957,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 +5199,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 +5442,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 +5660,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..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", @@ -208,13 +218,17 @@ "vscode:prepublish": "npm run build", "package": "bash scripts/rebuild.sh", "clean": "rm -rf dist", - "lint": "tsc --noEmit" + "lint": "tsc --noEmit", + "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" + "typescript": "~6.0.3", + "vitest": "^4.1.11" } } 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/scripts/wfb_dep_update.sh b/scripts/wfb_dep_update.sh index e3ed0b9..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")); @@ -62,6 +64,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/activate.test.ts b/test/activate.test.ts new file mode 100644 index 0000000..aeeca56 --- /dev/null +++ b/test/activate.test.ts @@ -0,0 +1,51 @@ +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(13); + expect((context as any).subscriptions).toHaveLength(14); // tree view + 13 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.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 }); + 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(13); + }); +}); + +describe('deactivate', () => { + it('is a no-op', () => { + expect(() => deactivate()).not.toThrow(); + }); +}); diff --git a/test/bookmarkStore.test.ts b/test/bookmarkStore.test.ts new file mode 100644 index 0000000..4a81a63 --- /dev/null +++ b/test/bookmarkStore.test.ts @@ -0,0 +1,118 @@ +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('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'); + 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/commands.test.ts b/test/commands.test.ts new file mode 100644 index 0000000..0122d60 --- /dev/null +++ b/test/commands.test.ts @@ -0,0 +1,344 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + BookmarkStore, + addActiveFileBookmark, + addBookmarksForUris, + addBookmarksToFolder, + createFolder, + deleteFolder, + moveToFolder, + openAllInFolder, + openBookmark, + pickFolder, + renameBookmark, + 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('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(); + 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/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/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 new file mode 100644 index 0000000..e4489b5 --- /dev/null +++ b/test/vscode-mock.ts @@ -0,0 +1,74 @@ +// Minimal stand-in for the 'vscode' module, covering only the surface that +// 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> = []; + + event = (listener: (e: T) => void) => { + this.listeners.push(listener); + return { dispose: () => {} }; + }; + + fire(data: T) { + for (const listener of this.listeners) { + listener(data); + } + } +} + +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; + } +} + +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 new file mode 100644 index 0000000..8f88dbc --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,25 @@ +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') + } + }, + 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 + } + } + } +});