diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c793582c..00000000 --- a/.eslintrc +++ /dev/null @@ -1,106 +0,0 @@ -{ - "env": { - "node": true, - "browser": true - }, - "plugins": [ - "svelte3" - ], - "overrides": [ - { - "files": ["*.svelte"], - "processor": "svelte3/svelte3" - } - ], - "parserOptions": { - "ecmaVersion": 2019, - "sourceType": "module" - }, - "rules": { - "accessor-pairs": "error", - "block-spacing": ["error", "never"], - "brace-style": ["error", "stroustrup", {"allowSingleLine": true}], - "curly": ["error", "multi-line", "consistent"], - "dot-location": ["error", "property"], - "dot-notation": "error", - // "eqeqeq": ["error", "smart"], - "func-call-spacing": "error", - "handle-callback-err": "error", - "key-spacing": "error", - "keyword-spacing": "error", - "linebreak-style": ["error", "windows"], - "new-cap": ["error", {"newIsCap": true}], - "no-array-constructor": "error", - "no-caller": "error", - "no-console": "error", - "no-duplicate-imports": "error", - "no-else-return": "error", - "no-eval": "error", - "no-floating-decimal": "error", - "no-implied-eval": "error", - "no-iterator": "error", - "no-label-var": "error", - "no-labels": "error", - "no-lone-blocks": "error", - "no-mixed-spaces-and-tabs": "error", - "no-multi-spaces": "error", - "no-multi-str": "error", - "no-new": "error", - "no-new-func": "error", - "no-new-object": "error", - "no-new-wrappers": "error", - "no-octal-escape": "error", - "no-path-concat": "error", - "no-proto": "error", - "no-prototype-builtins": "off", - "no-redeclare": ["error", {"builtinGlobals": true}], - "no-self-compare": "error", - "no-sequences": "error", - "no-shadow": ["warn", {"builtinGlobals": false, "hoist": "functions"}], - "no-tabs": "error", - "no-template-curly-in-string": "error", - "no-throw-literal": "error", - // "no-trailing-spaces": "error", - "no-undef": "error", - "no-undef-init": "error", - "no-unmodified-loop-condition": "error", - "no-unneeded-ternary": "error", - "no-useless-call": "error", - "no-useless-computed-key": "error", - "no-useless-constructor": "error", - "no-useless-rename": "error", - "no-var": "error", - "no-whitespace-before-property": "error", - "object-curly-spacing": ["error", "never", {"objectsInObjects": false}], - "object-property-newline": ["error", {"allowAllPropertiesOnSameLine": true}], - "operator-linebreak": ["error", "none", {"overrides": {"?": "before", ":": "before"}}], - "prefer-const": "error", - "quote-props": ["error", "consistent-as-needed", {"keywords": true}], - "quotes": ["error", "double", {"allowTemplateLiterals": true}], - "rest-spread-spacing": "error", - "semi": "error", - "semi-spacing": "error", - "space-before-blocks": "error", - "space-in-parens": "error", - "space-infix-ops": "error", - "space-unary-ops": ["error", {"words": true, "nonwords": false, "overrides": {"typeof": false}}], - "spaced-comment": ["error", "always", {"exceptions": ["-", "*"]}], - "template-curly-spacing": "error", - "wrap-iife": ["error", "inside"], - "yield-star-spacing": "error", - "yoda": "error" - }, - "globals": { - "Proxy": "readonly", - "Set": "readonly", - "WeakMap": "readonly", - "Map": "readonly", - "Promise": "readonly", - "Reflect": "readonly", - "DiscordNative": "readonly", - "__non_webpack_require__": "readonly", - "Symbol": "readonly", - "__static": "readonly", - "status": "off" - } -} \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 04acc8c3..cf50b2fa 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: rauenzi +github: zerebos diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..1efb8266 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + frontend: + name: Frontend Checks + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install + working-directory: frontend + + - name: Typecheck + run: bun run check + working-directory: frontend + + - name: Lint + run: bun run lint + working-directory: frontend + + - name: Build + run: bun run build + working-directory: frontend + + backend: + name: Go Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run tests + run: go test ./... + + build: + name: Wails Build + runs-on: ubuntu-latest + needs: + - frontend + - backend + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Linux dependencies + if: ${{ runner.os == 'Linux' }} + run: sudo apt-get -yq update && sudo apt-get -yq install libgtk-3-0 libwebkit2gtk-4.1-dev gcc-aarch64-linux-gnu + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install Wails + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0 + + - name: Build + run: wails build -tags webkit2_41 diff --git a/.github/workflows/release-pipeline.yml b/.github/workflows/release-pipeline.yml deleted file mode 100644 index 55c17461..00000000 --- a/.github/workflows/release-pipeline.yml +++ /dev/null @@ -1,190 +0,0 @@ -name: Release Pipeline - -# Execution needs to be triggered manually at https://github.com/BetterDiscord/Installer/actions/workflows/release-pipeline.yml. - -# This pipeline is pretty hard-coded and will deliver -# identical results no matter the branch it's run against. - -on: - workflow_dispatch: - inputs: - version_tag: - description: The version label to be used for this release - required: true - -concurrency: release - -jobs: - - # Checkout 'release', merge 'development', bump version, upload source artifact. - prepare: - name: Prepare Repo - runs-on: ubuntu-latest - outputs: - old_version: ${{ steps.version_bump.outputs.old_version }} - new_version: ${{ steps.version_bump.outputs.new_version }} - steps: - - - name: checkout 'release' - uses: actions/checkout@v2 - with: - ref: 'release' - fetch-depth: 0 - - - name: merge 'development' and bump version - id: version_bump - run: | - git config --global user.name "BetterDiscord CI" - git config --global user.email "BetterDiscord@users.noreply.github.com" - git merge --no-ff --no-commit 'origin/development' - node << 'EOF' - const fs = require("fs"); - const packageJson = JSON.parse(fs.readFileSync("package.json", "utf-8")); - let version = process.env.VERSION_TAG; - if (/^v\d/.test(version)) version = version.slice(1); - if ( - !/^\d/.test(version) || version.includes("-") && !/[a-z\d]$/.test(version) - ) throw new Error(`Bad version tag: '${process.env.VERSION_TAG}'`); - if (version.includes("-") && !/\.\d+$/.test(version)) version += ".0"; - version = version.replace(/\.+/g, "."); - fs.writeFileSync("package.json", JSON.stringify({ - ...packageJson, - version, - }, null, 2) + "\n"); - let oldVersion = "v" + packageJson.version; - let newVersion = "v" + version; - console.log(`::set-output name=old_version::${oldVersion}`); - console.log(`::set-output name=new_version::${newVersion}`); - fs.writeFileSync("commit-message", `CI: Prepare release '${newVersion}`); - EOF - git add package.json - git commit -F commit-message - env: - VERSION_TAG: ${{ github.event.inputs.version_tag }} - - - uses: actions/upload-artifact@v2 - with: - name: source - path: | - ./* - !.git/config - - # Download source artifact, build, upload build artifact. - # Runs once on each release platform. - build: - name: Build - needs: prepare - strategy: - fail-fast: true - matrix: - os: - # ordered by how fast they build (muh cosmetics) - - ubuntu-latest - - windows-latest - - macos-latest - runs-on: ${{ matrix.os }} - steps: - - - uses: actions/download-artifact@v2 - with: - name: source - - - run: yarn install && yarn dist - env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - - - uses: actions/upload-artifact@v2 - if: ${{ success() && matrix.os == 'ubuntu-latest' }} - with: - name: build - path: dist/BetterDiscord-Linux.AppImage - if-no-files-found: error - - - uses: actions/upload-artifact@v2 - if: ${{ success() && matrix.os == 'windows-latest' }} - with: - name: build - path: dist/BetterDiscord-Windows.exe - if-no-files-found: error - - - if: ${{ success() && matrix.os == 'macos-latest' }} - run: mv -f dist/BetterDiscord-*-mac.zip dist/BetterDiscord-Mac.zip - - if: ${{ success() && matrix.os == 'macos-latest' }} - uses: actions/upload-artifact@v2 - with: - name: build - path: dist/BetterDiscord-Mac.zip - if-no-files-found: error - - # Download source artifact, push to 'release'. - push_changes: - name: Push Version Bump - needs: - - prepare - - build - runs-on: ubuntu-latest - outputs: - sha_1: ${{ steps.push.outputs.sha_1 }} - steps: - - - name: checkout 'development' - uses: actions/checkout@v2 - with: - ref: 'development' - token: ${{ secrets.CI_PAT }} - clean: false - - uses: actions/download-artifact@v2 - with: - name: source - - - name: merge 'release' and push everything - id: push - run: | - git checkout 'development' - git config --global user.name "BetterDiscord CI" - git config --global user.email "BetterDiscord@users.noreply.github.com" - # TODO: rebase 'development' on 'release' - git merge --ff-only --no-commit 'release' - SHA1=$(git rev-parse --verify HEAD) - git tag "$NEW_VERSION" "$SHA1" - git push --all && git push --tags - echo "::set-output name=sha_1::$SHA1" - env: - NEW_VERSION: ${{ needs.prepare.outputs.new_version }} - - # Download build artifact, do github release. - publish: - name: Draft Release - needs: - - prepare - - build - - push_changes - runs-on: ubuntu-latest - steps: - - - uses: actions/checkout@v2 - with: - ref: ${{ needs.push_changes.outputs.sha_1 }} - - - name: authenticate gh-cli - run: echo "$GITHUB_TOKEN_STUPIDO" | gh auth login --with-token - env: - GITHUB_TOKEN_STUPIDO: ${{ secrets.CI_PAT }} - - - uses: actions/download-artifact@v2 - with: - name: build - - - name: upload release draft - run: | - gh release create "$NEW_VERSION" --draft --title "$NEW_VERSION" --target "$SHA1" --generate-notes \ - "BetterDiscord-Linux.AppImage#Linux (AppImage)" \ - "BetterDiscord-Mac.zip#Mac OS (Zip)" \ - "BetterDiscord-Windows.exe#Windows (Exe)" - env: - SHA1: ${{ needs.push_changes.outputs.sha_1 }} - OLD_VERSION: ${{ needs.prepare.outputs.old_version }} - NEW_VERSION: ${{ needs.prepare.outputs.new_version }} - NAKED_INPUT_VERSION_TAG: ${{ github.event.inputs.version_tag }} - RELEASE_NOTES_TEMPLATE_LOCATION: .github/RELEASE_TEMPLATE.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..47ece9d6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,132 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + build: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Linux dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sudo apt-get -yq update && sudo apt-get -yq install libgtk-3-0 libwebkit2gtk-4.1-dev gcc-aarch64-linux-gnu + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install Wails + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0 + + - name: Derive version + id: version + shell: bash + run: echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + + - name: Sync Wails product version + shell: bash + run: | + bun << 'EOF' + const fs = require("fs"); + const version = process.env.RELEASE_VERSION.replace(/^v/, ""); + const wailsConfig = JSON.parse(fs.readFileSync("wails.json", "utf-8")); + wailsConfig.info.productVersion = version; + fs.writeFileSync("wails.json", JSON.stringify(wailsConfig, null, 2) + "\n"); + EOF + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} + + - name: Build installer (Linux) + if: ${{ matrix.os == 'ubuntu-latest' }} + shell: bash + run: wails build -tags webkit2_41 -ldflags="-X main.version=${{ steps.version.outputs.version }}" + + - name: Build installer (Windows) + if: ${{ matrix.os == 'windows-latest' }} + shell: bash + run: wails build -webview2 embed -ldflags="-X main.version=${{ steps.version.outputs.version }}" + + - name: Build installer (macOS) + if: ${{ matrix.os == 'macos-latest' }} + shell: bash + run: wails build -ldflags="-X main.version=${{ steps.version.outputs.version }}" + + - name: Upload Linux binary + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: actions/upload-artifact@v4 + with: + name: build-linux + path: build/bin/BetterDiscord-Installer + if-no-files-found: error + + - name: Upload Windows binary + if: ${{ matrix.os == 'windows-latest' }} + uses: actions/upload-artifact@v4 + with: + name: build-windows + path: build/bin/BetterDiscord-Installer.exe + if-no-files-found: error + + - name: Upload macOS app + if: ${{ matrix.os == 'macos-latest' }} + uses: actions/upload-artifact@v4 + with: + name: build-macos + path: build/bin/BetterDiscord-Installer.app + if-no-files-found: error + + release: + name: Create Release + runs-on: ubuntu-latest + needs: build + steps: + - name: Download Linux artifact + uses: actions/download-artifact@v4 + with: + name: build-linux + path: release + + - name: Download macOS artifact + uses: actions/download-artifact@v4 + with: + name: build-macos + path: release + + - name: Download Windows artifact + uses: actions/download-artifact@v4 + with: + name: build-windows + path: release + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${GITHUB_REF_NAME}" --draft --title "${GITHUB_REF_NAME}" --generate-notes \ + "release/BetterDiscord-Installer#Linux" \ + "release/BetterDiscord-Installer.app#macOS" \ + "release/BetterDiscord-Installer.exe#Windows" diff --git a/.gitignore b/.gitignore index 72623cce..b41d3666 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,11 @@ dist/ node_modules/ thumbs.db .idea/ -app/ \ No newline at end of file +app/ +build/bin +snippets/ +.task/ +*.md +!README.md +!CONTRIBUTING.md +*.log \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 5071bdc4..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eslint.validate": [ "javascript", "svelte" ] -} \ No newline at end of file diff --git a/.yarnclean b/.yarnclean deleted file mode 100644 index 09cfc9ec..00000000 --- a/.yarnclean +++ /dev/null @@ -1,39 +0,0 @@ -# test directories -__tests__ -node_modules/*/test -node_modules/*/tests -powered-test - -# asset directories -docs -doc -website -images - -# examples -example -examples - -# code coverage directories -coverage -.nyc_output - -# build scripts -Makefile -Gulpfile.js -Gruntfile.js - -# configs -.tern-project -.gitattributes -.editorconfig -.*ignore -.eslintrc -.jshintrc -.flowconfig -.documentup.json -.yarn-metadata.json - -# misc -*.gz -*.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b08f7fe..6646794d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,7 @@ The following is a set of guidelines for contributing to BetterDiscord's Install [Code of Conduct](#code-of-conduct) [What should I know before I get started?](#what-should-i-know-before-i-get-started) + * [Development Setup](#development-setup) [How Can I Contribute?](#how-can-i-contribute) * [Reporting Bugs](#reporting-bugs) @@ -18,7 +19,8 @@ The following is a set of guidelines for contributing to BetterDiscord's Install [Styleguides](#styleguides) * [Git Commit Messages](#git-commit-messages) - * [JavaScript Styleguide](#javascript-styleguide) + * [Frontend (Svelte + TypeScript) Styleguide](#frontend-svelte--typescript-styleguide) + * [Go Styleguide](#go-styleguide) [Additional Notes](#additional-notes) * [Issue Labels](#issue-labels) @@ -29,33 +31,79 @@ This project and everyone participating in it is governed by the [Code of Conduc ## What should I know before I get started? - +This installer is built with [Wails](https://wails.io/) (Go backend + Svelte frontend). The Go runtime embeds the compiled frontend assets from `frontend/build` (see `//go:embed` in `main.go`). + +The repository is organized into: + +``` +. +├── api // Wails bindings and backend runtime helpers. +├── build // Build assets and platform-specific packaging files. +├── frontend // Svelte UI bundled by Vite (Bun-powered). +├── types // Shared Go types used by bindings. +├── utils // Backend utilities. +├── main.go // Wails application entry point. +├── wails.json // Wails configuration + frontend hooks. +``` + +### Development Setup + +Prerequisites: + +* [Go](https://go.dev/) (matches `go.mod`) +* [Bun](https://bun.sh/) +* [Wails CLI](https://wails.io/docs/gettingstarted/installation) + +Linux prerequisites: + +* Wails requires GTK/WebKit and build tooling. Follow the official Linux dependencies guide: + https://wails.io/docs/gettingstarted/installation#linux +* Fedora note: you may need to set `PKG_CONFIG_PATH` to include system pkgconfig directories: + ```sh + export PKG_CONFIG_PATH="/usr/lib64/pkgconfig:/usr/share/pkgconfig" + ``` + +Common commands: + +* `wails dev` - run the app locally with backend bindings. +* `wails build` - build distributable binaries. +* `bun run --bun eslint .` (from `frontend/`) - lint frontend changes. +* `bun run --bun svelte-check --tsconfig ./tsconfig.json` (from `frontend/`) - static checks. +* `go test ./...` - run backend tests. +* `gofmt -w .` - format Go files you touch. + +Build and development details: + +* Frontend assets are built by Vite into `frontend/build` and embedded by Go via `//go:embed` in `main.go`. +* Wails uses the hooks in `wails.json` to install and build frontend assets during `wails dev` and `wails build`. ## How Can I Contribute? ### Reporting Bugs - +Please search for existing issues first. If you find a match, add any extra context you have (logs, OS details, screenshots). #### Before Submitting A Bug Report - +* Reproduce on the latest `development` branch or a recent release. +* Collect logs from the installer UI (copy any error output shown). +* Note your OS and architecture (Windows/macOS/Linux, x64/arm64). #### How Do I Submit A (Good) Bug Report? - +Include clear steps to reproduce, expected vs. actual behavior, and any logs shown in the UI. ### Suggesting Enhancements - +Open an issue describing the problem you are trying to solve, the proposed solution, and any alternatives considered. #### Before Submitting An Enhancement Suggestion - +Confirm the change aligns with the existing installer flow and doesn't require a new runtime permission without a clear UX plan. #### How Do I Submit A (Good) Enhancement Suggestion? - +Provide context, screenshots/mockups (if UI changes), and a minimal spec of expected behavior. ### Your First Code Contribution @@ -81,9 +129,9 @@ While the prerequisites above must be satisfied prior to having your pull reques * Reference issues and pull requests liberally after the first line * When only changing documentation, include `[ci skip]` in the commit title -### JavaScript Styleguide +### Frontend (Svelte + TypeScript) Styleguide -All JavaScript must adhere to the [ESLint rules](https://github.com/BetterDiscord/Installer/blob/main/.eslintrc) of the repo. +All frontend code must adhere to the [ESLint rules](https://github.com/BetterDiscord/Installer/blob/main/frontend/eslint.config.js) of the repo. Some other style related points not covered by ESLint: @@ -104,13 +152,11 @@ Some other style related points not covered by ESLint: * Place class properties in the following order: * Class methods and properties (methods starting with `static`) * Instance methods and properties -* Place requires in the following order: - * Built in Node Modules (such as `path`) - * Repo level global imports (such as `modules`, `builtins`) - * Local Modules (using relative paths) -* Prefer to import whole modules instead of singular functions - * Keep modules namespaced and organized - * This includes Node Modules (such as `fs`) +* Place imports in the following order: + * Built in modules (such as `path`) + * Third-party dependencies + * Local modules (relative paths or `$lib` aliases) +* Prefer to import whole modules instead of singular functions when working in Node/Go tooling ```js const fs = require("fs"); // Use this const {readFile, writeFile} = require("fs"); // Avoid this @@ -119,12 +165,31 @@ import Utilities from "./utilities"; // Use this import {deepclone, isEmpty} from "./utilties"; // Avoid this ``` +### Go Styleguide + +* Run `gofmt` on any Go files you touch. +* Keep standard library imports first, then a blank line, then external deps, then local packages. +* Prefer early returns for error handling and emit Wails events (`log`, `success`, `failure`, `reset`) instead of silently swallowing failures. + ## Additional Notes +### Releases + +Releases are tag-driven. Create a tag like `vX.Y.Z` and push it to trigger the release workflow. The workflow uses the tag to: + +* Set the runtime version via `-ldflags="-X main.version=vX.Y.Z"`. +* Override `wails.json` `info.productVersion` during CI builds so Windows metadata matches the tag. + +If you build locally for a release, pass the same ldflags and update `wails.json` to keep metadata aligned: + +```sh +wails build -ldflags="-X main.version=vX.Y.Z" +``` + ### Issue Labels - +Use labels to clarify impact (`bug`, `enhancement`, `maintenance`) and scope (`frontend`, `backend`, `release`). #### Type of Issue and Issue State - +If you can, include an expected fix timeline or milestone to help triage. diff --git a/README.md b/README.md index e3c0e8ac..ad07461b 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ # Overview -This repository contains the source code for the BetterDiscord installer. This installer is written with [electron-webpack](https://webpack.electron.build/) and [Svelte 3](https://svelte.dev/). +This repository contains the source code for the BetterDiscord installer. The app is built with [Wails](https://wails.io/) (Go backend + Svelte frontend) and ships as a native desktop application. ## Downloads These will link you to the latest builds found in the [releases](https://github.com/BetterDiscord/installer/releases/) tab of this repository. -| [Windows (7+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Windows.exe) | [macOS (10.10+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Mac.zip) | [Linux](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Linux.AppImage) | +| [Windows (7+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Windows.exe) | [macOS (10.10+)](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Mac.zip) | [Linux](https://github.com/BetterDiscord/Installer/releases/latest/download/BetterDiscord-Linux) | | ------------- | ------------- | ------------- | @@ -42,19 +42,13 @@ These will link you to the latest builds found in the [releases](https://github. ``` . -├──assets // Contains static assets (such as images) used by the installer. -| └──images // Images (logos, backgrounds, etc...) used by the installer. -├──scripts // Scripts needed for development and contributing. -└──src // The installer's source code. - ├──main // Electron "main" process. Creates and configures the BrowserWindow. - └──renderer // Electron "renderer" process. Contains most components and scripts. - ├──actions // Scripts performed by the installer such as installing, repairing and uninstalling. - | └──utils // Common utilities used by installer actions (such as killing discord). - ├──common // Common UI components such as buttons, checkboxes, radios, etc... - ├──pages // Component files for each page in the installer's setup process. - ├──stores // Svelte store used for storing global data. - | └──types // Used for defining custom svelte stores. - └──transitions // Contains custom Svelte transitions and animations. +├── api // Wails bindings and backend runtime helpers. +├── build // Build assets and platform-specific packaging files. +├── frontend // Svelte UI bundled by Vite (Bun-powered). +├── types // Shared Go types used by bindings. +├── utils // Backend utilities. +├── main.go // Wails application entry point. +├── wails.json // Wails configuration + frontend hooks. ``` --- @@ -65,10 +59,23 @@ These will link you to the latest builds found in the [releases](https://github. ## Prerequisites - [Git](https://git-scm.com) -- [Node.js](https://nodejs.org/en/) -- [yarn](https://yarnpkg.com) +- [Go](https://go.dev/) (matches `go.mod`) +- [Bun](https://bun.sh/) +- [Wails CLI](https://wails.io/docs/gettingstarted/installation) - Command line of your choice. +### Linux prerequisites + +Wails requires additional system packages on Linux (GTK/WebKit and build tooling). See the official Wails Linux dependencies guide: + +https://wails.io/docs/gettingstarted/installation#linux + +Fedora note: you may need to set `PKG_CONFIG_PATH` to include system pkgconfig directories: + +```sh +export PKG_CONFIG_PATH="/usr/lib64/pkgconfig:/usr/share/pkgconfig" +``` + ## Building ### 1: Clone the repository. @@ -77,32 +84,48 @@ git clone https://github.com/BetterDiscord/installer && cd installer ``` This will create a local copy of this repository and navigate you to the root folder of the repository. -### 2: Install Dependencies -Run this command at the root folder to install dependencies: +### Quick Start (Development) + +To run the installer locally with backend bindings: + ```ps -yarn install +wails dev ``` -### 3: Run Build Script -To run the installer in development mode, simply run the following command: +To build a distributable binary: + ```ps -yarn dev +wails build ``` ## Additional Scripts -### Linting -This project uses [ESLint](https://eslint.org/). Run this command to lint your changes: +### Frontend checks + ```ps -yarn lint +cd frontend +bun install +bun run --bun svelte-kit sync +bun run --bun svelte-check --tsconfig ./tsconfig.json +bun run --bun eslint . ``` -### Compiling & Distribution +### Frontend tests ```ps -yarn dist +cd frontend +bun install +bun run test ``` +### Backend checks + +```ps +go test ./... +go fmt ./... +``` + + --- # Contributors diff --git a/api/controller.go b/api/controller.go new file mode 100644 index 00000000..986ca841 --- /dev/null +++ b/api/controller.go @@ -0,0 +1,172 @@ +package api + +import ( + "context" + "fmt" + "os" + + "installer/discord" + "installer/types" + + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +type Controller struct { + ctx context.Context +} + +func NewController() *Controller { + return &Controller{} +} + +// SetContext is called when the app starts. The context is saved +// so we can call the runtime methods +func (action *Controller) SetContext(ctx context.Context) { + action.ctx = ctx +} + +func (a *Controller) Write(p []byte) (n int, err error) { + fmt.Println(string(p[:])) + runtime.EventsEmit(a.ctx, "log", string(p[:])) + return len(p), nil +} + +func (d *Controller) GetDiscordPath(channel string) string { + return discord.GetSuggestedPath(types.ParseChannel(channel)) +} + +// #region Actions +func (action *Controller) Install(corePaths []string, options types.InstallOptions) { + for i := range corePaths { + install := discord.ResolvePath(corePaths[i]) + if install == nil { + continue + } + + if err := install.InstallBD(options); err != nil { + runtime.EventsEmit(action.ctx, "failure") + return + } + } + + runtime.EventsEmit(action.ctx, "success") +} + +func (action *Controller) Uninstall(corePaths []string, options types.UninstallOptions) { + for i := range corePaths { + install := discord.ResolvePath(corePaths[i]) + if install == nil { + continue + } + + if err := install.UninstallBD(options); err != nil { + runtime.EventsEmit(action.ctx, "failure") + return + } + } + + runtime.EventsEmit(action.ctx, "success") +} + +func (action *Controller) Repair(corePaths []string, options types.RepairOptions) { + for i := range corePaths { + install := discord.ResolvePath(corePaths[i]) + if install == nil { + continue + } + + if err := install.RepairBD(options); err != nil { + runtime.EventsEmit(action.ctx, "failure") + return + } + } + + result, err := runtime.MessageDialog(action.ctx, runtime.MessageDialogOptions{ + Type: runtime.QuestionDialog, + Title: "Repair Complete", + Message: "Repair is complete. Would you like to reinstall BetterDiscord now?", + DefaultButton: "Yes", + }) + + if err != nil { + runtime.EventsEmit(action.ctx, "success") + return + } + + if result == "Yes" { + runtime.EventsEmit(action.ctx, "reset") + runtime.EventsEmit(action.ctx, "navigate", map[string]string{ + "action": "install", + }) + return + } + + runtime.EventsEmit(action.ctx, "success") +} + +// #endregion + +// #region Dialogs +func (d *Controller) BrowseForDiscord(schannel string) string { + var browsePath string + browsePath, err := os.UserConfigDir() + if err != nil { + browsePath = os.Getenv("HOME") + } + + channel := types.ParseChannel(schannel) + + selection, err := runtime.OpenDirectoryDialog(d.ctx, runtime.OpenDialogOptions{ + Title: "Browsing to " + channel.Name(), + DefaultDirectory: browsePath, + ShowHiddenFiles: true, + TreatPackagesAsDirectories: true, + }) + + if err != nil || selection == "" { + return "" + } + + if result := discord.ResolvePath(selection); result != nil { + return result.CorePath + } + + return "" +} + +func (d *Controller) ConfirmAction(title string, message string) string { + result, err := runtime.MessageDialog(d.ctx, runtime.MessageDialogOptions{ + Type: runtime.QuestionDialog, + Title: title, + Message: message, + DefaultButton: "No", + }) + + if err != nil { + return "" + } + + return result +} + +func (d *Controller) ShowNotice(dialog string, title string, message string) string { + + dialogType := runtime.InfoDialog + if dialog == "error" { + dialogType = runtime.ErrorDialog + } + + result, err := runtime.MessageDialog(d.ctx, runtime.MessageDialogOptions{ + Type: dialogType, + Title: title, + Message: message, + }) + + if err != nil { + return "" + } + + return result +} + +// #endregion diff --git a/app.go b/app.go new file mode 100644 index 00000000..65684915 --- /dev/null +++ b/app.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "installer/types" + "installer/utils" + + "github.com/pkg/browser" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +type App struct { + ctx context.Context +} + +func NewApp() *App { + return &App{} +} + +func (a *App) SetContext(ctx context.Context) { + a.ctx = ctx +} + +func (a *App) GetVersion() string { + return version +} + +func (a *App) CheckForUpdate() { + // If the version doesn't start with "v" then it's a + // development build, so we don't check for updates + if !strings.HasPrefix(version, "v") { + return + } + + // Get latest installer version from GitHub API + apiData, err := utils.DownloadJSON[types.GitHubRelease]("https://api.github.com/repos/BetterDiscord/Installer/releases/latest") + if err != nil { + return + } + + // If the current version is greater than or equal + // to the latest version, no update is needed + if utils.CompareVersions(version, apiData.TagName) >= 0 { + return + } + + result, err := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Type: runtime.QuestionDialog, + Title: "Update Available", + Message: fmt.Sprintf("A new version (%s) of the installer is available. Would you like to download it now?", apiData.TagName), + DefaultButton: "Yes", + }) + + if err != nil { + return + } + + if result == "Yes" { + browser.OpenURL(apiData.HTMLURL) + } +} diff --git a/assets/images/splash.bmp b/assets/images/splash.bmp deleted file mode 100644 index ddef694e..00000000 Binary files a/assets/images/splash.bmp and /dev/null differ diff --git a/betterdiscord/download.go b/betterdiscord/download.go new file mode 100644 index 00000000..4c4ece7f --- /dev/null +++ b/betterdiscord/download.go @@ -0,0 +1,77 @@ +package betterdiscord + +import ( + "fmt" + "installer/types" + "installer/utils" + "log" +) + +func (i *BDInstall) download() error { + if i.hasDownloaded { + log.Printf("✅ Already downloaded to %s\n", i.asar) + return nil + } + + resp, err := utils.DownloadFile("https://betterdiscord.app/Download/betterdiscord.asar", i.asar) + if err == nil { + version := resp.Header.Get("x-bd-version") + if version == "" { + log.Println("✅ Downloaded BetterDiscord from the official website") + } else { + log.Printf("✅ Downloaded BetterDiscord version %s from the official website\n", utils.FormatVersion(version)) + } + i.hasDownloaded = true + return nil + } else { + log.Println("❌ Failed to download BetterDiscord from official website") + log.Printf("❌ %s\n", err.Error()) + log.Println("") + log.Println("🔁 Falling back to GitHub...") + } + + // Get download URL from GitHub API + apiData, err := utils.DownloadJSON[types.GitHubRelease]("https://api.github.com/repos/BetterDiscord/BetterDiscord/releases/latest") + if err != nil { + log.Println("❌ Failed to get asset url from GitHub") + log.Printf("❌ %s\n", err.Error()) + return err + } + + var index = -1 + for idx, asset := range apiData.Assets { + if asset.Name == "betterdiscord.asar" { + index = idx + break + } + } + + if index == -1 { + log.Println("❌ Failed to find the BetterDiscord asar on GitHub") + return fmt.Errorf("failed to find betterdiscord.asar asset in GitHub release") + } + + var downloadUrl = apiData.Assets[index].URL + var version = apiData.TagName + + if downloadUrl != "" { + log.Printf("✅ Found BetterDiscord: %s\n", downloadUrl) + } + + // Download asar into the BD folder + _, err = utils.DownloadFile(downloadUrl, i.asar) + if err != nil { + log.Println("❌ Failed to download BetterDiscord from GitHub") + log.Printf("❌ %s\n", err.Error()) + return err + } + + if version == "" { + log.Println("✅ Downloaded BetterDiscord from GitHub") + } else { + log.Printf("✅ Downloaded BetterDiscord version %s from GitHub\n", utils.FormatVersion(version)) + } + i.hasDownloaded = true + + return nil +} diff --git a/betterdiscord/install.go b/betterdiscord/install.go new file mode 100644 index 00000000..d862d5d5 --- /dev/null +++ b/betterdiscord/install.go @@ -0,0 +1,131 @@ +package betterdiscord + +import ( + "installer/types" + "installer/utils" + "installer/wsl" + "log" + "os" + "path/filepath" + "sync" +) + +type BDInstall struct { + root string + data string + asar string + plugins string + themes string + hasDownloaded bool +} + +// Root returns the root directory path of the BetterDiscord installation +func (i *BDInstall) Root() string { + return i.root +} + +// Data returns the data directory path +func (i *BDInstall) Data() string { + return i.data +} + +// Asar returns the path to the BetterDiscord asar file +func (i *BDInstall) Asar() string { + return i.asar +} + +// Plugins returns the plugins directory path +func (i *BDInstall) Plugins() string { + return i.plugins +} + +// Themes returns the themes directory path +func (i *BDInstall) Themes() string { + return i.themes +} + +// HasDownloaded returns whether BetterDiscord has been downloaded +func (i *BDInstall) HasDownloaded() bool { + return i.hasDownloaded +} + +// Download downloads the BetterDiscord asar file +func (i *BDInstall) Download() error { + return i.download() +} + +// Prepare creates all necessary directories for BetterDiscord +func (i *BDInstall) Prepare() error { + return i.prepare() +} + +// Repair repairs the BetterDiscord installation with specific options +func (i *BDInstall) Repair(channel types.DiscordChannel, options types.RepairOptions) error { + return i.repair(channel, options) +} + +func (i *BDInstall) IsAsarInstalled() bool { + return utils.Exists(i.asar) +} + +// RemoveAll deletes the BetterDiscord installation directory and all contents. +func (i *BDInstall) RemoveAll() error { + if !utils.Exists(i.root) { + log.Printf("✅ BetterDiscord folder not found: %s\n", i.root) + return nil + } + + if err := os.RemoveAll(i.root); err != nil { + log.Printf("❌ Failed to remove BetterDiscord folder: %s\n", i.root) + log.Printf(" %s\n", err.Error()) + return err + } + + log.Printf("✅ Removed BetterDiscord folder: %s\n", i.root) + return nil +} + +var lock = &sync.Mutex{} +var globalInstance *BDInstall + +func GetInstallation(base ...string) *BDInstall { + if len(base) == 0 { + if globalInstance != nil { + return globalInstance + } + + lock.Lock() + defer lock.Unlock() + if globalInstance != nil { + return globalInstance + } + + // Default to user config directory + configDir, _ := os.UserConfigDir() + + // Handle WSL with Windows home directory + if wsl.IsWSL() { + winHome, err := wsl.WindowsHome() + if err == nil && winHome != "" { + configDir = filepath.Join(winHome, "AppData", "Roaming") + } + } + + globalInstance = GetInstallation(configDir) + + return globalInstance + } + + return New(filepath.Join(base[0], "BetterDiscord")) +} + +func New(root string) *BDInstall { + return &BDInstall{ + root: root, + data: filepath.Join(root, "data"), + asar: filepath.Join(root, "data", "betterdiscord.asar"), + plugins: filepath.Join(root, "plugins"), + themes: filepath.Join(root, "themes"), + hasDownloaded: false, + } +} diff --git a/betterdiscord/install_test.go b/betterdiscord/install_test.go new file mode 100644 index 00000000..ae2a5371 --- /dev/null +++ b/betterdiscord/install_test.go @@ -0,0 +1,431 @@ +package betterdiscord + +import ( + "installer/types" + "os" + "path/filepath" + "testing" +) + +func TestNew(t *testing.T) { + rootPath := "/test/root/BetterDiscord" + install := New(rootPath) + + if install.Root() != rootPath { + t.Errorf("Root() = %s, expected %s", install.Root(), rootPath) + } + + expectedData := filepath.Join(rootPath, "data") + if install.Data() != expectedData { + t.Errorf("Data() = %s, expected %s", install.Data(), expectedData) + } + + expectedAsar := filepath.Join(rootPath, "data", "betterdiscord.asar") + if install.Asar() != expectedAsar { + t.Errorf("Asar() = %s, expected %s", install.Asar(), expectedAsar) + } + + expectedPlugins := filepath.Join(rootPath, "plugins") + if install.Plugins() != expectedPlugins { + t.Errorf("Plugins() = %s, expected %s", install.Plugins(), expectedPlugins) + } + + expectedThemes := filepath.Join(rootPath, "themes") + if install.Themes() != expectedThemes { + t.Errorf("Themes() = %s, expected %s", install.Themes(), expectedThemes) + } + + if install.HasDownloaded() { + t.Error("HasDownloaded() should be false for new install") + } +} + +func TestBDInstall_GettersSetters(t *testing.T) { + rootPath := "/test/path" + install := New(rootPath) + + // Test all getters return expected paths + tests := []struct { + name string + getter func() string + expected string + }{ + {"Root", install.Root, rootPath}, + {"Data", install.Data, filepath.Join(rootPath, "data")}, + {"Asar", install.Asar, filepath.Join(rootPath, "data", "betterdiscord.asar")}, + {"Plugins", install.Plugins, filepath.Join(rootPath, "plugins")}, + {"Themes", install.Themes, filepath.Join(rootPath, "themes")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.getter() + if result != tt.expected { + t.Errorf("%s() = %s, expected %s", tt.name, result, tt.expected) + } + }) + } +} + +func TestBDInstall_Prepare(t *testing.T) { + // Create temporary directory for testing + tmpDir := t.TempDir() + bdRoot := filepath.Join(tmpDir, "BetterDiscord") + + install := New(bdRoot) + + // Prepare should create all necessary directories + err := install.Prepare() + if err != nil { + t.Fatalf("Prepare() failed: %v", err) + } + + // Verify directories were created + dirs := []string{ + install.Data(), + install.Plugins(), + install.Themes(), + } + + for _, dir := range dirs { + if _, err := os.Stat(dir); os.IsNotExist(err) { + t.Errorf("Directory not created: %s", dir) + } + } +} + +func TestBDInstall_Prepare_AlreadyExists(t *testing.T) { + // Create temporary directory with existing structure + tmpDir := t.TempDir() + bdRoot := filepath.Join(tmpDir, "BetterDiscord") + + install := New(bdRoot) + + // Create directories manually first + os.MkdirAll(install.Data(), 0755) //nolint:errcheck + os.MkdirAll(install.Plugins(), 0755) //nolint:errcheck + os.MkdirAll(install.Themes(), 0755) //nolint:errcheck + + // Prepare should succeed even if directories already exist + err := install.Prepare() + if err != nil { + t.Fatalf("Prepare() failed when directories already exist: %v", err) + } + + // Verify directories still exist + dirs := []string{ + install.Data(), + install.Plugins(), + install.Themes(), + } + + for _, dir := range dirs { + if _, err := os.Stat(dir); os.IsNotExist(err) { + t.Errorf("Directory should exist: %s", dir) + } + } +} + +func TestBDInstall_Repair(t *testing.T) { + // Create temporary directory for testing + tmpDir := t.TempDir() + bdRoot := filepath.Join(tmpDir, "BetterDiscord") + + install := New(bdRoot) + + // Create the data directory and a test plugins.json file + channelFolder := filepath.Join(install.Data(), types.Stable.String()) + os.MkdirAll(channelFolder, 0755) //nolint:errcheck + + pluginsJson := filepath.Join(channelFolder, "plugins.json") + err := os.WriteFile(pluginsJson, []byte(`{"test": "data"}`), 0644) + if err != nil { + t.Fatalf("Failed to create test plugins.json: %v", err) + } + + // Verify file exists before repair + if _, err := os.Stat(pluginsJson); os.IsNotExist(err) { + t.Fatal("plugins.json should exist before repair") + } + + // Run repair + err = install.Repair(types.Stable, types.RepairOptions{DisablePlugins: true}) + if err != nil { + t.Fatalf("Repair() failed: %v", err) + } + + // Verify file was removed + if _, err := os.Stat(pluginsJson); !os.IsNotExist(err) { + t.Error("plugins.json should be removed after repair") + } +} + +func TestBDInstall_Repair_NoPluginsFile(t *testing.T) { + // Create temporary directory for testing + tmpDir := t.TempDir() + bdRoot := filepath.Join(tmpDir, "BetterDiscord") + + install := New(bdRoot) + + // Don't create any files - repair should succeed without error + err := install.Repair(types.Stable, types.RepairOptions{DisablePlugins: true}) + if err != nil { + t.Fatalf("Repair() should succeed when plugins.json doesn't exist: %v", err) + } +} + +func TestBDInstall_Repair_MultipleChannels(t *testing.T) { + // Create temporary directory for testing + tmpDir := t.TempDir() + bdRoot := filepath.Join(tmpDir, "BetterDiscord") + + install := New(bdRoot) + + // Create plugins.json for multiple channels + channels := []types.DiscordChannel{types.Stable, types.Canary, types.PTB} + pluginsFiles := make(map[types.DiscordChannel]string) + + for _, channel := range channels { + channelFolder := filepath.Join(install.Data(), channel.String()) + os.MkdirAll(channelFolder, 0755) //nolint:errcheck + + pluginsJson := filepath.Join(channelFolder, "plugins.json") + os.WriteFile(pluginsJson, []byte(`{}`), 0644) //nolint:errcheck + pluginsFiles[channel] = pluginsJson + } + + // Repair only Stable channel + err := install.Repair(types.Stable, types.RepairOptions{DisablePlugins: true}) + if err != nil { + t.Fatalf("Repair(Stable) failed: %v", err) + } + + // Verify only Stable's plugins.json was removed + if _, err := os.Stat(pluginsFiles[types.Stable]); !os.IsNotExist(err) { + t.Error("Stable plugins.json should be removed") + } + + // Verify other channels' files still exist + if _, err := os.Stat(pluginsFiles[types.Canary]); os.IsNotExist(err) { + t.Error("Canary plugins.json should still exist") + } + if _, err := os.Stat(pluginsFiles[types.PTB]); os.IsNotExist(err) { + t.Error("PTB plugins.json should still exist") + } +} + +func TestBDInstall_RepairWithOptions_AllToggles(t *testing.T) { + tmpDir := t.TempDir() + bdRoot := filepath.Join(tmpDir, "BetterDiscord") + + install := New(bdRoot) + channel := types.Stable + channelFolder := filepath.Join(install.Data(), channel.String()) + if err := os.MkdirAll(channelFolder, 0755); err != nil { + t.Fatalf("Failed to create channel folder: %v", err) + } + + paths := []string{ + filepath.Join(channelFolder, "plugins.json"), + filepath.Join(channelFolder, "themes.json"), + filepath.Join(channelFolder, "custom.css"), + filepath.Join(channelFolder, "settings.json"), + filepath.Join(channelFolder, "webpack.json"), + filepath.Join(channelFolder, "addon-store.json"), + } + for _, path := range paths { + if err := os.WriteFile(path, []byte("{}"), 0644); err != nil { + t.Fatalf("Failed to create file %s: %v", path, err) + } + } + + options := types.RepairOptions{ + DisablePlugins: true, + DisableThemes: true, + ClearCustomCSS: true, + ClearWebpackCache: true, + ClearAddonStoreCache: true, + ResetSettings: true, + } + + if err := install.Repair(channel, options); err != nil { + t.Fatalf("RepairWithOptions() failed: %v", err) + } + + for _, path := range paths { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("Expected %s to be removed", path) + } + } + + backupRoot := filepath.Join(channelFolder, "backup") + entries, err := os.ReadDir(backupRoot) + if err != nil { + t.Fatalf("Expected backup folder to exist: %v", err) + } + if len(entries) == 0 { + t.Fatalf("Expected at least one backup folder") + } + backupDir := filepath.Join(backupRoot, entries[0].Name()) + if _, err := os.Stat(filepath.Join(backupDir, "custom.css")); err != nil { + t.Fatalf("Expected custom.css to be backed up: %v", err) + } + if _, err := os.Stat(filepath.Join(backupDir, "settings.json")); err != nil { + t.Fatalf("Expected settings.json to be backed up: %v", err) + } + if _, err := os.Stat(filepath.Join(backupDir, "plugins.json")); err != nil { + t.Fatalf("Expected plugins.json to be backed up: %v", err) + } + if _, err := os.Stat(filepath.Join(backupDir, "themes.json")); err != nil { + t.Fatalf("Expected themes.json to be backed up: %v", err) + } + if _, err := os.Stat(filepath.Join(backupDir, "webpack.json")); err != nil { + t.Fatalf("Expected webpack.json to be backed up: %v", err) + } + if _, err := os.Stat(filepath.Join(backupDir, "addon-store.json")); err != nil { + t.Fatalf("Expected addon-store.json to be backed up: %v", err) + } +} + +func TestBDInstall_RepairWithOptions_PartialToggles(t *testing.T) { + tmpDir := t.TempDir() + bdRoot := filepath.Join(tmpDir, "BetterDiscord") + + install := New(bdRoot) + channel := types.Stable + channelFolder := filepath.Join(install.Data(), channel.String()) + if err := os.MkdirAll(channelFolder, 0755); err != nil { + t.Fatalf("Failed to create channel folder: %v", err) + } + + pluginsJson := filepath.Join(channelFolder, "plugins.json") + themesJson := filepath.Join(channelFolder, "themes.json") + if err := os.WriteFile(pluginsJson, []byte("{}"), 0644); err != nil { + t.Fatalf("Failed to create plugins.json: %v", err) + } + if err := os.WriteFile(themesJson, []byte("{}"), 0644); err != nil { + t.Fatalf("Failed to create themes.json: %v", err) + } + + options := types.RepairOptions{ + DisablePlugins: true, + DisableThemes: false, + } + + if err := install.Repair(channel, options); err != nil { + t.Fatalf("RepairWithOptions() failed: %v", err) + } + + if _, err := os.Stat(pluginsJson); !os.IsNotExist(err) { + t.Errorf("Expected plugins.json to be removed") + } + if _, err := os.Stat(themesJson); os.IsNotExist(err) { + t.Errorf("Expected themes.json to remain") + } +} + +func TestGetInstallation_WithBase(t *testing.T) { + basePath := "/test/config" + install := GetInstallation(basePath) + + expectedRoot := filepath.Join(basePath, "BetterDiscord") + if install.Root() != expectedRoot { + t.Errorf("GetInstallation(%s).Root() = %s, expected %s", basePath, install.Root(), expectedRoot) + } +} + +func TestGetInstallation_Singleton(t *testing.T) { + // Reset the global instance to ensure clean test + // Note: In a real test, you might want to add a reset function + // For now, we'll just test that multiple calls work + + install1 := GetInstallation() + install2 := GetInstallation() + + // Both should return the same instance (singleton pattern) + if install1 != install2 { + t.Error("GetInstallation() should return the same instance (singleton)") + } + + // Both should have the same root path + if install1.Root() != install2.Root() { + t.Errorf("Singleton instances have different roots: %s vs %s", install1.Root(), install2.Root()) + } +} + +func TestMakeDirectory(t *testing.T) { + tmpDir := t.TempDir() + testDir := filepath.Join(tmpDir, "test", "nested", "directory") + + // Make the directory + err := makeDirectory(testDir) + if err != nil { + t.Fatalf("makeDirectory() failed: %v", err) + } + + // Verify it exists + if _, err := os.Stat(testDir); os.IsNotExist(err) { + t.Error("Directory was not created") + } + + // Test making the same directory again (should not error) + err = makeDirectory(testDir) + if err != nil { + t.Errorf("makeDirectory() should succeed when directory already exists: %v", err) + } +} + +func TestBDInstall_HasDownloaded(t *testing.T) { + install := New("/test/path") + + // Initially should be false + if install.HasDownloaded() { + t.Error("HasDownloaded() should initially be false") + } + + // After setting hasDownloaded (internal state) + install.hasDownloaded = true + if !install.HasDownloaded() { + t.Error("HasDownloaded() should be true after download") + } +} + +func TestBDInstall_PathStructure(t *testing.T) { + // Test with different root paths + tests := []struct { + name string + rootPath string + }{ + {"Unix absolute path", "/home/user/.config/BetterDiscord"}, + {"Windows absolute path", "C:\\Users\\User\\AppData\\Roaming\\BetterDiscord"}, + {"Relative path", "BetterDiscord"}, + {"Path with spaces", "/path with spaces/BetterDiscord"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + install := New(tt.rootPath) + + // Verify all paths are constructed correctly relative to root + if install.Root() != tt.rootPath { + t.Errorf("Root() incorrect") + } + + if install.Data() != filepath.Join(tt.rootPath, "data") { + t.Errorf("Data() path incorrect: %s", install.Data()) + } + + if install.Asar() != filepath.Join(tt.rootPath, "data", "betterdiscord.asar") { + t.Errorf("Asar() path incorrect: %s", install.Asar()) + } + + if install.Plugins() != filepath.Join(tt.rootPath, "plugins") { + t.Errorf("Plugins() path incorrect: %s", install.Plugins()) + } + + if install.Themes() != filepath.Join(tt.rootPath, "themes") { + t.Errorf("Themes() path incorrect: %s", install.Themes()) + } + }) + } +} diff --git a/betterdiscord/setup.go b/betterdiscord/setup.go new file mode 100644 index 00000000..d3764b18 --- /dev/null +++ b/betterdiscord/setup.go @@ -0,0 +1,169 @@ +package betterdiscord + +import ( + "installer/types" + "installer/utils" + "log" + "os" + "path/filepath" + "time" +) + +func makeDirectory(folder string) error { + exists := utils.Exists(folder) + + if exists { + log.Printf("✅ Directory exists: %s\n", folder) + return nil + } + + if err := os.MkdirAll(folder, 0755); err != nil { + log.Printf("❌ Failed to create directory: %s\n", folder) + log.Printf(" %s\n", err.Error()) + return err + } + + log.Printf("✅ Directory created: %s\n", folder) + return nil +} + +func (i *BDInstall) prepare() error { + if err := makeDirectory(i.data); err != nil { + return err + } + if err := makeDirectory(i.plugins); err != nil { + return err + } + if err := makeDirectory(i.themes); err != nil { + return err + } + return nil +} + +func (i *BDInstall) repair(channel types.DiscordChannel, options types.RepairOptions) error { + channelFolder := filepath.Join(i.data, channel.String()) + backupRoot := filepath.Join(channelFolder, "backup") + backupStamp := time.Now().Format("20060102-150405") + backupDir := filepath.Join(backupRoot, "repair-"+backupStamp) + + if options.DisablePlugins { + if err := backupAndRemoveFile(channelFolder, "plugins.json", "plugins", channel.Name(), backupDir, true); err != nil { + return err + } + } + + if options.DisableThemes { + if err := backupAndRemoveFile(channelFolder, "themes.json", "themes", channel.Name(), backupDir, true); err != nil { + return err + } + } + + if options.ClearCustomCSS { + if err := backupAndRemoveFile(channelFolder, "custom.css", "custom CSS", channel.Name(), backupDir, false); err != nil { + return err + } + } + + if options.ClearWebpackCache { + if err := backupAndRemoveFile(channelFolder, "webpack.json", "webpack cache", channel.Name(), backupDir, true); err != nil { + return err + } + } + + if options.ClearAddonStoreCache { + if err := backupAndRemoveFile(channelFolder, "addon-store.json", "addon store cache", channel.Name(), backupDir, true); err != nil { + return err + } + } + + if options.ResetSettings { + if err := backupAndRemoveFile(channelFolder, "settings.json", "settings", channel.Name(), backupDir, false); err != nil { + return err + } + } + + return nil +} + +func removeFile(basePath string, filename string, label string, channelName string) error { + path := filepath.Join(basePath, filename) + if !utils.Exists(path) { + log.Printf("✅ No %s found for %s\n", label, channelName) + return nil + } + + if err := os.Remove(path); err != nil { + log.Printf("❌ Unable to remove %s: %s\n", label, path) + log.Printf(" %s\n", err.Error()) + return err + } + + log.Printf("✅ Removed %s for %s\n", label, channelName) + return nil +} + +func backupAndRemoveFile(basePath string, filename string, label string, channelName string, backupDir string, allowDeleteOnBackupFailure bool) error { + path := filepath.Join(basePath, filename) + if !utils.Exists(path) { + log.Printf("✅ No %s found for %s\n", label, channelName) + return nil + } + + backupErr := backupFile(path, backupDir, label, channelName) + if backupErr != nil && !allowDeleteOnBackupFailure { + return backupErr + } + + if backupErr != nil { + log.Printf("⚠️ Proceeding without backup for %s\n", label) + } + + if err := os.Remove(path); err != nil { + log.Printf("❌ Unable to remove %s: %s\n", label, path) + log.Printf(" %s\n", err.Error()) + return err + } + log.Printf("✅ Removed %s for %s\n", label, channelName) + return nil +} + +func backupFile(path string, backupDir string, label string, channelName string) error { + if err := os.MkdirAll(backupDir, 0755); err != nil { + log.Printf("❌ Unable to create backup folder: %s\n", backupDir) + log.Printf(" %s\n", err.Error()) + return err + } + + contents, err := os.ReadFile(path) + if err != nil { + log.Printf("❌ Unable to read %s: %s\n", label, path) + log.Printf(" %s\n", err.Error()) + return err + } + + backupPath := filepath.Join(backupDir, filepath.Base(path)) + if err := os.WriteFile(backupPath, contents, 0644); err != nil { + log.Printf("❌ Unable to backup %s to %s\n", label, backupPath) + log.Printf(" %s\n", err.Error()) + return err + } + log.Printf("✅ Backed up %s for %s\n", label, channelName) + return nil +} + +func removeDir(basePath string, dirname string, label string, channelName string) error { + path := filepath.Join(basePath, dirname) + if !utils.Exists(path) { + log.Printf("✅ No %s found for %s\n", label, channelName) + return nil + } + + if err := os.RemoveAll(path); err != nil { + log.Printf("❌ Unable to remove %s: %s\n", label, path) + log.Printf(" %s\n", err.Error()) + return err + } + + log.Printf("✅ Removed %s for %s\n", label, channelName) + return nil +} diff --git a/build/README.md b/build/README.md new file mode 100644 index 00000000..1ae2f677 --- /dev/null +++ b/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 00000000..f554e0ac Binary files /dev/null and b/build/appicon.png differ diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist new file mode 100644 index 00000000..04727c23 --- /dev/null +++ b/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.Name}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist new file mode 100644 index 00000000..19cc9370 --- /dev/null +++ b/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.Name}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/assets/icon.icns b/build/darwin/icon.icns similarity index 100% rename from assets/icon.icns rename to build/darwin/icon.icns diff --git a/assets/icon.ico b/build/windows/icon.ico similarity index 100% rename from assets/icon.ico rename to build/windows/icon.ico diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 00000000..9727946b --- /dev/null +++ b/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi new file mode 100644 index 00000000..654ae2e4 --- /dev/null +++ b/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh new file mode 100644 index 00000000..f9c0f885 --- /dev/null +++ b/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest new file mode 100644 index 00000000..17e1a238 --- /dev/null +++ b/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/discord/assets/injection.js b/discord/assets/injection.js new file mode 100644 index 00000000..3d176be5 --- /dev/null +++ b/discord/assets/injection.js @@ -0,0 +1,21 @@ +// BetterDiscord's Injection Script +const path = require("path"); +const electron = require("electron"); + +// Windows and macOS both use the fixed global BetterDiscord folder but +// Electron gives the postfixed version of userData, so go up a directory +let userConfig = path.join(electron.app.getPath("userData"), ".."); + +// If we're on Linux there are a couple cases to deal with +if (process.platform !== "win32" && process.platform !== "darwin") { + // Use || instead of ?? because a falsey value of "" is invalid per XDG spec + userConfig = process.env.XDG_CONFIG_HOME || path.join(process.env.HOME, ".config"); + + // HOST_XDG_CONFIG_HOME is set by flatpak, so use without validation if set + if (process.env.HOST_XDG_CONFIG_HOME) userConfig = process.env.HOST_XDG_CONFIG_HOME; +} + +require(path.join(userConfig, "BetterDiscord", "data", "betterdiscord.asar")); + +// Discord's Default Export +module.exports = require("./core.asar"); \ No newline at end of file diff --git a/discord/injection.go b/discord/injection.go new file mode 100644 index 00000000..1d7f83a4 --- /dev/null +++ b/discord/injection.go @@ -0,0 +1,68 @@ +package discord + +import ( + _ "embed" + "installer/betterdiscord" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +//go:embed assets/injection.js +var injectionScript string + +func (discord *DiscordInstall) inject(bd *betterdiscord.BDInstall) error { + if discord.IsFlatpak { + cmd := exec.Command("flatpak", "--user", "override", "com.discordapp."+discord.Channel.Exe(), "--filesystem="+bd.Root()) + if err := cmd.Run(); err != nil { + log.Printf("❌ Could not give flatpak access to %s\n", bd.Root()) + log.Printf(" %s\n", err.Error()) + return err + } + } + + if err := os.WriteFile(filepath.Join(discord.CorePath, "index.js"), []byte(injectionScript), 0755); err != nil { + log.Printf("❌ Unable to write index.js in %s\n", discord.CorePath) + log.Printf(" %s\n", err.Error()) + return err + } + + log.Printf("✅ Injected into %s\n", discord.CorePath) + return nil +} + +func (discord *DiscordInstall) uninject() error { + indexFile := filepath.Join(discord.CorePath, "index.js") + + contents, err := os.ReadFile(indexFile) + + // First try to check the file, but if there's an issue we try to blindly overwrite below + if err == nil { + if !strings.Contains(strings.ToLower(string(contents)), "betterdiscord") { + log.Printf("✅ No injection found for %s\n", discord.Channel.Name()) + return nil + } + } + + if err := os.WriteFile(indexFile, []byte(`module.exports = require("./core.asar");`), 0o644); err != nil { + log.Printf("❌ Unable to write file %s\n", indexFile) + log.Printf(" %s\n", err.Error()) + return err + } + log.Printf("✅ Removed from %s\n", discord.Channel.Name()) + + return nil +} + +// TODO: consider putting this in the betterdiscord package +func (discord *DiscordInstall) IsInjected() bool { + indexFile := filepath.Join(discord.CorePath, "index.js") + contents, err := os.ReadFile(indexFile) + if err != nil { + return false + } + lower := strings.ToLower(string(contents)) + return strings.Contains(lower, "betterdiscord") +} diff --git a/discord/injection_test.go b/discord/injection_test.go new file mode 100644 index 00000000..bf1a8002 --- /dev/null +++ b/discord/injection_test.go @@ -0,0 +1,40 @@ +package discord + +import ( + "installer/types" + "os" + "path/filepath" + "testing" +) + +func TestIsInjected(t *testing.T) { + tmpDir := t.TempDir() + corePath := filepath.Join(tmpDir, "discord_desktop_core") + if err := os.MkdirAll(corePath, 0755); err != nil { + t.Fatalf("Failed to create core path: %v", err) + } + indexFile := filepath.Join(corePath, "index.js") + + install := &DiscordInstall{ + CorePath: corePath, + Channel: types.Stable, + } + + if install.IsInjected() { + t.Fatalf("Expected IsInjected to be false with missing index.js") + } + + if err := os.WriteFile(indexFile, []byte(`module.exports = require("./core.asar");`), 0644); err != nil { + t.Fatalf("Failed to write index.js: %v", err) + } + if install.IsInjected() { + t.Fatalf("Expected IsInjected to be false for default index.js") + } + + if err := os.WriteFile(indexFile, []byte(`// BetterDiscord injected`), 0644); err != nil { + t.Fatalf("Failed to write injection index.js: %v", err) + } + if !install.IsInjected() { + t.Fatalf("Expected IsInjected to be true when BetterDiscord is present") + } +} diff --git a/discord/install.go b/discord/install.go new file mode 100644 index 00000000..d7f79c97 --- /dev/null +++ b/discord/install.go @@ -0,0 +1,116 @@ +package discord + +import ( + "installer/betterdiscord" + "installer/types" + "log" + "path/filepath" +) + +type DiscordInstall struct { + CorePath string `json:"corePath"` + Channel types.DiscordChannel `json:"channel"` + Version string `json:"version"` + IsFlatpak bool `json:"isFlatpak"` + IsSnap bool `json:"isSnap"` +} + +// InstallBD installs BetterDiscord into this Discord installation +func (discord *DiscordInstall) InstallBD(options types.InstallOptions) error { + bd := discord.GetBetterDiscordInstall() + + // Make BetterDiscord folders + log.Println("🛠 Preparing BetterDiscord...") + if err := bd.Prepare(); err != nil { + return err + } + log.Println("✅ BetterDiscord prepared for install") + log.Println("") + + // Download and write betterdiscord.asar + log.Println("📥 Downloading BetterDiscord...") + if err := bd.Download(); err != nil { + return err + } + log.Println("✅ BetterDiscord downloaded") + log.Println("") + + // Write injection script to discord_desktop_core/index.js + log.Println("🔌 Injecting into Discord...") + if err := discord.inject(bd); err != nil { + return err + } + log.Println("✅ Injection successful") + log.Println("") + + if options.RestartDiscord { + // Terminate and restart Discord if possible + log.Printf("🔄 Restarting %s...\n", discord.Channel.Name()) + if err := discord.restart(); err != nil { + return err + } + log.Println("") + } + + return nil +} + +// UninstallBD removes BetterDiscord from this Discord installation +func (discord *DiscordInstall) UninstallBD(options types.UninstallOptions) error { + log.Println("🧹 Removing injection...") + if err := discord.uninject(); err != nil { + return err + } + log.Println("") + + if options.FullUninstall { + install := discord.GetBetterDiscordInstall() + if err := install.RemoveAll(); err != nil { + return err + } + log.Println("") + } + + if options.RestartDiscord { + log.Printf("🔄 Restarting %s...\n", discord.Channel.Name()) + if err := discord.restart(); err != nil { + return err + } + log.Println("") + } + + return nil +} + +// RepairBD repairs BetterDiscord for this Discord installation +func (discord *DiscordInstall) RepairBD(options types.RepairOptions) error { + if err := discord.UninstallBD(types.UninstallOptions{FullUninstall: false}); err != nil { + return err + } + + // Gets the global BetterDiscord install + bd := betterdiscord.GetInstallation() + + // Snaps get their own local BD install + if discord.IsSnap { + bd = betterdiscord.GetInstallation(filepath.Clean(filepath.Join(discord.CorePath, "..", "..", "..", ".."))) + } + + if err := bd.Repair(discord.Channel, options); err != nil { + return err + } + + return nil +} + +func (discord *DiscordInstall) GetBetterDiscordInstall() *betterdiscord.BDInstall { + // Gets the global BetterDiscord install + bd := betterdiscord.GetInstallation() + + // Snaps get their own local BD install + if discord.IsSnap { + bd = betterdiscord.GetInstallation(filepath.Clean(filepath.Join(discord.CorePath, "..", "..", "..", ".."))) + } + + return bd +} diff --git a/discord/paths.go b/discord/paths.go new file mode 100644 index 00000000..4c7ee997 --- /dev/null +++ b/discord/paths.go @@ -0,0 +1,99 @@ +package discord + +import ( + "installer/types" + "path/filepath" + "regexp" + "slices" + "strings" +) + +var searchPaths []string +var versionRegex = regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+`) +var allDiscordInstalls map[types.DiscordChannel][]*DiscordInstall + +func GetAllInstalls() map[types.DiscordChannel][]*DiscordInstall { + allDiscordInstalls = map[types.DiscordChannel][]*DiscordInstall{} + + for _, path := range searchPaths { + if result := Validate(path); result != nil { + allDiscordInstalls[result.Channel] = append(allDiscordInstalls[result.Channel], result) + } + } + + sortInstalls() + + return allDiscordInstalls +} + +func GetVersion(proposed string) string { + for folder := range strings.SplitSeq(proposed, string(filepath.Separator)) { + if version := versionRegex.FindString(folder); version != "" { + return version + } + } + return "" +} + +func GetChannel(proposed string) types.DiscordChannel { + for folder := range strings.SplitSeq(proposed, string(filepath.Separator)) { + for _, channel := range types.Channels { + if strings.ToLower(folder) == strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "") { + return channel + } + } + } + return types.Stable +} + +func GetSuggestedPath(channel types.DiscordChannel) string { + if len(allDiscordInstalls[channel]) > 0 { + return allDiscordInstalls[channel][0].CorePath + } + return "" +} + +func AddCustomPath(proposed string) *DiscordInstall { + result := Validate(proposed) + if result == nil { + return nil + } + + // Check if this already exists in our list and return reference + index := slices.IndexFunc(allDiscordInstalls[result.Channel], func(d *DiscordInstall) bool { return d.CorePath == result.CorePath }) + if index >= 0 { + return allDiscordInstalls[result.Channel][index] + } + + allDiscordInstalls[result.Channel] = append(allDiscordInstalls[result.Channel], result) + + sortInstalls() + + return result +} + +func ResolvePath(proposed string) *DiscordInstall { + for channel := range allDiscordInstalls { + index := slices.IndexFunc(allDiscordInstalls[channel], func(d *DiscordInstall) bool { return d.CorePath == proposed }) + if index >= 0 { + return allDiscordInstalls[channel][index] + } + } + + // If it wasn't found as an existing install, try to add it + return AddCustomPath(proposed) +} + +func sortInstalls() { + for channel := range allDiscordInstalls { + slices.SortFunc(allDiscordInstalls[channel], func(a, b *DiscordInstall) int { + switch { + case a.Version > b.Version: + return -1 + case b.Version > a.Version: + return 1 + } + return 0 + }) + } +} diff --git a/discord/paths_common.go b/discord/paths_common.go new file mode 100644 index 00000000..b2763cc4 --- /dev/null +++ b/discord/paths_common.go @@ -0,0 +1,147 @@ +//go:build darwin || linux || windows + +package discord + +import ( + "installer/utils" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" +) + +// validateWindowsStyleInstall validates a Windows-style Discord installation path. +// This is used for native Windows installs and WSL installs that point to Windows Discord. +// Windows Discord has a nested structure: Discord/app-1.0.9002/modules/discord_desktop_core-1/discord_desktop_core +func validateWindowsStyleInstall(proposed string) *DiscordInstall { + var finalPath = "" + var selected = filepath.Base(proposed) + + if strings.HasPrefix(selected, "Discord") { + // Get version dir like app-1.0.9002 + dFiles, err := os.ReadDir(proposed) + if err != nil { + return nil + } + + candidates := utils.Filter(dFiles, func(file fs.DirEntry) bool { + return file.IsDir() && versionRegex.MatchString(file.Name()) + }) + if len(candidates) == 0 { + return nil + } + sort.Slice(candidates, func(i, j int) bool { return candidates[i].Name() < candidates[j].Name() }) + versionDir := candidates[len(candidates)-1].Name() + + // Get core wrap like discord_desktop_core-1 + dFiles, err = os.ReadDir(filepath.Join(proposed, versionDir, "modules")) + if err != nil { + return nil + } + candidates = utils.Filter(dFiles, func(file fs.DirEntry) bool { + return file.IsDir() && strings.HasPrefix(file.Name(), "discord_desktop_core") + }) + if len(candidates) == 0 { + return nil + } + coreWrap := candidates[len(candidates)-1].Name() + + finalPath = filepath.Join(proposed, versionDir, "modules", coreWrap, "discord_desktop_core") + } + + // Handle app-* directories (e.g., app-1.0.9002) + if strings.HasPrefix(selected, "app-") { + dFiles, err := os.ReadDir(filepath.Join(proposed, "modules")) + if err != nil { + return nil + } + + candidates := utils.Filter(dFiles, func(file fs.DirEntry) bool { + return file.IsDir() && strings.HasPrefix(file.Name(), "discord_desktop_core") + }) + if len(candidates) == 0 { + return nil + } + coreWrap := candidates[len(candidates)-1].Name() + finalPath = filepath.Join(proposed, "modules", coreWrap, "discord_desktop_core") + } + + if selected == "discord_desktop_core" { + finalPath = proposed + } + + // Verify the path and core.asar exist + if utils.Exists(finalPath) && utils.Exists(filepath.Join(finalPath, "core.asar")) { + return &DiscordInstall{ + CorePath: finalPath, + Channel: GetChannel(finalPath), + Version: GetVersion(finalPath), + IsFlatpak: false, + IsSnap: false, + } + } + + return nil +} + +// validateUnixStyleInstall validates a Unix-style Discord installation path (Linux native, macOS). +// Unix Discord has a flatter structure: discord/0.0.35/modules/discord_desktop_core +func validateUnixStyleInstall(proposed string, detectFlatpak bool, detectSnap bool) *DiscordInstall { + var finalPath = "" + var selected = filepath.Base(proposed) + + if strings.HasPrefix(strings.ToLower(selected), "discord") { + // Get version dir like 0.0.35 + dFiles, err := os.ReadDir(proposed) + if err != nil { + return nil + } + + candidates := utils.Filter(dFiles, func(file fs.DirEntry) bool { + return file.IsDir() && versionRegex.MatchString(file.Name()) + }) + if len(candidates) == 0 { + return nil + } + sort.Slice(candidates, func(i, j int) bool { return candidates[i].Name() < candidates[j].Name() }) + versionDir := candidates[len(candidates)-1].Name() + finalPath = filepath.Join(proposed, versionDir, "modules", "discord_desktop_core") + } + + // Handle version directories (e.g., 0.0.35) + if len(strings.Split(selected, ".")) == 3 { + finalPath = filepath.Join(proposed, "modules", "discord_desktop_core") + } + + if selected == "modules" { + finalPath = filepath.Join(proposed, "discord_desktop_core") + } + + if selected == "discord_desktop_core" { + finalPath = proposed + } + + // Verify the path and core.asar exist + if utils.Exists(finalPath) && utils.Exists(filepath.Join(finalPath, "core.asar")) { + isFlatpak := false + isSnap := false + + if detectFlatpak { + isFlatpak = strings.Contains(finalPath, "com.discordapp.") + } + if detectSnap { + isSnap = strings.Contains(finalPath, "snap/") + } + + return &DiscordInstall{ + CorePath: finalPath, + Channel: GetChannel(finalPath), + Version: GetVersion(finalPath), + IsFlatpak: isFlatpak, + IsSnap: isSnap, + } + } + + return nil +} diff --git a/discord/paths_common_test.go b/discord/paths_common_test.go new file mode 100644 index 00000000..f0bc2cfa --- /dev/null +++ b/discord/paths_common_test.go @@ -0,0 +1,167 @@ +package discord + +import ( + "os" + "path/filepath" + "testing" +) + +func writeCoreAsar(t *testing.T, corePath string) { + t.Helper() + if err := os.MkdirAll(corePath, 0755); err != nil { + t.Fatalf("Failed to create core path: %v", err) + } + if err := os.WriteFile(filepath.Join(corePath, "core.asar"), []byte("test"), 0644); err != nil { + t.Fatalf("Failed to write core.asar: %v", err) + } +} + +func TestValidateWindowsStyleInstall_FromDiscordRoot(t *testing.T) { + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "Discord") + versionDir := filepath.Join(root, "app-1.0.9002") + coreWrap := filepath.Join(versionDir, "modules", "discord_desktop_core-1", "discord_desktop_core") + + writeCoreAsar(t, coreWrap) + + result := validateWindowsStyleInstall(root) + if result == nil { + t.Fatalf("Expected install for %s", root) + } + if result.CorePath != coreWrap { + t.Errorf("CorePath = %s, expected %s", result.CorePath, coreWrap) + } +} + +func TestValidateWindowsStyleInstall_FromAppFolder(t *testing.T) { + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "Discord") + versionDir := filepath.Join(root, "app-1.0.9002") + coreWrap := filepath.Join(versionDir, "modules", "discord_desktop_core-1", "discord_desktop_core") + + writeCoreAsar(t, coreWrap) + + result := validateWindowsStyleInstall(versionDir) + if result == nil { + t.Fatalf("Expected install for %s", versionDir) + } + if result.CorePath != coreWrap { + t.Errorf("CorePath = %s, expected %s", result.CorePath, coreWrap) + } +} + +func TestValidateWindowsStyleInstall_FromCoreFolder(t *testing.T) { + tmpDir := t.TempDir() + corePath := filepath.Join(tmpDir, "discord_desktop_core") + writeCoreAsar(t, corePath) + + result := validateWindowsStyleInstall(corePath) + if result == nil { + t.Fatalf("Expected install for %s", corePath) + } + if result.CorePath != corePath { + t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath) + } +} + +func TestValidateWindowsStyleInstall_MissingAsar(t *testing.T) { + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "Discord") + versionDir := filepath.Join(root, "app-1.0.9002") + coreWrap := filepath.Join(versionDir, "modules", "discord_desktop_core-1", "discord_desktop_core") + + if err := os.MkdirAll(coreWrap, 0755); err != nil { + t.Fatalf("Failed to create core path: %v", err) + } + + result := validateWindowsStyleInstall(root) + if result != nil { + t.Fatalf("Expected no install when core.asar is missing") + } +} + +func TestValidateUnixStyleInstall_FromDiscordRoot(t *testing.T) { + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "discord") + corePath := filepath.Join(root, "0.0.35", "modules", "discord_desktop_core") + + writeCoreAsar(t, corePath) + + result := validateUnixStyleInstall(root, true, true) + if result == nil { + t.Fatalf("Expected install for %s", root) + } + if result.CorePath != corePath { + t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath) + } +} + +func TestValidateUnixStyleInstall_FromVersionFolder(t *testing.T) { + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "discord") + versionDir := filepath.Join(root, "0.0.35") + corePath := filepath.Join(versionDir, "modules", "discord_desktop_core") + + writeCoreAsar(t, corePath) + + result := validateUnixStyleInstall(versionDir, true, true) + if result == nil { + t.Fatalf("Expected install for %s", versionDir) + } + if result.CorePath != corePath { + t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath) + } +} + +func TestValidateUnixStyleInstall_FromModulesFolder(t *testing.T) { + tmpDir := t.TempDir() + corePath := filepath.Join(tmpDir, "modules", "discord_desktop_core") + + writeCoreAsar(t, corePath) + + result := validateUnixStyleInstall(filepath.Join(tmpDir, "modules"), true, true) + if result == nil { + t.Fatalf("Expected install for %s", filepath.Join(tmpDir, "modules")) + } + if result.CorePath != corePath { + t.Errorf("CorePath = %s, expected %s", result.CorePath, corePath) + } +} + +func TestValidateUnixStyleInstall_FlatpakDetection(t *testing.T) { + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "com.discordapp.Discord", "config", "discord") + corePath := filepath.Join(root, "0.0.35", "modules", "discord_desktop_core") + + writeCoreAsar(t, corePath) + + result := validateUnixStyleInstall(root, true, false) + if result == nil { + t.Fatalf("Expected install for %s", root) + } + if !result.IsFlatpak { + t.Fatalf("Expected flatpak detection") + } + if result.IsSnap { + t.Fatalf("Did not expect snap detection") + } +} + +func TestValidateUnixStyleInstall_SnapDetection(t *testing.T) { + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "snap", "discord", "current", ".config", "discord") + corePath := filepath.Join(root, "0.0.35", "modules", "discord_desktop_core") + + writeCoreAsar(t, corePath) + + result := validateUnixStyleInstall(root, false, true) + if result == nil { + t.Fatalf("Expected install for %s", root) + } + if !result.IsSnap { + t.Fatalf("Expected snap detection") + } + if result.IsFlatpak { + t.Fatalf("Did not expect flatpak detection") + } +} diff --git a/discord/paths_darwin.go b/discord/paths_darwin.go new file mode 100644 index 00000000..7954a356 --- /dev/null +++ b/discord/paths_darwin.go @@ -0,0 +1,31 @@ +package discord + +import ( + "installer/types" + "os" + "path/filepath" + "strings" +) + +func init() { + config, _ := os.UserConfigDir() + paths := []string{ + filepath.Join(config, "{channel}"), + } + + for _, channel := range types.Channels { + for _, path := range paths { + folder := strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "") + searchPaths = append( + searchPaths, + strings.ReplaceAll(path, "{channel}", folder), + ) + } + } + + allDiscordInstalls = GetAllInstalls() +} + +func Validate(proposed string) *DiscordInstall { + return validateUnixStyleInstall(proposed, false, false) +} diff --git a/discord/paths_linux.go b/discord/paths_linux.go new file mode 100644 index 00000000..c606c125 --- /dev/null +++ b/discord/paths_linux.go @@ -0,0 +1,68 @@ +package discord + +import ( + "installer/types" + "installer/wsl" + "os" + "path/filepath" + "strings" +) + +func init() { + config, _ := os.UserConfigDir() + home, _ := os.UserHomeDir() + paths := []string{ + // Native. Data is stored under `~/.config`. + // Example: `~/.config/discordcanary`. + // Core: `~/.config/discordcanary/0.0.90/modules/discord_desktop_core/core.asar`. + filepath.Join(config, "{channel}"), + + // Flatpak. These user data paths are universal for all Flatpak installations on all machines. + // Example: `.var/app/com.discordapp.DiscordCanary/config/discordcanary`. + // Core: `.var/app/com.discordapp.DiscordCanary/config/discordcanary/0.0.90/modules/discord_desktop_core/core.asar` + filepath.Join(home, ".var", "app", "com.discordapp.{CHANNEL}", "config", "{channel}"), + + // Snap. Just like with Flatpaks, these paths are universal for all Snap installations. + // Example: `snap/discord/current/.config/discord`. + // Example: `snap/discord-canary/current/.config/discordcanary`. + // Core: `snap/discord-canary/current/.config/discordcanary/0.0.90/modules/discord_desktop_core/core.asar`. + // NOTE: Snap user data always exists, even when the Snap isn't mounted/running. + filepath.Join(home, "snap", "{channel-}", "current", ".config", "{channel}"), + } + + if wsl.IsWSL() { + winHome, err := wsl.WindowsHome() + if err == nil && winHome != "" { + // WSL. Data is stored under the Windows user's AppData folder. + // Example: `/mnt/c/Users/Username/AppData/Local/DiscordCanary`. + // Core: `/mnt/c/Users/Username/AppData/Local/DiscordCanary/app-1.0.9218/modules/discord_desktop_core-1/discord_desktop_core core.asar`. + paths = append(paths, filepath.Join(winHome, "AppData", "Local", "{CHANNEL}")) + } + } + + for _, channel := range types.Channels { + for _, path := range paths { + upper := strings.ReplaceAll(channel.Name(), " ", "") + lower := strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "") + dash := strings.ReplaceAll(strings.ToLower(channel.Name()), " ", "-") + folder := strings.ReplaceAll(path, "{CHANNEL}", upper) + folder = strings.ReplaceAll(folder, "{channel}", lower) + folder = strings.ReplaceAll(folder, "{channel-}", dash) + searchPaths = append(searchPaths, folder) + } + } + + allDiscordInstalls = GetAllInstalls() +} + +// Validate validates a Discord installation path on Linux. +// For WSL environments, it uses Windows-style validation. +// For native Linux, it detects Flatpak and Snap installations. +func Validate(proposed string) *DiscordInstall { + if wsl.IsWSL() { + return validateWindowsStyleInstall(proposed) + } + + // Native Linux validation with Flatpak and Snap detection + return validateUnixStyleInstall(proposed, true, true) +} diff --git a/discord/paths_test.go b/discord/paths_test.go new file mode 100644 index 00000000..1ed26a38 --- /dev/null +++ b/discord/paths_test.go @@ -0,0 +1,83 @@ +package discord + +import ( + "installer/types" + "path/filepath" + "testing" +) + +func TestGetVersion(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "Finds version in Windows style path", + path: filepath.Join("C:", "Users", "Me", "AppData", "Local", "Discord", "app-1.0.9002", "modules", "discord_desktop_core-1", "discord_desktop_core"), + expected: "1.0.9002", + }, + { + name: "Finds version in Unix style path", + path: filepath.Join("/home/me/.config/discord", "0.0.35", "modules", "discord_desktop_core"), + expected: "0.0.35", + }, + { + name: "No version", + path: filepath.Join("/home/me/.config/discord", "modules", "discord_desktop_core"), + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetVersion(tt.path) + if result != tt.expected { + t.Errorf("GetVersion(%q) = %q, expected %q", tt.path, result, tt.expected) + } + }) + } +} + +func TestGetChannel(t *testing.T) { + tests := []struct { + name string + path string + expected types.DiscordChannel + }{ + { + name: "Stable path", + path: filepath.Join("/home/me/.config/discord", "0.0.35", "modules", "discord_desktop_core"), + expected: types.Stable, + }, + { + name: "Canary path", + path: filepath.Join("/home/me/.config/discordcanary", "0.0.90", "modules", "discord_desktop_core"), + expected: types.Canary, + }, + { + name: "PTB path", + path: filepath.Join("/home/me/.config/discordptb", "0.0.56", "modules", "discord_desktop_core"), + expected: types.PTB, + }, + { + name: "Unknown defaults to stable", + path: filepath.Join("/home/me/.config/discordunknown", "0.0.56", "modules", "discord_desktop_core"), + expected: types.Stable, + }, + { + name: "Windows path names", + path: filepath.Join("C:", "Users", "Me", "AppData", "Local", "DiscordCanary", "app-1.0.9002", "modules", "discord_desktop_core-1", "discord_desktop_core"), + expected: types.Canary, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetChannel(tt.path) + if result != tt.expected { + t.Errorf("GetChannel(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} diff --git a/discord/paths_windows.go b/discord/paths_windows.go new file mode 100644 index 00000000..12bd4b47 --- /dev/null +++ b/discord/paths_windows.go @@ -0,0 +1,30 @@ +package discord + +import ( + "installer/types" + "os" + "path/filepath" + "strings" +) + +func init() { + paths := []string{ + filepath.Join(os.Getenv("LOCALAPPDATA"), "{channel}"), + filepath.Join(os.Getenv("PROGRAMDATA"), os.Getenv("USERNAME"), "{channel}"), + } + + for _, channel := range types.Channels { + for _, path := range paths { + searchPaths = append( + searchPaths, + strings.ReplaceAll(path, "{channel}", strings.ReplaceAll(channel.Name(), " ", "")), + ) + } + } + + allDiscordInstalls = GetAllInstalls() +} + +func Validate(proposed string) *DiscordInstall { + return validateWindowsStyleInstall(proposed) +} diff --git a/discord/process.go b/discord/process.go new file mode 100644 index 00000000..e8e5a9cc --- /dev/null +++ b/discord/process.go @@ -0,0 +1,134 @@ +package discord + +import ( + "fmt" + "log" + "os" + "os/exec" + + "github.com/shirou/gopsutil/v3/process" +) + +func (discord *DiscordInstall) restart() error { + exeName := discord.getFullExe() + + if running, _ := discord.isRunning(); !running { + log.Printf("✅ %s is not running; skipping restart.\n", discord.Channel.Name()) + return nil + } + + if err := discord.kill(); err != nil { + log.Printf("❌ Unable to restart %s, please do so manually.\n", discord.Channel.Name()) + log.Printf(" %s\n", err.Error()) + return err + } + + // Determine command based on installation type + var cmd *exec.Cmd + if discord.IsFlatpak { + cmd = exec.Command("flatpak", "run", "com.discordapp."+discord.Channel.Exe()) + } else if discord.IsSnap { + cmd = exec.Command("snap", "run", discord.Channel.Exe()) + } else { + // Use binary found in killing process for non-Flatpak/Snap installs + if exeName == "" { + log.Printf("❌ Unable to restart %s, please do so manually.\n", discord.Channel.Name()) + return fmt.Errorf("could not determine executable path for %s", discord.Channel.Name()) + } + cmd = exec.Command(exeName) + } + + // Set working directory to user home + cmd.Dir, _ = os.UserHomeDir() + + if err := cmd.Start(); err != nil { + log.Printf("❌ Unable to restart %s, please do so manually.\n", discord.Channel.Name()) + log.Printf(" %s\n", err.Error()) + return err + } + log.Printf("✅ Restarted %s\n", discord.Channel.Name()) + return nil +} + +func (discord *DiscordInstall) isRunning() (bool, error) { + name := discord.Channel.Exe() + processes, err := process.Processes() + + // If we can't even list processes, bail out + if err != nil { + return false, fmt.Errorf("could not list processes") + } + + // Search for desired process(es) + for _, p := range processes { + n, err := p.Name() + + // Ignore processes requiring Admin/Sudo + if err != nil { + continue + } + + // We found our target return + if n == name { + return true, nil + } + } + + // If we got here, process was not found + return false, nil +} + +func (discord *DiscordInstall) kill() error { + name := discord.Channel.Exe() + processes, err := process.Processes() + + // If we can't even list processes, bail out + if err != nil { + return fmt.Errorf("could not list processes") + } + + // Search for desired process(es) + for _, p := range processes { + n, err := p.Name() + + // Ignore processes requiring Admin/Sudo + if err != nil { + continue + } + + // We found our target, kill it + if n == name { + var killErr = p.Kill() + + // We found it but can't kill it, bail out + if killErr != nil { + return killErr + } + } + } + + // If we got here, everything was killed without error + return nil +} + +func (discord *DiscordInstall) getFullExe() string { + name := discord.Channel.Exe() + + var exe = "" + processes, err := process.Processes() + if err != nil { + return exe + } + for _, p := range processes { + n, err := p.Name() + if err != nil { + continue + } + if n == name { + if len(exe) == 0 { + exe, _ = p.Exe() + } + } + } + return exe +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..a774edea --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build/* +!/build/.gitkeep + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/build/.gitkeep b/frontend/build/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 00000000..fc738bd9 --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,644 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "betterdiscord-installer", + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.52.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@testing-library/jest-dom": "^6.9.0", + "@testing-library/svelte": "^5.2.8", + "@testing-library/user-event": "^14.6.1", + "@types/bun": "^1.3.9", + "@zerebos/eslint-config": "^1.0.2", + "@zerebos/eslint-config-svelte": "^1.0.3", + "@zerebos/eslint-config-typescript": "^1.0.3", + "eslint": "^10.0.0", + "focus-visible": "^5.2.1", + "globals": "^17.3.0", + "jsdom": "^26.0.0", + "svelte": "^5.53.0", + "svelte-check": "^4.4.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.0", + "vite": "^7.3.1", + "vitest": "^3.2.4", + }, + }, + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.2", "", { "dependencies": { "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", "minimatch": "^10.2.1" } }, "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.2", "", { "dependencies": { "@eslint/core": "^1.1.0" } }, "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ=="], + + "@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.2", "", {}, "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.58.0", "", { "os": "android", "cpu": "arm" }, "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.58.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.58.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.58.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.58.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.58.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.58.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.58.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.58.0", "", { "os": "none", "cpu": "arm64" }, "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.58.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.58.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="], + + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.53.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Brh/9h8QEg7rWIj+Nnz/2sC49NUeS8g3Qd9H5dTO3EbWG8vCEUl06jE+r5jQVDMHdr1swmCkwZkONFsWelGTpQ=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/svelte": ["@testing-library/svelte@5.3.1", "", { "dependencies": { "@testing-library/dom": "9.x.x || 10.x.x", "@testing-library/svelte-core": "1.0.0" }, "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", "vite": "*", "vitest": "*" }, "optionalPeers": ["vite", "vitest"] }, "sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w=="], + + "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.0", "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0" } }, "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.0", "@typescript-eslint/tsconfig-utils": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "@zerebos/eslint-config": ["@zerebos/eslint-config@1.0.2", "", { "dependencies": { "@eslint/js": "^10.0.1", "globals": "^17.3.0" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-l+rXuWCnID1VKzbH5MEXsdHvZKc+nmsAU0p9cMW7lFgATWYUhBIFOKAAw4iXh7C+WwDwoyWHEKeUB1vs6iH+iQ=="], + + "@zerebos/eslint-config-svelte": ["@zerebos/eslint-config-svelte@1.0.3", "", { "dependencies": { "eslint-plugin-svelte": "^3.15.0", "typescript-eslint": "^8.56.0" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-Pf66UfeoZKcY/ECVp3rQQz9HnOZo/71ntMpDuapq+7Ghkk5W+K7BDPDn0rqHaRDhh6dl9r3sOZgleFlnsay6Rw=="], + + "@zerebos/eslint-config-typescript": ["@zerebos/eslint-config-typescript@1.0.3", "", { "dependencies": { "typescript-eslint": "^8.56.0" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-nLaLb79n80ya0/zPt5gcbyAGeCZ7fzHNH1KuosV+v36P+p5PrP03ZqupUwm5OoABKRPyL5CEsY8Cro26xHmm8Q=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.0.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.2", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.1", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ=="], + + "eslint-plugin-svelte": ["eslint-plugin-svelte@3.15.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q=="], + + "eslint-scope": ["eslint-scope@9.1.1", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "espree": ["espree@11.1.1", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "focus-visible": ["focus-visible@5.2.1", "", {}, "sha512-8Bx950VD1bWTQJEH/AM6SpEk+SU55aVnp4Ujhuuxy3eMEBCRwBnTBnVXr9YAPvZL3/CNjCa8u4IWfNmEO53whA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], + + "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], + + "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "rollup": ["rollup@4.58.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.58.0", "@rollup/rollup-android-arm64": "4.58.0", "@rollup/rollup-darwin-arm64": "4.58.0", "@rollup/rollup-darwin-x64": "4.58.0", "@rollup/rollup-freebsd-arm64": "4.58.0", "@rollup/rollup-freebsd-x64": "4.58.0", "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", "@rollup/rollup-linux-arm-musleabihf": "4.58.0", "@rollup/rollup-linux-arm64-gnu": "4.58.0", "@rollup/rollup-linux-arm64-musl": "4.58.0", "@rollup/rollup-linux-loong64-gnu": "4.58.0", "@rollup/rollup-linux-loong64-musl": "4.58.0", "@rollup/rollup-linux-ppc64-gnu": "4.58.0", "@rollup/rollup-linux-ppc64-musl": "4.58.0", "@rollup/rollup-linux-riscv64-gnu": "4.58.0", "@rollup/rollup-linux-riscv64-musl": "4.58.0", "@rollup/rollup-linux-s390x-gnu": "4.58.0", "@rollup/rollup-linux-x64-gnu": "4.58.0", "@rollup/rollup-linux-x64-musl": "4.58.0", "@rollup/rollup-openbsd-x64": "4.58.0", "@rollup/rollup-openharmony-arm64": "4.58.0", "@rollup/rollup-win32-arm64-msvc": "4.58.0", "@rollup/rollup-win32-ia32-msvc": "4.58.0", "@rollup/rollup-win32-x64-gnu": "4.58.0", "@rollup/rollup-win32-x64-msvc": "4.58.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw=="], + + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "svelte": ["svelte@5.53.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WzxFHZhhD23Qzu7JCYdvm1rxvRSzdt9HtHO8TScMBX51bLRFTcJmATVqjqXG+6Ln6hrViGCo9DzwOhAasxwC/w=="], + + "svelte-check": ["svelte-check@4.4.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw=="], + + "svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + + "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.56.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.0", "@typescript-eslint/parser": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "svelte-eslint-parser/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + } +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..32c190fd --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,25 @@ +import {node} from "@zerebos/eslint-config"; +import ts from "@zerebos/eslint-config-typescript"; +import {defineConfig} from "eslint/config"; +import {build} from "@zerebos/eslint-config-svelte"; +import svelteConfig from "./svelte.config.js"; + + +/** @type {import("@zerebos/eslint-config-typescript").ConfigArray} */ +export default defineConfig( + defineConfig( + ...node, + ...ts.configs.recommendedWithTypes + ), + ...build(svelteConfig), + { + languageOptions: { + globals: { + __INSTALLER_LICENSE__: "readonly", + } + } + }, + { + ignores: ["build/", ".svelte-kit/", "dist/", "src/lib/wailsjs/", "node_modules/"] + }, +); diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..aa6ed1af --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "betterdiscord-installer", + "productName": "BetterDiscord Installer", + "description": "A simple standalone program which automates the installation, removal and maintenance of BetterDiscord.", + "author": "BetterDiscord", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "bun run --bun vite dev", + "build": "bun run --bun vite build", + "postbuild": "touch build/.gitkeep", + "preview": "bun run --bun vite preview", + "check": "bun run --bun svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "bun run --bun svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "bun run --bun eslint .", + "test": "vitest", + "test:watch": "vitest --watch", + "test:ui": "vitest --ui" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.52.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@testing-library/jest-dom": "^6.9.0", + "@testing-library/svelte": "^5.2.8", + "@testing-library/user-event": "^14.6.1", + "@types/bun": "^1.3.9", + "@zerebos/eslint-config": "^1.0.2", + "@zerebos/eslint-config-svelte": "^1.0.3", + "@zerebos/eslint-config-typescript": "^1.0.3", + "eslint": "^10.0.0", + "focus-visible": "^5.2.1", + "globals": "^17.3.0", + "jsdom": "^26.0.0", + "svelte": "^5.53.0", + "svelte-check": "^4.4.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.0", + "vite": "^7.3.1", + "vitest": "^3.2.4" + } +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100644 index 00000000..bf0f6694 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +433b24fd52063a0df29a5d5a99ac0fdb \ No newline at end of file diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 00000000..5889edbd --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 00000000..78d22928 --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1 @@ +declare const __INSTALLER_LICENSE__: string; \ No newline at end of file diff --git a/assets/images/background.png b/frontend/src/lib/assets/images/background.png similarity index 100% rename from assets/images/background.png rename to frontend/src/lib/assets/images/background.png diff --git a/assets/images/canary.png b/frontend/src/lib/assets/images/canary.png similarity index 100% rename from assets/images/canary.png rename to frontend/src/lib/assets/images/canary.png diff --git a/assets/images/ptb.png b/frontend/src/lib/assets/images/ptb.png similarity index 100% rename from assets/images/ptb.png rename to frontend/src/lib/assets/images/ptb.png diff --git a/assets/images/stable.png b/frontend/src/lib/assets/images/stable.png similarity index 100% rename from assets/images/stable.png rename to frontend/src/lib/assets/images/stable.png diff --git a/assets/license.txt b/frontend/src/lib/assets/license.txt similarity index 100% rename from assets/license.txt rename to frontend/src/lib/assets/license.txt diff --git a/src/renderer/common/Button.svelte b/frontend/src/lib/components/Button.svelte similarity index 56% rename from src/renderer/common/Button.svelte rename to frontend/src/lib/components/Button.svelte index 88f6fccc..ed9262de 100644 --- a/src/renderer/common/Button.svelte +++ b/frontend/src/lib/components/Button.svelte @@ -1,18 +1,30 @@ - @@ -45,23 +57,23 @@ text-overflow: ellipsis; } - .button.type-primary { + .button.style-primary { border: 1px solid transparent; background-color: var(--accent); color: #ffffff; } - .button.type-primary:hover { + .button.style-primary:hover { background-color: var(--accent-hover); } - .button.type-secondary { + .button.style-secondary { background-color: transparent; border: 1px solid rgba(255, 255, 255, 0.05); color: var(--text-normal); } - .button.type-secondary:hover { + .button.style-secondary:hover { border-color: rgba(255, 255, 255, 0.1); } \ No newline at end of file diff --git a/frontend/src/lib/components/Button.test.ts b/frontend/src/lib/components/Button.test.ts new file mode 100644 index 00000000..1f2fcfba --- /dev/null +++ b/frontend/src/lib/components/Button.test.ts @@ -0,0 +1,36 @@ +import {createRawSnippet} from "svelte"; +import {expect, it, describe} from "vitest"; +import {render} from "@testing-library/svelte"; +import Button from "./Button.svelte"; + + +describe("Button", () => { + it("renders content inside the button", () => { + const label = createRawSnippet(() => ({ + render: () => "Click me" + })); + + const {getByRole} = render(Button, { + props: { + children: label + } + }); + + expect(getByRole("button")).toHaveTextContent("Click me"); + }); + + it("uses the primary style when requested", () => { + const label = createRawSnippet(() => ({ + render: () => "Primary" + })); + + const {getByRole} = render(Button, { + props: { + style: "primary", + children: label + } + }); + + expect(getByRole("button")).toHaveClass("style-primary"); + }); +}); diff --git a/src/renderer/common/ButtonGroup.svelte b/frontend/src/lib/components/ButtonGroup.svelte similarity index 60% rename from src/renderer/common/ButtonGroup.svelte rename to frontend/src/lib/components/ButtonGroup.svelte index 62c44753..349dfd31 100644 --- a/src/renderer/common/ButtonGroup.svelte +++ b/frontend/src/lib/components/ButtonGroup.svelte @@ -1,7 +1,15 @@ -
- + + + +
+ {@render children()}
+ \ No newline at end of file diff --git a/src/renderer/pages/Loading.svelte b/frontend/src/lib/components/Loader.svelte similarity index 71% rename from src/renderer/pages/Loading.svelte rename to frontend/src/lib/components/Loader.svelte index fea6185f..970b2918 100644 --- a/src/renderer/pages/Loading.svelte +++ b/frontend/src/lib/components/Loader.svelte @@ -1,9 +1,9 @@ -
- +
\ No newline at end of file diff --git a/frontend/src/lib/components/Page.svelte b/frontend/src/lib/components/Page.svelte new file mode 100644 index 00000000..20cf3009 --- /dev/null +++ b/frontend/src/lib/components/Page.svelte @@ -0,0 +1,50 @@ + + + +
+ {#key title} +
+ {title} + {@render children()} +
+ {/key} +
+ + + \ No newline at end of file diff --git a/src/renderer/common/PageHeader.svelte b/frontend/src/lib/components/PageHeader.svelte similarity index 85% rename from src/renderer/common/PageHeader.svelte rename to frontend/src/lib/components/PageHeader.svelte index 2501cf18..07e83fe9 100644 --- a/src/renderer/common/PageHeader.svelte +++ b/frontend/src/lib/components/PageHeader.svelte @@ -1,22 +1,32 @@ - \ No newline at end of file diff --git a/frontend/src/lib/components/ProgressBar.test.ts b/frontend/src/lib/components/ProgressBar.test.ts new file mode 100644 index 00000000..a3a0ef86 --- /dev/null +++ b/frontend/src/lib/components/ProgressBar.test.ts @@ -0,0 +1,33 @@ +import {render} from "@testing-library/svelte"; +import ProgressBar from "./ProgressBar.svelte"; +import {describe, expect, it} from "vitest"; + + +describe("ProgressBar", () => { + it("renders a determinate width from value and max", () => { + const {container} = render(ProgressBar, { + props: { + "value": 25, + "max": 100, + "class": "" + } + }); + + const fill = container.querySelector(".progress-fill"); + expect(fill).toHaveStyle({width: "25%"}); + }); + + it("renders two fills in indeterminate mode", () => { + const {container} = render(ProgressBar, { + props: { + "indeterminate": true, + "class": "" + } + }); + + const fills = container.querySelectorAll(".progress-fill"); + expect(fills.length).toBe(2); + expect(fills[0]).toHaveClass("increase"); + expect(fills[1]).toHaveClass("decrease"); + }); +}); diff --git a/src/renderer/common/Radio.svelte b/frontend/src/lib/components/Radio.svelte similarity index 68% rename from src/renderer/common/Radio.svelte rename to frontend/src/lib/components/Radio.svelte index d948321f..bdd9d142 100644 --- a/src/renderer/common/Radio.svelte +++ b/frontend/src/lib/components/Radio.svelte @@ -1,20 +1,28 @@ - +