├── .eslintrc.json ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── add-to-project.yml │ ├── docs.yml │ ├── release.yml │ ├── semantic.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierrc.json ├── .releaserc.json ├── .yarn └── releases │ └── yarn-4.10.3.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── Cache.ts ├── Downloader.ts ├── GotDownloader.ts ├── artifact-utils.ts ├── downloader-resolver.ts ├── index.ts ├── proxy.ts ├── types.ts └── utils.ts ├── test ├── Cache.spec.ts ├── FixtureDownloader.ts ├── GotDownloader.network.spec.ts ├── __snapshots__ │ ├── GotDownloader.network.spec.ts.snap │ └── index.spec.ts.snap ├── artifact-utils.spec.ts ├── checksums.spec.ts ├── fixtures │ ├── SHASUMS256.txt │ ├── chromedriver-v2.0.9-darwin-x64.zip │ ├── electron-v1.0.0-darwin-arm64.zip │ ├── electron-v1.0.0-darwin-x64.zip │ ├── electron-v1.0.0-linux-x64.zip │ ├── electron-v1.0.0-win32-x64.zip │ ├── electron-v1.3.3-darwin-arm64.zip │ ├── electron-v1.3.3-darwin-x64 copy.zip │ ├── electron-v1.3.3-darwin-x64.zip │ ├── electron-v1.3.3-linux-x64 copy.zip │ ├── electron-v1.3.3-linux-x64.zip │ ├── electron-v1.3.3-win32-x64 copy.zip │ ├── electron-v1.3.3-win32-x64.zip │ ├── electron-v1.3.5-darwin-arm64.zip │ ├── electron-v1.3.5-darwin-x64.zip │ ├── electron-v1.3.5-linux-x64.zip │ ├── electron-v1.3.5-win32-x64.zip │ ├── electron-v2.0.10-darwin-arm64.zip │ ├── electron-v2.0.10-darwin-x64.zip │ ├── electron-v2.0.10-linux-x64.zip │ ├── electron-v2.0.10-win32-x64.zip │ ├── electron-v2.0.3-darwin-arm64.zip │ ├── electron-v2.0.3-darwin-x64.zip │ ├── electron-v2.0.3-linux-x64.zip │ ├── electron-v2.0.3-win32-x64.zip │ ├── electron-v2.0.9-darwin-arm64.zip │ ├── electron-v2.0.9-darwin-x64.zip │ ├── electron-v2.0.9-linux-armv7l.zip │ ├── electron-v2.0.9-linux-x64.zip │ ├── electron-v2.0.9-win32-x64.zip │ ├── electron-v6.0.0-nightly.20190213-darwin-arm64.zip │ ├── electron-v6.0.0-nightly.20190213-darwin-x64.zip │ ├── electron-v6.0.0-nightly.20190213-linux-x64.zip │ ├── electron-v6.0.0-nightly.20190213-win32-x64.zip │ └── electron.d.ts ├── index.spec.ts └── utils.spec.ts ├── tsconfig.json ├── typedoc.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:import/errors", 7 | "plugin:import/warnings", 8 | "plugin:import/typescript", 9 | "prettier", 10 | "prettier/@typescript-eslint" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/prefer-ts-expect-error": 0, 14 | "@typescript-eslint/ban-ts-comment": 0, 15 | "import/no-unresolved": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @electron/wg-ecosystem 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add to Ecosystem WG Project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | pull_request_target: 8 | types: 9 | - opened 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | add-to-project: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Generate GitHub App token 18 | uses: electron/github-app-auth-action@384fd19694fe7b6dcc9a684746c6976ad78228ae # v1.1.1 19 | id: generate-token 20 | with: 21 | creds: ${{ secrets.ECOSYSTEM_ISSUE_TRIAGE_GH_APP_CREDS }} 22 | org: electron 23 | - name: Add to Project 24 | uses: dsanders11/project-actions/add-item@2134fe7cc71c58b7ae259c82a8e63c6058255678 # v1.7.0 25 | with: 26 | field: Opened 27 | field-value: ${{ github.event.pull_request.created_at || github.event.issue.created_at }} 28 | project-number: 89 29 | token: ${{ steps.generate-token.outputs.token }} 30 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+* 7 | 8 | permissions: 9 | id-token: write 10 | contents: read 11 | 12 | jobs: 13 | docs: 14 | runs-on: ubuntu-latest 15 | environment: docs-publish 16 | steps: 17 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag: v6.0.0 18 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # tag: v6.0.0 19 | with: 20 | node-version: 22.12.x 21 | cache: 'yarn' 22 | - name: Install dependencies 23 | run: yarn --immutable 24 | - name: Build API documentation 25 | run: yarn build:docs 26 | - name: Azure login 27 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 28 | with: 29 | client-id: ${{ secrets.AZURE_OIDC_CLIENT_ID }} 30 | tenant-id: ${{ secrets.AZURE_OIDC_TENANT_ID }} 31 | subscription-id: ${{ secrets.AZURE_OIDC_SUBSCRIPTION_ID }} 32 | - name: Upload to Azure Blob Storage 33 | uses: azure/cli@9f7ce6f37c31b777ec6c6b6d1dfe7db79f497956 # tag: v2.2.0 34 | with: 35 | inlineScript: | 36 | az storage blob upload-batch --account-name ${{ secrets.AZURE_ECOSYSTEM_PACKAGES_STORAGE_ACCOUNT_NAME }} -d '$web/get/${{ github.ref_name }}' -s ./docs --overwrite --auth-mode login 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | uses: ./.github/workflows/test.yml 11 | 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | needs: test 16 | environment: npm-trusted-publisher 17 | permissions: 18 | id-token: write # for publishing releases 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 22 | with: 23 | persist-credentials: false 24 | - name: Setup Node.js 25 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 26 | with: 27 | node-version-file: .nvmrc 28 | package-manager-cache: false 29 | - name: Install 30 | run: yarn install --immutable 31 | - name: Get GitHub app token 32 | id: secret-service 33 | uses: electron/secret-service-action@3476425e8b30555aac15b1b7096938e254b0e155 # v1.0.0 34 | - name: Run semantic release 35 | uses: electron/semantic-trusted-release@5eceb399ac8de8863205cf6e34109bce473ba566 # v1.0.1 36 | with: 37 | github-token: ${{ fromJSON(steps.secret-service.outputs.secrets).GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/semantic.yml: -------------------------------------------------------------------------------- 1 | name: "Check Semantic Commit" 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | main: 15 | permissions: 16 | pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs 17 | statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR 18 | name: Validate PR Title 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: semantic-pull-request 22 | uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | validateSingleCommit: false 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | schedule: 8 | - cron: '0 22 * * 3' 9 | workflow_call: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | name: Test 17 | strategy: 18 | matrix: 19 | node-version: 20 | - 22.12.x 21 | os: 22 | - macos-latest 23 | - ubuntu-latest 24 | - windows-latest 25 | runs-on: "${{ matrix.os }}" 26 | steps: 27 | - run: git config --global core.autocrlf input 28 | - name: Checkout 29 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 30 | - name: Setup Node.js 31 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 32 | with: 33 | node-version: "${{ matrix.node-version }}" 34 | cache: 'yarn' 35 | - name: Install dependencies 36 | run: yarn install --immutable 37 | - name: Test 38 | run: yarn lint && yarn test 39 | - name: Build docs 40 | run: yarn build:docs 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | docs 4 | node_modules 5 | .idea 6 | .vscode 7 | .yarn/install-state.gz 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "parser": "typescript", 7 | "overrides": [ 8 | { 9 | "files": ["*.json", "*.jsonc", "*.json5"], 10 | "options": { 11 | "parser": "json" 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | "@semantic-release/npm", 6 | "@semantic-release/github" 7 | ], 8 | "branches": [ "main" ] 9 | } 10 | 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableScripts: false 2 | 3 | nodeLinker: node-modules 4 | 5 | npmMinimalAgeGate: 10080 6 | 7 | npmPreapprovedPackages: 8 | - "@electron/*" 9 | 10 | yarnPath: .yarn/releases/yarn-4.10.3.cjs 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Contributors to the Electron project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @electron/get 2 | 3 | > Download Electron release artifacts 4 | 5 | [![Test](https://github.com/electron/get/actions/workflows/test.yml/badge.svg)](https://github.com/electron/get/actions/workflows/test.yml) 6 | [![NPM package](https://img.shields.io/npm/v/@electron/get)](https://npm.im/@electron/get) 7 | [![API docs](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.npmjs.org%2F%40electron%2Fget%2Flatest&query=%24.version&logo=typescript&logoColor=white&label=API%20Docs 8 | )](https://packages.electronjs.org/get) 9 | 10 | ## Usage 11 | 12 | For full API details, see the [API documentation](https://packages.electronjs.org/get). 13 | 14 | ### Simple: Downloading an Electron Binary ZIP 15 | 16 | ```typescript 17 | import { download } from '@electron/get'; 18 | 19 | // NB: Use this syntax within an async function, Node does not have support for 20 | // top-level await as of Node 12. 21 | const zipFilePath = await download('4.0.4'); 22 | ``` 23 | 24 | ### Advanced: Downloading a macOS Electron Symbol File 25 | 26 | ```typescript 27 | import { downloadArtifact } from '@electron/get'; 28 | 29 | // NB: Use this syntax within an async function, Node does not have support for 30 | // top-level await as of Node 12. 31 | const zipFilePath = await downloadArtifact({ 32 | version: '4.0.4', 33 | platform: 'darwin', 34 | artifactName: 'electron', 35 | artifactSuffix: 'symbols', 36 | arch: 'x64', 37 | }); 38 | ``` 39 | 40 | ### Specifying a mirror 41 | 42 | To specify another location to download Electron assets from, the following options are 43 | available: 44 | 45 | * `mirrorOptions` Object 46 | * `mirror` String (optional) - The base URL of the mirror to download from. 47 | * `nightlyMirror` String (optional) - The Electron nightly-specific mirror URL. 48 | * `customDir` String (optional) - The name of the directory to download from, often scoped by version number. 49 | * `customFilename` String (optional) - The name of the asset to download. 50 | * `resolveAssetURL` Function (optional) - A function allowing customization of the url used to download the asset. 51 | 52 | Anatomy of a download URL, in terms of `mirrorOptions`: 53 | 54 | ``` 55 | https://github.com/electron/electron/releases/download/v4.0.4/electron-v4.0.4-linux-x64.zip 56 | | | | | 57 | ------------------------------------------------------- ----------------------------- 58 | | | 59 | mirror / nightlyMirror | | customFilename 60 | ------ 61 | || 62 | customDir 63 | ``` 64 | 65 | Example: 66 | 67 | ```typescript 68 | import { download } from '@electron/get'; 69 | 70 | const zipFilePath = await download('4.0.4', { 71 | mirrorOptions: { 72 | mirror: 'https://mirror.example.com/electron/', 73 | customDir: 'custom', 74 | customFilename: 'unofficial-electron-linux.zip' 75 | } 76 | }); 77 | // Will download from https://mirror.example.com/electron/custom/unofficial-electron-linux.zip 78 | 79 | const nightlyZipFilePath = await download('8.0.0-nightly.20190901', { 80 | mirrorOptions: { 81 | nightlyMirror: 'https://nightly.example.com/', 82 | customDir: 'nightlies', 83 | customFilename: 'nightly-linux.zip' 84 | } 85 | }); 86 | // Will download from https://nightly.example.com/nightlies/nightly-linux.zip 87 | ``` 88 | 89 | `customDir` can have the placeholder `{{ version }}`, which will be replaced by the version 90 | specified (without the leading `v`). For example: 91 | 92 | ```javascript 93 | const zipFilePath = await download('4.0.4', { 94 | mirrorOptions: { 95 | mirror: 'https://mirror.example.com/electron/', 96 | customDir: 'version-{{ version }}', 97 | platform: 'linux', 98 | arch: 'x64' 99 | } 100 | }); 101 | // Will download from https://mirror.example.com/electron/version-4.0.4/electron-v4.0.4-linux-x64.zip 102 | ``` 103 | 104 | #### Using environment variables for mirror options 105 | Mirror options can also be specified via the following environment variables: 106 | * `ELECTRON_CUSTOM_DIR` - Specifies the custom directory to download from. 107 | * `ELECTRON_CUSTOM_FILENAME` - Specifies the custom file name to download. 108 | * `ELECTRON_MIRROR` - Specifies the URL of the server to download from if the version is not a nightly version. 109 | * `ELECTRON_NIGHTLY_MIRROR` - Specifies the URL of the server to download from if the version is a nightly version. 110 | 111 | ### Overriding the version downloaded 112 | 113 | The version downloaded can be overriden by setting the `ELECTRON_CUSTOM_VERSION` environment variable. 114 | Setting this environment variable will override the version passed in to `download` or `downloadArtifact`. 115 | 116 | ## How It Works 117 | 118 | This module downloads Electron to a known place on your system and caches it 119 | so that future requests for that asset can be returned instantly. The cache 120 | locations are: 121 | 122 | * Linux: `$XDG_CACHE_HOME` or `~/.cache/electron/` 123 | * MacOS: `~/Library/Caches/electron/` 124 | * Windows: `%LOCALAPPDATA%/electron/Cache` or `~/AppData/Local/electron/Cache/` 125 | 126 | By default, the module uses [`got`](https://github.com/sindresorhus/got) as the 127 | downloader. As a result, you can use the same [options](https://github.com/sindresorhus/got#options) 128 | via `downloadOptions`. 129 | 130 | ### Progress Bar 131 | 132 | By default, a progress bar is shown when downloading an artifact for more than 30 seconds. To 133 | disable, set the `ELECTRON_GET_NO_PROGRESS` environment variable to any non-empty value, or set 134 | `quiet` to `true` in `downloadOptions`. If you need to monitor progress yourself via the API, set 135 | `getProgressCallback` in `downloadOptions`, which has the same function signature as `got`'s 136 | [`downloadProgress` event callback](https://github.com/sindresorhus/got#ondownloadprogress-progress). 137 | 138 | ### Proxies 139 | 140 | Downstream packages should utilize the `initializeProxy` function to add HTTP(S) proxy support. If 141 | the environment variable `ELECTRON_GET_USE_PROXY` is set, it is called automatically. 142 | 143 | ### Debug 144 | 145 | [`debug`](https://www.npmjs.com/package/debug) is used to display logs and messages. 146 | Set the `DEBUG=@electron/get*` environment variable to log additional 147 | debug information from this module. 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@electron/get", 3 | "version": "0.0.0-development", 4 | "type": "module", 5 | "exports": "./dist/index.js", 6 | "description": "Utility for downloading artifacts from different versions of Electron", 7 | "repository": "https://github.com/electron/get", 8 | "author": "Samuel Attard", 9 | "license": "MIT", 10 | "publishConfig": { 11 | "provenance": true 12 | }, 13 | "scripts": { 14 | "build": "tsc", 15 | "build:docs": "npx typedoc", 16 | "eslint": "eslint --ext .ts src test", 17 | "lint": "npm run prettier && npm run eslint", 18 | "prepare": "husky", 19 | "prettier": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", 20 | "prettier:write": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 21 | "prepublishOnly": "npm run build", 22 | "test": "vitest run --coverage", 23 | "test:nonetwork": "npm run lint && vitest run --coverage --testPathIgnorePatterns network.spec" 24 | }, 25 | "files": [ 26 | "dist/", 27 | "README.md" 28 | ], 29 | "engines": { 30 | "node": ">=22.12.0" 31 | }, 32 | "dependencies": { 33 | "debug": "^4.1.1", 34 | "env-paths": "^3.0.0", 35 | "got": "^14.4.5", 36 | "graceful-fs": "^4.2.11", 37 | "progress": "^2.0.3", 38 | "semver": "^7.6.3", 39 | "sumchecker": "^3.0.1" 40 | }, 41 | "devDependencies": { 42 | "@tsconfig/node22": "^22.0.0", 43 | "@types/debug": "^4.1.4", 44 | "@types/graceful-fs": "^4.1.9", 45 | "@types/node": "~22.10.5", 46 | "@types/progress": "^2.0.3", 47 | "@types/semver": "^7.5.8", 48 | "@typescript-eslint/eslint-plugin": "^8.19.1", 49 | "@typescript-eslint/parser": "^8.0.0", 50 | "@vitest/coverage-v8": "3.0.5", 51 | "esbuild-plugin-file-path-extensions": "^2.1.4", 52 | "eslint": "^8.57.0", 53 | "eslint-config-prettier": "^6.15.0", 54 | "eslint-plugin-import": "^2.31.0", 55 | "husky": "^9.1.7", 56 | "lint-staged": "^15.4.1", 57 | "prettier": "^3.4.2", 58 | "typedoc": "~0.25.13", 59 | "typescript": "~5.4.5", 60 | "vitest": "^3.0.5" 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "lint-staged" 65 | } 66 | }, 67 | "lint-staged": { 68 | "*.ts": [ 69 | "eslint --fix", 70 | "prettier --write" 71 | ] 72 | }, 73 | "keywords": [ 74 | "electron", 75 | "download", 76 | "prebuild", 77 | "get", 78 | "artifact", 79 | "release" 80 | ], 81 | "optionalDependencies": { 82 | "global-agent": "^3.0.0" 83 | }, 84 | "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f" 85 | } 86 | -------------------------------------------------------------------------------- /src/Cache.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import envPaths from 'env-paths'; 3 | import fs from 'graceful-fs'; 4 | 5 | import crypto from 'node:crypto'; 6 | import path from 'node:path'; 7 | import { pipeline } from 'node:stream/promises'; 8 | import url from 'node:url'; 9 | 10 | const d = debug('@electron/get:cache'); 11 | 12 | const defaultCacheRoot = envPaths('electron', { 13 | suffix: '', 14 | }).cache; 15 | 16 | export class Cache { 17 | constructor(private cacheRoot = defaultCacheRoot) {} 18 | 19 | public static getCacheDirectory(downloadUrl: string): string { 20 | const parsedDownloadUrl = url.parse(downloadUrl); 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | const { search, hash, pathname, ...rest } = parsedDownloadUrl; 23 | const strippedUrl = url.format({ ...rest, pathname: path.dirname(pathname || 'electron') }); 24 | 25 | return crypto.createHash('sha256').update(strippedUrl).digest('hex'); 26 | } 27 | 28 | public getCachePath(downloadUrl: string, fileName: string): string { 29 | return path.resolve(this.cacheRoot, Cache.getCacheDirectory(downloadUrl), fileName); 30 | } 31 | 32 | public getPathForFileInCache(url: string, fileName: string): string | null { 33 | const cachePath = this.getCachePath(url, fileName); 34 | if (fs.existsSync(cachePath)) { 35 | return cachePath; 36 | } 37 | 38 | return null; 39 | } 40 | 41 | private async hashFile(path: string): Promise { 42 | const hasher = crypto.createHash('sha256'); 43 | await pipeline(fs.createReadStream(path), hasher); 44 | return hasher.digest('hex'); 45 | } 46 | 47 | public async putFileInCache(url: string, currentPath: string, fileName: string): Promise { 48 | const attempt = async (attemptsLeft = 3): Promise => { 49 | try { 50 | const cachePath = this.getCachePath(url, fileName); 51 | d(`Moving ${currentPath} to ${cachePath}`); 52 | 53 | if (!fs.existsSync(path.dirname(cachePath))) { 54 | await fs.promises.mkdir(path.dirname(cachePath), { recursive: true }); 55 | } 56 | 57 | if (fs.existsSync(cachePath)) { 58 | const [existingHash, currentHash] = await Promise.all([ 59 | this.hashFile(cachePath), 60 | this.hashFile(currentPath), 61 | ]); 62 | if (existingHash !== currentHash) { 63 | d('* Replacing existing file as it does not match our inbound file'); 64 | await fs.promises.rm(cachePath, { recursive: true, force: true }); 65 | } else { 66 | d('* Using existing file as the hash matches our inbound file, no need to replace'); 67 | return cachePath; 68 | } 69 | } 70 | 71 | try { 72 | await fs.promises.rename(currentPath, cachePath); 73 | } catch (err) { 74 | if ((err as NodeJS.ErrnoException).code === 'EXDEV') { 75 | // Cross-device link, fallback to copy and delete 76 | await fs.promises.cp(currentPath, cachePath, { 77 | force: true, 78 | recursive: true, 79 | verbatimSymlinks: true, 80 | }); 81 | await fs.promises.rm(currentPath, { force: true, recursive: true }); 82 | } else { 83 | throw err; 84 | } 85 | } 86 | 87 | return cachePath; 88 | } catch (err) { 89 | if (process.platform === 'win32' && (err as NodeJS.ErrnoException).code === 'EPERM') { 90 | // On windows this normally means we're fighting another instance of @electron/get 91 | // also trying to write this file to the cache 92 | d('Experienced error putting thing in cache', err); 93 | if (attemptsLeft > 0) { 94 | d('Trying again in a few seconds'); 95 | await new Promise((resolve) => { 96 | setTimeout(resolve, 2000); 97 | }); 98 | return await attempt(attemptsLeft - 1); 99 | } 100 | d('We have already tried too many times, giving up...'); 101 | } 102 | throw err; 103 | } 104 | }; 105 | 106 | return await attempt(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Downloader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic interface for the artifact downloader library. 3 | * The default implementation is {@link GotDownloader}, 4 | * but any custom downloader can be passed to `@electron/get` via 5 | * the {@link ElectronDownloadRequestOptions.downloader} option. 6 | * 7 | * @typeParam T - Options to pass to the downloader 8 | * @category Downloader 9 | */ 10 | export interface Downloader { 11 | /** 12 | * Download an artifact from an arbitrary URL to a file path on system 13 | * @param url URL of the file to download 14 | * @param targetFilePath Filesystem path to download the artifact to (including the file name) 15 | * @param options Options to pass to the downloader 16 | */ 17 | download(url: string, targetFilePath: string, options: T): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /src/GotDownloader.ts: -------------------------------------------------------------------------------- 1 | import got, { HTTPError, Progress as GotProgress, Options as GotOptions, Progress } from 'got'; 2 | import fs from 'graceful-fs'; 3 | 4 | import path from 'node:path'; 5 | import ProgressBar from 'progress'; 6 | 7 | import { Downloader } from './Downloader.js'; 8 | import { pipeline } from 'node:stream/promises'; 9 | 10 | const PROGRESS_BAR_DELAY_IN_SECONDS = 30; 11 | 12 | /** 13 | * Options for the default [`got`](https://github.com/sindresorhus/got) Downloader implementation. 14 | * 15 | * @category Downloader 16 | * @see {@link https://github.com/sindresorhus/got/tree/v11.8.5?tab=readme-ov-file#options | `got#options`} for possible keys/values. 17 | */ 18 | export type GotDownloaderOptions = GotOptions & { isStream?: true } & { 19 | /** 20 | * if defined, triggers every time `got`'s 21 | * {@link https://github.com/sindresorhus/got/tree/v11.8.5?tab=readme-ov-file#downloadprogress | `downloadProgress``} event callback is triggered. 22 | */ 23 | getProgressCallback?: (progress: GotProgress) => Promise; 24 | /** 25 | * if `true`, disables the console progress bar (setting the `ELECTRON_GET_NO_PROGRESS` 26 | * environment variable to a non-empty value also does this). 27 | */ 28 | quiet?: boolean; 29 | }; 30 | 31 | /** 32 | * Default {@link Downloader} implemented with {@link https://npmjs.com/package/got | `got`}. 33 | * @category Downloader 34 | */ 35 | export class GotDownloader implements Downloader { 36 | async download( 37 | url: string, 38 | targetFilePath: string, 39 | options?: Partial, 40 | ): Promise { 41 | if (!options) { 42 | options = {}; 43 | } 44 | const { quiet, getProgressCallback, ...gotOptions } = options; 45 | let downloadCompleted = false; 46 | let bar: ProgressBar | undefined; 47 | let progressPercent: number; 48 | let timeout: NodeJS.Timeout | undefined = undefined; 49 | await fs.promises.mkdir(path.dirname(targetFilePath), { recursive: true }); 50 | const writeStream = fs.createWriteStream(targetFilePath); 51 | 52 | if (!quiet || !process.env.ELECTRON_GET_NO_PROGRESS) { 53 | const start = new Date(); 54 | timeout = setTimeout(() => { 55 | if (!downloadCompleted) { 56 | bar = new ProgressBar( 57 | `Downloading ${path.basename(url)}: [:bar] :percent ETA: :eta seconds `, 58 | { 59 | curr: progressPercent, 60 | total: 100, 61 | }, 62 | ); 63 | // https://github.com/visionmedia/node-progress/issues/159 64 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 65 | (bar as any).start = start; 66 | } 67 | }, PROGRESS_BAR_DELAY_IN_SECONDS * 1000); 68 | } 69 | const downloadStream = got.stream(url, gotOptions); 70 | downloadStream.on('downloadProgress', async (progress: Progress) => { 71 | progressPercent = progress.percent; 72 | if (bar) { 73 | bar.update(progress.percent); 74 | } 75 | if (getProgressCallback) { 76 | await getProgressCallback(progress); 77 | } 78 | }); 79 | try { 80 | await pipeline(downloadStream, writeStream); 81 | } catch (error) { 82 | if (error instanceof HTTPError && (error as HTTPError).response.statusCode === 404) { 83 | error.message += ` for ${(error as HTTPError).response.url}`; 84 | } 85 | throw error; 86 | } 87 | 88 | downloadCompleted = true; 89 | if (timeout) { 90 | clearTimeout(timeout); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/artifact-utils.ts: -------------------------------------------------------------------------------- 1 | import { ElectronArtifactDetails, MirrorOptions } from './types.js'; 2 | import { ensureIsTruthyString, normalizeVersion } from './utils.js'; 3 | 4 | const BASE_URL = 'https://github.com/electron/electron/releases/download/'; 5 | const NIGHTLY_BASE_URL = 'https://github.com/electron/nightlies/releases/download/'; 6 | 7 | export function getArtifactFileName(details: ElectronArtifactDetails): string { 8 | ensureIsTruthyString(details, 'artifactName'); 9 | 10 | if (details.isGeneric) { 11 | return details.artifactName; 12 | } 13 | 14 | ensureIsTruthyString(details, 'arch'); 15 | ensureIsTruthyString(details, 'platform'); 16 | ensureIsTruthyString(details, 'version'); 17 | 18 | return `${[ 19 | details.artifactName, 20 | details.version, 21 | details.platform, 22 | details.arch, 23 | ...(details.artifactSuffix ? [details.artifactSuffix] : []), 24 | ].join('-')}.zip`; 25 | } 26 | 27 | function mirrorVar( 28 | name: keyof Omit, 29 | options: MirrorOptions, 30 | defaultValue: string, 31 | ): string { 32 | // Convert camelCase to camel_case for env var reading 33 | const snakeName = name.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}_${b}`).toLowerCase(); 34 | 35 | return ( 36 | // .npmrc 37 | process.env[`npm_config_electron_${name.toLowerCase()}`] || 38 | process.env[`NPM_CONFIG_ELECTRON_${snakeName.toUpperCase()}`] || 39 | process.env[`npm_config_electron_${snakeName}`] || 40 | // package.json 41 | process.env[`npm_package_config_electron_${name}`] || 42 | process.env[`npm_package_config_electron_${snakeName.toLowerCase()}`] || 43 | // env 44 | process.env[`ELECTRON_${snakeName.toUpperCase()}`] || 45 | options[name] || 46 | defaultValue 47 | ); 48 | } 49 | 50 | export async function getArtifactRemoteURL(details: ElectronArtifactDetails): Promise { 51 | const opts: MirrorOptions = details.mirrorOptions || {}; 52 | let base = mirrorVar('mirror', opts, BASE_URL); 53 | if (details.version.includes('nightly')) { 54 | base = mirrorVar('nightlyMirror', opts, NIGHTLY_BASE_URL); 55 | } 56 | const path = mirrorVar('customDir', opts, details.version).replace( 57 | '{{ version }}', 58 | details.version.replace(/^v/, ''), 59 | ); 60 | const file = mirrorVar('customFilename', opts, getArtifactFileName(details)); 61 | 62 | // Allow customized download URL resolution. 63 | if (opts.resolveAssetURL) { 64 | const url = await opts.resolveAssetURL(details); 65 | return url; 66 | } 67 | 68 | return `${base}${path}/${file}`; 69 | } 70 | 71 | export function getArtifactVersion(details: ElectronArtifactDetails): string { 72 | return normalizeVersion(mirrorVar('customVersion', details.mirrorOptions || {}, details.version)); 73 | } 74 | -------------------------------------------------------------------------------- /src/downloader-resolver.ts: -------------------------------------------------------------------------------- 1 | import { DownloadOptions } from './types.js'; 2 | import { Downloader } from './Downloader.js'; 3 | 4 | // TODO: Resolve the downloader or default to GotDownloader 5 | // Current thoughts are a dot-file traversal for something like 6 | // ".electron.downloader" which would be a text file with the name of the 7 | // npm module to import() and use as the downloader 8 | import { GotDownloader } from './GotDownloader.js'; 9 | 10 | export async function getDownloaderForSystem(): Promise> { 11 | return new GotDownloader(); 12 | } 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import fs from 'graceful-fs'; 3 | import path from 'node:path'; 4 | import util from 'node:util'; 5 | import semver from 'semver'; 6 | import sumchecker from 'sumchecker'; 7 | 8 | import { getArtifactFileName, getArtifactRemoteURL, getArtifactVersion } from './artifact-utils.js'; 9 | import { 10 | ElectronArtifactDetails, 11 | ElectronDownloadCacheMode, 12 | ElectronDownloadRequestOptions, 13 | ElectronGenericArtifactDetails, 14 | ElectronPlatformArtifactDetails, 15 | ElectronPlatformArtifactDetailsWithDefaults, 16 | } from './types.js'; 17 | import { Cache } from './Cache.js'; 18 | import { getDownloaderForSystem } from './downloader-resolver.js'; 19 | import { initializeProxy } from './proxy.js'; 20 | import { 21 | withTempDirectoryIn, 22 | getHostArch, 23 | getNodeArch, 24 | ensureIsTruthyString, 25 | isOfficialLinuxIA32Download, 26 | mkdtemp, 27 | doesCallerOwnTemporaryOutput, 28 | effectiveCacheMode, 29 | shouldTryReadCache, 30 | TempDirCleanUpMode, 31 | } from './utils.js'; 32 | 33 | export { getHostArch } from './utils.js'; 34 | export { initializeProxy } from './proxy.js'; 35 | export * from './types.js'; 36 | 37 | const d = debug('@electron/get:index'); 38 | 39 | if (process.env.ELECTRON_GET_USE_PROXY) { 40 | initializeProxy(); 41 | } 42 | 43 | type ArtifactDownloader = ( 44 | _artifactDetails: ElectronPlatformArtifactDetailsWithDefaults | ElectronGenericArtifactDetails, 45 | ) => Promise; 46 | 47 | async function validateArtifact( 48 | artifactDetails: ElectronArtifactDetails, 49 | downloadedAssetPath: string, 50 | _downloadArtifact: ArtifactDownloader, 51 | ): Promise { 52 | return await withTempDirectoryIn( 53 | artifactDetails.tempDirectory, 54 | async (tempFolder) => { 55 | // Don't try to verify the hash of the hash file itself 56 | // and for older versions that don't have a SHASUMS256.txt 57 | if ( 58 | !artifactDetails.artifactName.startsWith('SHASUMS256') && 59 | !artifactDetails.unsafelyDisableChecksums && 60 | semver.gte(artifactDetails.version, '1.3.2') 61 | ) { 62 | let shasumPath: string; 63 | const checksums = artifactDetails.checksums; 64 | if (checksums) { 65 | shasumPath = path.resolve(tempFolder, 'SHASUMS256.txt'); 66 | const fileNames: string[] = Object.keys(checksums); 67 | if (fileNames.length === 0) { 68 | throw new Error( 69 | 'Provided "checksums" object is empty, cannot generate a valid SHASUMS256.txt', 70 | ); 71 | } 72 | const generatedChecksums = fileNames 73 | .map((fileName) => `${checksums[fileName]} *${fileName}`) 74 | .join('\n'); 75 | await util.promisify(fs.writeFile)(shasumPath, generatedChecksums); 76 | } else { 77 | shasumPath = await _downloadArtifact({ 78 | isGeneric: true, 79 | version: artifactDetails.version, 80 | artifactName: 'SHASUMS256.txt', 81 | downloadOptions: artifactDetails.downloadOptions, 82 | cacheRoot: artifactDetails.cacheRoot, 83 | downloader: artifactDetails.downloader, 84 | mirrorOptions: artifactDetails.mirrorOptions, 85 | // Never use the cache for loading checksums, load 86 | // them fresh every time 87 | cacheMode: ElectronDownloadCacheMode.Bypass, 88 | }); 89 | } 90 | 91 | try { 92 | // For versions 1.3.2 - 1.3.4, need to overwrite the `defaultTextEncoding` option: 93 | // https://github.com/electron/electron/pull/6676#discussion_r75332120 94 | if (semver.satisfies(artifactDetails.version, '1.3.2 - 1.3.4')) { 95 | const validatorOptions: sumchecker.ChecksumOptions = {}; 96 | validatorOptions.defaultTextEncoding = 'binary'; 97 | const checker = new sumchecker.ChecksumValidator( 98 | 'sha256', 99 | shasumPath, 100 | validatorOptions, 101 | ); 102 | await checker.validate( 103 | path.dirname(downloadedAssetPath), 104 | path.basename(downloadedAssetPath), 105 | ); 106 | } else { 107 | await sumchecker('sha256', shasumPath, path.dirname(downloadedAssetPath), [ 108 | path.basename(downloadedAssetPath), 109 | ]); 110 | } 111 | } finally { 112 | // Once we're done make sure we clean up the shasum temp dir 113 | await fs.promises.rm(path.dirname(shasumPath), { recursive: true, force: true }); 114 | } 115 | } 116 | }, 117 | doesCallerOwnTemporaryOutput(effectiveCacheMode(artifactDetails)) 118 | ? TempDirCleanUpMode.ORPHAN 119 | : TempDirCleanUpMode.CLEAN, 120 | ); 121 | } 122 | 123 | /** 124 | * Downloads an artifact from an Electron release and returns an absolute path 125 | * to the downloaded file. 126 | * 127 | * Each release of Electron comes with artifacts, many of which are 128 | * platform/arch-specific (e.g. `ffmpeg-v31.0.0-darwin-arm64.zip`) and others that 129 | * are generic (e.g. `SHASUMS256.txt`). 130 | * 131 | * 132 | * @param artifactDetails - The information required to download the artifact 133 | * @category Download Artifact 134 | */ 135 | export async function downloadArtifact( 136 | artifactDetails: ElectronPlatformArtifactDetailsWithDefaults | ElectronGenericArtifactDetails, 137 | ): Promise { 138 | const details: ElectronArtifactDetails = { 139 | ...(artifactDetails as ElectronArtifactDetails), 140 | }; 141 | if (!artifactDetails.isGeneric) { 142 | const platformArtifactDetails = details as ElectronPlatformArtifactDetails; 143 | if (!platformArtifactDetails.platform) { 144 | d('No platform found, defaulting to the host platform'); 145 | platformArtifactDetails.platform = process.platform; 146 | } 147 | if (platformArtifactDetails.arch) { 148 | platformArtifactDetails.arch = getNodeArch(platformArtifactDetails.arch); 149 | } else { 150 | d('No arch found, defaulting to the host arch'); 151 | platformArtifactDetails.arch = getHostArch(); 152 | } 153 | } 154 | ensureIsTruthyString(details, 'version'); 155 | 156 | details.version = getArtifactVersion(details); 157 | const fileName = getArtifactFileName(details); 158 | const url = await getArtifactRemoteURL(details); 159 | const cache = new Cache(details.cacheRoot); 160 | const cacheMode = effectiveCacheMode(details); 161 | 162 | // Do not check if the file exists in the cache when force === true 163 | if (shouldTryReadCache(cacheMode)) { 164 | d(`Checking the cache (${details.cacheRoot}) for ${fileName} (${url})`); 165 | const cachedPath = cache.getPathForFileInCache(url, fileName); 166 | 167 | if (cachedPath === null) { 168 | d('Cache miss'); 169 | } else { 170 | d('Cache hit'); 171 | let artifactPath = cachedPath; 172 | if (doesCallerOwnTemporaryOutput(cacheMode)) { 173 | // Copy out of cache into temporary directory if readOnly cache so 174 | // that the caller can take ownership of the returned file 175 | const tempDir = await mkdtemp(artifactDetails.tempDirectory); 176 | artifactPath = path.resolve(tempDir, fileName); 177 | await util.promisify(fs.copyFile)(cachedPath, artifactPath); 178 | } 179 | try { 180 | await validateArtifact(details, artifactPath, downloadArtifact); 181 | 182 | return artifactPath; 183 | } catch (err) { 184 | if (doesCallerOwnTemporaryOutput(cacheMode)) { 185 | await fs.promises.rm(path.dirname(artifactPath), { recursive: true, force: true }); 186 | } 187 | d("Artifact in cache didn't match checksums", err); 188 | d('falling back to re-download'); 189 | } 190 | } 191 | } 192 | 193 | if ( 194 | !details.isGeneric && 195 | isOfficialLinuxIA32Download( 196 | details.platform, 197 | details.arch, 198 | details.version, 199 | details.mirrorOptions, 200 | ) 201 | ) { 202 | console.warn('Official Linux/ia32 support is deprecated.'); 203 | console.warn('For more info: https://electronjs.org/blog/linux-32bit-support'); 204 | } 205 | 206 | return await withTempDirectoryIn( 207 | details.tempDirectory, 208 | async (tempFolder) => { 209 | const tempDownloadPath = path.resolve(tempFolder, getArtifactFileName(details)); 210 | 211 | const downloader = details.downloader || (await getDownloaderForSystem()); 212 | d( 213 | `Downloading ${url} to ${tempDownloadPath} with options: ${JSON.stringify( 214 | details.downloadOptions, 215 | )}`, 216 | ); 217 | await downloader.download(url, tempDownloadPath, details.downloadOptions); 218 | 219 | d('attempting to validate artifact...', { details }); 220 | await validateArtifact(details, tempDownloadPath, downloadArtifact); 221 | 222 | d('artifact validated'); 223 | 224 | if (doesCallerOwnTemporaryOutput(cacheMode)) { 225 | return tempDownloadPath; 226 | } else { 227 | return await cache.putFileInCache(url, tempDownloadPath, fileName); 228 | } 229 | }, 230 | doesCallerOwnTemporaryOutput(cacheMode) ? TempDirCleanUpMode.ORPHAN : TempDirCleanUpMode.CLEAN, 231 | ); 232 | } 233 | 234 | /** 235 | * Downloads the Electron binary for a specific version and returns an absolute path to a 236 | * ZIP file. 237 | * 238 | * @param version - The version of Electron you want to download (e.g. `31.0.0`) 239 | * @param options - Options to customize the download behavior 240 | * @returns An absolute path to the downloaded ZIP file 241 | * @category Download Electron 242 | */ 243 | export function download( 244 | version: string, 245 | options?: ElectronDownloadRequestOptions, 246 | ): Promise { 247 | return downloadArtifact({ 248 | ...options, 249 | version, 250 | platform: process.platform, 251 | arch: process.arch, 252 | artifactName: 'electron', 253 | }); 254 | } 255 | -------------------------------------------------------------------------------- /src/proxy.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import { getEnv, setEnv } from './utils.js'; 3 | 4 | const d = debug('@electron/get:proxy'); 5 | 6 | /** 7 | * Initializes a third-party proxy module for HTTP(S) requests. Call this function before 8 | * using the {@link download} and {@link downloadArtifact} APIs if you need proxy support. 9 | * 10 | * If the `ELECTRON_GET_USE_PROXY` environment variable is set to `true`, this function will be 11 | * called automatically for `@electron/get` requests. 12 | * 13 | * @category Utility 14 | * @see {@link https://github.com/gajus/global-agent?tab=readme-ov-file#environment-variables | `global-agent`} 15 | * documentation for available environment variables. 16 | * 17 | * @example 18 | * ```sh 19 | * export GLOBAL_AGENT_HTTPS_PROXY="$HTTPS_PROXY" 20 | * ``` 21 | */ 22 | export function initializeProxy(): void { 23 | try { 24 | // See: https://github.com/electron/get/pull/214#discussion_r798845713 25 | const env = getEnv('GLOBAL_AGENT_'); 26 | 27 | setEnv('GLOBAL_AGENT_HTTP_PROXY', env('HTTP_PROXY')); 28 | setEnv('GLOBAL_AGENT_HTTPS_PROXY', env('HTTPS_PROXY')); 29 | setEnv('GLOBAL_AGENT_NO_PROXY', env('NO_PROXY')); 30 | 31 | /** 32 | * TODO: replace global-agent with a hpagent. @BlackHole1 33 | * https://github.com/sindresorhus/got/blob/HEAD/documentation/tips.md#proxying 34 | */ 35 | // eslint-disable-next-line @typescript-eslint/no-require-imports 36 | require('global-agent').bootstrap(); 37 | } catch (e) { 38 | d('Could not load either proxy modules, built-in proxy support not available:', e); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Downloader } from './Downloader.js'; 2 | import { GotDownloader, GotDownloaderOptions } from './GotDownloader.js'; 3 | 4 | export { Downloader, GotDownloader, GotDownloaderOptions }; 5 | 6 | /** 7 | * Custom downloaders can implement any set of options. 8 | * @category Downloader 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export type DownloadOptions = any; 12 | 13 | /** 14 | * Options for specifying an alternative download mirror for Electron. 15 | * 16 | * @category Utility 17 | * @example 18 | * 19 | * To download the Electron v4.0.4 release for x64 Linux from 20 | * https://github.com/electron/electron/releases/download/v4.0.4/electron-v4.0.4-linux-x64.zip 21 | * 22 | * ```js 23 | * const opts = { 24 | * mirror: 'https://github.com/electron/electron/releases/download', 25 | * customDir: 'v4.0.4', 26 | * customFilename: 'electron-v4.0.4-linux-x64.zip', 27 | * } 28 | * ``` 29 | */ 30 | export interface MirrorOptions { 31 | /** 32 | * The mirror URL for [`electron-nightly`](https://npmjs.com/package/electron-nightly), 33 | * which lives in a separate npm package. 34 | */ 35 | nightlyMirror?: string; 36 | /** 37 | * The base URL of the mirror to download from. 38 | * e.g https://github.com/electron/electron/releases/download 39 | */ 40 | mirror?: string; 41 | /** 42 | * The name of the directory to download from, 43 | * often scoped by version number e.g 'v4.0.4' 44 | */ 45 | customDir?: string; 46 | /** 47 | * The name of the asset to download, 48 | * e.g 'electron-v4.0.4-linux-x64.zip' 49 | */ 50 | customFilename?: string; 51 | /** 52 | * The version of the asset to download, 53 | * e.g '4.0.4' 54 | */ 55 | customVersion?: string; 56 | /** 57 | * A function allowing customization of the url returned 58 | * from getArtifactRemoteURL(). 59 | */ 60 | resolveAssetURL?: (opts: DownloadOptions) => Promise; 61 | } 62 | 63 | /** 64 | * @category Download Artifact 65 | * @internal 66 | */ 67 | export interface ElectronDownloadRequest { 68 | /** 69 | * The version of Electron associated with the artifact. 70 | */ 71 | version: string; 72 | /** 73 | * The type of artifact. For example: 74 | * * `electron` 75 | * * `ffmpeg` 76 | */ 77 | artifactName: string; 78 | } 79 | 80 | export enum ElectronDownloadCacheMode { 81 | /** 82 | * Reads from the cache if present 83 | * Writes to the cache after fetch if not present 84 | */ 85 | ReadWrite, 86 | /** 87 | * Reads from the cache if present 88 | * Will **not** write back to the cache after fetching missing artifact 89 | */ 90 | ReadOnly, 91 | /** 92 | * Skips reading from the cache 93 | * Will write back into the cache, overwriting anything currently in the cache after fetch 94 | */ 95 | WriteOnly, 96 | /** 97 | * Bypasses the cache completely, neither reads from nor writes to the cache 98 | */ 99 | Bypass, 100 | } 101 | 102 | /** 103 | * @category Download Electron 104 | */ 105 | export interface ElectronDownloadRequestOptions { 106 | /** 107 | * When set to `true`, disables checking that the artifact download completed successfully 108 | * with the correct payload. 109 | * 110 | * @defaultValue `false` 111 | */ 112 | unsafelyDisableChecksums?: boolean; 113 | /** 114 | * Provides checksums for the artifact as strings. 115 | * Can be used if you already know the checksums of the Electron artifact 116 | * you are downloading and want to skip the checksum file download 117 | * without skipping the checksum validation. 118 | * 119 | * This should be an object whose keys are the file names of the artifacts and 120 | * the values are their respective SHA256 checksums. 121 | * 122 | * @example 123 | * ```json 124 | * { 125 | * "electron-v4.0.4-linux-x64.zip": "877617029f4c0f2b24f3805a1c3554ba166fda65c4e88df9480ae7b6ffa26a22" 126 | * } 127 | * ``` 128 | */ 129 | checksums?: Record; 130 | /** 131 | * The directory that caches Electron artifact downloads. 132 | * 133 | * @defaultValue The default value is dependent upon the host platform: 134 | * 135 | * * Linux: `$XDG_CACHE_HOME` or `~/.cache/electron/` 136 | * * MacOS: `~/Library/Caches/electron/` 137 | * * Windows: `%LOCALAPPDATA%/electron/Cache` or `~/AppData/Local/electron/Cache/` 138 | */ 139 | cacheRoot?: string; 140 | /** 141 | * Options passed to the downloader module. 142 | * 143 | * @see {@link GotDownloaderOptions} for options for the default {@link GotDownloader}. 144 | */ 145 | downloadOptions?: DownloadOptions; 146 | /** 147 | * Options related to specifying an artifact mirror. 148 | */ 149 | mirrorOptions?: MirrorOptions; 150 | /** 151 | * A custom {@link Downloader} class used to download artifacts. Defaults to the 152 | * built-in {@link GotDownloader}. 153 | */ 154 | downloader?: Downloader; 155 | /** 156 | * A temporary directory for downloads. 157 | * It is used before artifacts are put into cache. 158 | * 159 | * @defaultValue the OS default temporary directory via [`os.tmpdir()`](https://nodejs.org/api/os.html#ostmpdir) 160 | */ 161 | tempDirectory?: string; 162 | /** 163 | * Controls the cache read and write behavior. 164 | * 165 | * When set to either {@link ElectronDownloadCacheMode.ReadOnly | ReadOnly} or 166 | * {@link ElectronDownloadCacheMode.Bypass | Bypass}, the caller is responsible 167 | * for cleaning up the returned file path once they are done using it 168 | * (e.g. via `fs.remove(path.dirname(pathFromElectronGet))`). 169 | * 170 | * When set to either {@link ElectronDownloadCacheMode.WriteOnly | WriteOnly} or 171 | * {@link ElectronDownloadCacheMode.ReadWrite | ReadWrite} (the default), the caller 172 | * should not move or delete the file path that is returned as the path 173 | * points directly to the disk cache. 174 | * 175 | * @defaultValue {@link ElectronDownloadCacheMode.ReadWrite} 176 | */ 177 | cacheMode?: ElectronDownloadCacheMode; 178 | } 179 | 180 | /** 181 | * @category Download Artifact 182 | * @internal 183 | */ 184 | export type ElectronPlatformArtifactDetails = { 185 | /** 186 | * The target artifact platform. These are Node-style platform names, for example: 187 | * * `win32` 188 | * * `darwin` 189 | * * `linux` 190 | * 191 | * @see Node.js {@link https://nodejs.org/api/process.html#processplatform | process.platform} docs 192 | */ 193 | platform: string; 194 | /** 195 | * The target artifact architecture. These are Node-style architecture names, for example: 196 | * * `ia32` 197 | * * `x64` 198 | * * `armv7l` 199 | * 200 | * @see Node.js {@link https://nodejs.org/api/process.html#processarch | process.arch} docs 201 | */ 202 | arch: string; 203 | artifactSuffix?: string; 204 | isGeneric?: false; 205 | } & ElectronDownloadRequest & 206 | ElectronDownloadRequestOptions; 207 | 208 | /** 209 | * Options to download a generic (i.e. platform and architecture-agnostic) 210 | * Electron artifact. Contains all options from {@link ElectronDownloadRequestOptions}, 211 | * but specifies a `version` and `artifactName` for the artifact to download. 212 | * 213 | * @category Download Artifact 214 | * @interface 215 | */ 216 | export type ElectronGenericArtifactDetails = { 217 | isGeneric: true; 218 | } & ElectronDownloadRequest & 219 | ElectronDownloadRequestOptions; 220 | 221 | /** 222 | * @category Download Artifact 223 | * @internal 224 | */ 225 | export type ElectronArtifactDetails = 226 | | ElectronPlatformArtifactDetails 227 | | ElectronGenericArtifactDetails; 228 | 229 | /** 230 | * Options to download a platform and architecture-specific Electron artifact. 231 | * Contains all options from {@link ElectronDownloadRequestOptions}, but 232 | * specifies a `version` and `artifactName` for the artifact to download. 233 | * 234 | * If `platform` and `arch` are omitted, they will be inferred using the host 235 | * system platform and architecture. 236 | * 237 | * @category Download Artifact 238 | * @interface 239 | */ 240 | export type ElectronPlatformArtifactDetailsWithDefaults = Omit< 241 | ElectronPlatformArtifactDetails, 242 | 'platform' | 'arch' 243 | > & { 244 | /** 245 | * The target artifact platform. These are Node-style platform names, for example: 246 | * * `win32` 247 | * * `darwin` 248 | * * `linux` 249 | * 250 | * @see Node.js {@link https://nodejs.org/api/process.html#processplatform | process.platform} docs 251 | */ 252 | platform?: string; 253 | /** 254 | * The target artifact architecture. These are Node-style architecture names, for example: 255 | * * `ia32` 256 | * * `x64` 257 | * * `armv7l` 258 | * 259 | * @see Node.js {@link https://nodejs.org/api/process.html#processarch | process.arch} docs 260 | */ 261 | arch?: string; 262 | }; 263 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import childProcess from 'node:child_process'; 2 | import fs from 'graceful-fs'; 3 | import os from 'node:os'; 4 | import path from 'node:path'; 5 | 6 | import { 7 | ElectronDownloadCacheMode, 8 | ElectronGenericArtifactDetails, 9 | ElectronPlatformArtifactDetailsWithDefaults, 10 | } from './types.js'; 11 | 12 | async function useAndRemoveDirectory( 13 | directory: string, 14 | fn: (directory: string) => Promise, 15 | ): Promise { 16 | let result: T; 17 | try { 18 | result = await fn(directory); 19 | } finally { 20 | await fs.promises.rm(directory, { recursive: true, force: true }); 21 | } 22 | 23 | return result; 24 | } 25 | 26 | export async function mkdtemp(parentDirectory: string = os.tmpdir()): Promise { 27 | const tempDirectoryPrefix = 'electron-download-'; 28 | return await fs.promises.mkdtemp(path.resolve(parentDirectory, tempDirectoryPrefix)); 29 | } 30 | 31 | export enum TempDirCleanUpMode { 32 | CLEAN, 33 | ORPHAN, 34 | } 35 | 36 | export async function withTempDirectoryIn( 37 | parentDirectory: string = os.tmpdir(), 38 | fn: (directory: string) => Promise, 39 | cleanUp: TempDirCleanUpMode, 40 | ): Promise { 41 | const tempDirectory = await mkdtemp(parentDirectory); 42 | if (cleanUp === TempDirCleanUpMode.CLEAN) { 43 | return useAndRemoveDirectory(tempDirectory, fn); 44 | } else { 45 | return fn(tempDirectory); 46 | } 47 | } 48 | 49 | export async function withTempDirectory( 50 | fn: (directory: string) => Promise, 51 | cleanUp: TempDirCleanUpMode, 52 | ): Promise { 53 | return withTempDirectoryIn(undefined, fn, cleanUp); 54 | } 55 | 56 | export function normalizeVersion(version: string): string { 57 | if (!version.startsWith('v')) { 58 | return `v${version}`; 59 | } 60 | return version; 61 | } 62 | 63 | /** 64 | * Runs the `uname` command and returns the trimmed output. 65 | */ 66 | export function uname(): string { 67 | return childProcess.execSync('uname -m').toString().trim(); 68 | } 69 | 70 | /** 71 | * Generates an architecture name that would be used in an Electron or Node.js 72 | * download file name. 73 | */ 74 | export function getNodeArch(arch: string): string { 75 | if (arch === 'arm') { 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | switch ((process.config.variables as any).arm_version) { 78 | case '6': 79 | return uname(); 80 | case '7': 81 | default: 82 | return 'armv7l'; 83 | } 84 | } 85 | 86 | return arch; 87 | } 88 | 89 | /** 90 | * Generates an architecture name that would be used in an Electron or Node.js 91 | * download file name from the `process` module information. 92 | * 93 | * @category Utility 94 | */ 95 | export function getHostArch(): string { 96 | return getNodeArch(process.arch); 97 | } 98 | 99 | export function ensureIsTruthyString(obj: T, key: K): void { 100 | if (!obj[key] || typeof obj[key] !== 'string') { 101 | throw new Error(`Expected property "${String(key)}" to be provided as a string but it was not`); 102 | } 103 | } 104 | 105 | export function isOfficialLinuxIA32Download( 106 | platform: string, 107 | arch: string, 108 | version: string, 109 | mirrorOptions?: object, 110 | ): boolean { 111 | return ( 112 | platform === 'linux' && 113 | arch === 'ia32' && 114 | Number(version.slice(1).split('.')[0]) >= 4 && 115 | typeof mirrorOptions === 'undefined' 116 | ); 117 | } 118 | 119 | /** 120 | * Find the value of a environment variable which may or may not have the 121 | * prefix, in a case-insensitive manner. 122 | */ 123 | export function getEnv(prefix = ''): (name: string) => string | undefined { 124 | const envsLowerCase: NodeJS.ProcessEnv = {}; 125 | 126 | for (const envKey in process.env) { 127 | envsLowerCase[envKey.toLowerCase()] = process.env[envKey]; 128 | } 129 | 130 | return (name: string): string | undefined => { 131 | return ( 132 | envsLowerCase[`${prefix}${name}`.toLowerCase()] || 133 | envsLowerCase[name.toLowerCase()] || 134 | undefined 135 | ); 136 | }; 137 | } 138 | 139 | export function setEnv(key: string, value: string | undefined): void { 140 | // The `void` operator always returns `undefined`. 141 | // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void 142 | if (value !== void 0) { 143 | process.env[key] = value; 144 | } 145 | } 146 | 147 | export function effectiveCacheMode( 148 | artifactDetails: ElectronPlatformArtifactDetailsWithDefaults | ElectronGenericArtifactDetails, 149 | ): ElectronDownloadCacheMode { 150 | return artifactDetails.cacheMode || ElectronDownloadCacheMode.ReadWrite; 151 | } 152 | 153 | export function shouldTryReadCache(cacheMode: ElectronDownloadCacheMode): boolean { 154 | return ( 155 | cacheMode === ElectronDownloadCacheMode.ReadOnly || 156 | cacheMode === ElectronDownloadCacheMode.ReadWrite 157 | ); 158 | } 159 | 160 | export function shouldWriteCache(cacheMode: ElectronDownloadCacheMode): boolean { 161 | return ( 162 | cacheMode === ElectronDownloadCacheMode.WriteOnly || 163 | cacheMode === ElectronDownloadCacheMode.ReadWrite 164 | ); 165 | } 166 | 167 | export function doesCallerOwnTemporaryOutput(cacheMode: ElectronDownloadCacheMode): boolean { 168 | return ( 169 | cacheMode === ElectronDownloadCacheMode.Bypass || 170 | cacheMode === ElectronDownloadCacheMode.ReadOnly 171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /test/Cache.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'graceful-fs'; 2 | import os from 'node:os'; 3 | import path from 'node:path'; 4 | import util from 'node:util'; 5 | 6 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 7 | 8 | import { Cache } from '../src/Cache'; 9 | 10 | describe('Cache', () => { 11 | let cacheDir: string; 12 | let cache: Cache; 13 | 14 | const dummyUrl = 'dummy://dummypath'; 15 | const sanitizedDummyUrl = '0c57d948bd4829db99d75c3b4a5d6836c37bc335f38012981baf5d1193b5a612'; 16 | 17 | beforeEach(async () => { 18 | cacheDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-download-spec-')); 19 | cache = new Cache(cacheDir); 20 | }); 21 | 22 | afterEach(() => fs.promises.rm(cacheDir, { recursive: true, force: true })); 23 | 24 | describe('getCachePath()', () => { 25 | it('should strip the hash and query params off the url', async () => { 26 | const firstUrl = 'https://example.com?foo=1'; 27 | const secondUrl = 'https://example.com?foo=2'; 28 | const assetName = 'electron-v7.2.4-darwin-x64.zip-v7.2.4-darwin-x64.zip'; 29 | 30 | expect(cache.getCachePath(firstUrl, assetName)).toEqual( 31 | cache.getCachePath(secondUrl, assetName), 32 | ); 33 | }); 34 | }); 35 | 36 | describe('getPathForFileInCache()', () => { 37 | it('should return null for a file not in the cache', async () => { 38 | expect(cache.getPathForFileInCache(dummyUrl, 'test.txt')).toBeNull(); 39 | }); 40 | 41 | it('should return an absolute path for a file in the cache', async () => { 42 | const cacheFolder = path.resolve(cacheDir, sanitizedDummyUrl); 43 | await fs.promises.mkdir(cacheFolder, { recursive: true }); 44 | const cachePath = path.resolve(cacheFolder, 'test.txt'); 45 | await util.promisify(fs.writeFile)(cachePath, 'dummy data'); 46 | expect(cache.getPathForFileInCache(dummyUrl, 'test.txt')).toEqual(cachePath); 47 | }); 48 | }); 49 | 50 | describe('putFileInCache()', () => { 51 | it('should throw an error if the provided file path does not exist', async () => { 52 | const fakePath = path.resolve(__dirname, 'fake.file'); 53 | await expect(cache.putFileInCache(dummyUrl, fakePath, 'fake.file')).rejects.toHaveProperty( 54 | 'message', 55 | expect.stringContaining(`ENOENT: no such file or directory, rename '${fakePath}'`), 56 | ); 57 | }); 58 | 59 | it('should delete the original file', async () => { 60 | const originalFolder = path.resolve(cacheDir, sanitizedDummyUrl); 61 | await fs.promises.mkdir(originalFolder, { recursive: true }); 62 | const originalPath = path.resolve(originalFolder, 'original.txt'); 63 | await util.promisify(fs.writeFile)(originalPath, 'dummy data'); 64 | await cache.putFileInCache(dummyUrl, originalPath, 'test.txt'); 65 | expect(fs.existsSync(originalPath)).toEqual(false); 66 | }); 67 | 68 | it('should create a new file in the cache with exactly the same content', async () => { 69 | const originalFolder = path.resolve(cacheDir, sanitizedDummyUrl); 70 | await fs.promises.mkdir(originalFolder, { recursive: true }); 71 | const originalPath = path.resolve(originalFolder, 'original.txt'); 72 | await util.promisify(fs.writeFile)(originalPath, 'example content'); 73 | const cachePath = await cache.putFileInCache(dummyUrl, originalPath, 'test.txt'); 74 | expect(cachePath.startsWith(cacheDir)).toEqual(true); 75 | expect(await util.promisify(fs.readFile)(cachePath, 'utf8')).toEqual('example content'); 76 | }); 77 | 78 | it('should overwrite the file if it already exists in cache', async () => { 79 | const originalFolder = path.resolve(cacheDir, sanitizedDummyUrl); 80 | await fs.promises.mkdir(originalFolder, { recursive: true }); 81 | const originalPath = path.resolve(cacheDir, 'original.txt'); 82 | await util.promisify(fs.writeFile)(originalPath, 'example content'); 83 | await util.promisify(fs.writeFile)( 84 | path.resolve(cacheDir, sanitizedDummyUrl, 'test.txt'), 85 | 'bad content', 86 | ); 87 | const cachePath = await cache.putFileInCache(dummyUrl, originalPath, 'test.txt'); 88 | expect(cachePath.startsWith(cacheDir)).toEqual(true); 89 | expect(await util.promisify(fs.readFile)(cachePath, 'utf8')).toEqual('example content'); 90 | }); 91 | 92 | it('can handle cross-device cache paths', async () => { 93 | const error: NodeJS.ErrnoException = new Error('EXDEV: cross-device link not permitted'); 94 | error.code = 'EXDEV'; 95 | 96 | const spy = vi.spyOn(fs.promises, 'rename').mockRejectedValueOnce(error); 97 | 98 | try { 99 | const originalFolder = path.resolve(cacheDir, sanitizedDummyUrl); 100 | await fs.promises.mkdir(originalFolder, { recursive: true }); 101 | const originalPath = path.resolve(originalFolder, 'original.txt'); 102 | await util.promisify(fs.writeFile)(originalPath, 'example content'); 103 | const cachePath = await cache.putFileInCache(dummyUrl, originalPath, 'test.txt'); 104 | expect(cachePath.startsWith(cacheDir)).toEqual(true); 105 | expect(await util.promisify(fs.readFile)(cachePath, 'utf8')).toEqual('example content'); 106 | expect(spy).toHaveBeenCalled(); 107 | } finally { 108 | spy.mockRestore(); 109 | } 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/FixtureDownloader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'graceful-fs'; 2 | import path from 'node:path'; 3 | import util from 'node:util'; 4 | 5 | import { DownloadOptions } from '../src/types'; 6 | import { Downloader } from '../src/Downloader'; 7 | 8 | const FIXTURE_DIR = path.resolve(__dirname, '../test/fixtures'); 9 | 10 | export class FixtureDownloader implements Downloader { 11 | async download(_url: string, targetFilePath: string): Promise { 12 | await fs.promises.mkdir(path.dirname(targetFilePath), { recursive: true }); 13 | const fixtureFile = path.join(FIXTURE_DIR, path.basename(targetFilePath)); 14 | if (!fs.existsSync(fixtureFile)) { 15 | throw new Error(`Cannot find the fixture '${fixtureFile}'`); 16 | } 17 | 18 | await util.promisify(fs.copyFile)(fixtureFile, targetFilePath); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/GotDownloader.network.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'graceful-fs'; 2 | import path from 'node:path'; 3 | import util from 'node:util'; 4 | 5 | import { describe, expect, it, vi } from 'vitest'; 6 | 7 | import { GotDownloader } from '../src/GotDownloader'; 8 | import { TempDirCleanUpMode, withTempDirectory } from '../src/utils'; 9 | import { PathLike } from 'node:fs'; 10 | 11 | describe('GotDownloader', () => { 12 | describe('download()', () => { 13 | it('should download a remote file to the given file path', async () => { 14 | const downloader = new GotDownloader(); 15 | let progressCallbackCalled = false; 16 | await withTempDirectory(async (dir) => { 17 | const testFile = path.resolve(dir, 'test.txt'); 18 | expect(fs.existsSync(testFile)).toEqual(false); 19 | await downloader.download( 20 | 'https://github.com/electron/electron/releases/download/v2.0.18/SHASUMS256.txt', 21 | testFile, 22 | { 23 | getProgressCallback: (/* progress: Progress */) => { 24 | progressCallbackCalled = true; 25 | return Promise.resolve(); 26 | }, 27 | }, 28 | ); 29 | expect(fs.existsSync(testFile)).toEqual(true); 30 | expect(await util.promisify(fs.readFile)(testFile, 'utf8')).toMatchSnapshot(); 31 | expect(progressCallbackCalled).toEqual(true); 32 | }, TempDirCleanUpMode.CLEAN); 33 | }); 34 | 35 | it('should throw an error if the file does not exist', async () => { 36 | const downloader = new GotDownloader(); 37 | await withTempDirectory(async (dir) => { 38 | const testFile = path.resolve(dir, 'test.txt'); 39 | const url = 'https://github.com/electron/electron/releases/download/v2.0.18/bad.file'; 40 | const snapshot = `[HTTPError: Response code 404 (Not Found) for ${url}]`; 41 | await expect(downloader.download(url, testFile)).rejects.toMatchInlineSnapshot(snapshot); 42 | }, TempDirCleanUpMode.CLEAN); 43 | }); 44 | 45 | it('should throw an error if the file write stream fails', async () => { 46 | const downloader = new GotDownloader(); 47 | const createWriteStream = fs.createWriteStream; 48 | const spy = vi.spyOn(fs, 'createWriteStream'); 49 | spy.mockImplementationOnce((path: PathLike) => { 50 | const stream = createWriteStream(path); 51 | setTimeout(() => stream.emit('error', 'bad write error thing'), 0); 52 | return stream; 53 | }); 54 | await withTempDirectory(async (dir) => { 55 | const testFile = path.resolve(dir, 'test.txt'); 56 | await expect( 57 | downloader.download( 58 | 'https://github.com/electron/electron/releases/download/v2.0.18/SHASUMS256.txt', 59 | testFile, 60 | ), 61 | ).rejects.toMatchInlineSnapshot(`"bad write error thing"`); 62 | }, TempDirCleanUpMode.CLEAN); 63 | }); 64 | 65 | it('should download to a deep uncreated path', async () => { 66 | const downloader = new GotDownloader(); 67 | await withTempDirectory(async (dir) => { 68 | const testFile = path.resolve(dir, 'f', 'b', 'test.txt'); 69 | expect(fs.existsSync(testFile)).toEqual(false); 70 | await expect( 71 | downloader.download( 72 | 'https://github.com/electron/electron/releases/download/v2.0.1/SHASUMS256.txt', 73 | testFile, 74 | ), 75 | ).resolves.toBeUndefined(); 76 | expect(fs.existsSync(testFile)).toEqual(true); 77 | expect(await util.promisify(fs.readFile)(testFile, 'utf8')).toMatchSnapshot(); 78 | }, TempDirCleanUpMode.CLEAN); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/__snapshots__/GotDownloader.network.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`GotDownloader > download() > should download a remote file to the given file path 1`] = ` 4 | "d186456c8e44155298521aafc5e270875c6cde41bbd3c19ce38ae53f1d98f284 *chromedriver-v2.0.18-darwin-x64.zip 5 | dfb244c4347481a526567418f78a0fb6b9ccff6205a55f51225cfe4d1796b0d9 *chromedriver-v2.0.18-linux-arm.zip 6 | 516981ca6f7465dba55228df9ba30b7e0566a3eec694e5a2cc2fbe58a41ff006 *chromedriver-v2.0.18-linux-arm64.zip 7 | dfb244c4347481a526567418f78a0fb6b9ccff6205a55f51225cfe4d1796b0d9 *chromedriver-v2.0.18-linux-armv7l.zip 8 | 6750ebb6f48e2ba104d33f8f407818d8316bcc438590bb04a2dfb03cd069f2f4 *chromedriver-v2.0.18-linux-ia32.zip 9 | 37140f6ec7d333dcd559c85815d547ef7a0046272f37fce0f78c308032779edc *chromedriver-v2.0.18-linux-x64.zip 10 | 081baba2d4c47f940ed19b101a53d9a576d7519cc9db8d15d1180190ce86b65c *chromedriver-v2.0.18-mas-x64.zip 11 | 794370f5ee270b83682a91c658092a3b3a71dbebfded9f48159dc162d32ad64b *chromedriver-v2.0.18-win32-ia32.zip 12 | 9c5ec68bb6e92ef6c61ffaf139d651e7760d79dc457b91fefa4b817f13fd2a9b *chromedriver-v2.0.18-win32-x64.zip 13 | db086eeb6619cbf61e673f3bcd69a24df04e4b98f4cf458cf76e1f02eb2b7ee8 *electron-api.json 14 | dbd50c1fd4bad524768243a3817bffc15a400779838099fb229d4861d3dba02b *electron-v2.0.18-darwin-x64-dsym.zip 15 | 3a829d3e0adc6994bdb27d818a9e9a8e404e2e3d5d4811db2f1a0c52f6a70742 *electron-v2.0.18-darwin-x64-symbols.zip 16 | 2055e52fc3a336c9c37dc0a03ca8559d62a4288bdf4160aed875e9dc57a0a2da *electron-v2.0.18-darwin-x64.zip 17 | 9d47382091dd917ade29d36af3e7a7ce9abcf1f76457c228ab8b4c59c7a02f2d *electron-v2.0.18-linux-arm-symbols.zip 18 | 75c37301e958368263cee27539bc80ab9bcf810a83f3e3dd6367f6646866b9fb *electron-v2.0.18-linux-arm.zip 19 | 7b3cca296b6f77b5c53488eca17ccda072ec09259431373067ffc026db11f4c4 *electron-v2.0.18-linux-arm64-symbols.zip 20 | dfcfb60fc57dcec41a25cebbea37c0486a0ffd690ac3ccbb80a97161851df81f *electron-v2.0.18-linux-arm64.zip 21 | 9d47382091dd917ade29d36af3e7a7ce9abcf1f76457c228ab8b4c59c7a02f2d *electron-v2.0.18-linux-armv7l-symbols.zip 22 | 75c37301e958368263cee27539bc80ab9bcf810a83f3e3dd6367f6646866b9fb *electron-v2.0.18-linux-armv7l.zip 23 | 16f738a1a85eb0f3f916e52130300e37e88177e9da57b5bf32766f2ff1114773 *electron-v2.0.18-linux-ia32-symbols.zip 24 | 05bbd3e73776143ac357b0c25a631bf2e503a922b9506b23d636bd0b06a3f2f6 *electron-v2.0.18-linux-ia32.zip 25 | 1441f7cea2d3d1964f3ead1c7fd7020e83dd9d06bd716ef3a527011ecf0f39ba *electron-v2.0.18-linux-x64-symbols.zip 26 | f196e06b6ecfa33bffb02b3d6c4a64bd5a076014e2f21c4a67356474ee014000 *electron-v2.0.18-linux-x64.zip 27 | c06dd8931e0583c6989369924edc4c115050086a1fb41f4741aff186cf979955 *electron-v2.0.18-mas-x64-dsym.zip 28 | b2f177134f11481c7ee4fd7fdcf027b699199d3b4c20b04bc3e31ac37937c2a9 *electron-v2.0.18-mas-x64-symbols.zip 29 | 25c20dbfcdab16e1ec3bd180bf845470ae0f4b784b4ef2e6dbba77fc5735258c *electron-v2.0.18-mas-x64.zip 30 | 03b82b19205ef6341b7f5fd6c7e90adb70c3e6207b83add00018b598602b3a2b *electron-v2.0.18-win32-ia32-pdb.zip 31 | 81f424eb0f318476a4fc878329f38c98666677b7cc507d27c54a11b61cf8580a *electron-v2.0.18-win32-ia32-symbols.zip 32 | 37fdb486fa3f0aea2678c4159f797176b02e15c50e81fc059e542fbd1f3ff0d7 *electron-v2.0.18-win32-ia32.zip 33 | af2d527148be51f11503112b63546419b8cd15f9e59fd24bc3e758fb15b7cc31 *electron-v2.0.18-win32-x64-pdb.zip 34 | 09c6ed3d4a52c176dcdd8ebdaef25563575809fddec1b8243675cde2b731c39a *electron-v2.0.18-win32-x64-symbols.zip 35 | 904884aca1dec4c036243dfd3cffb04df62d1cdd290323551476729f41db50fc *electron-v2.0.18-win32-x64.zip 36 | 80c89e7028a4db7fd028050bac19bd90bdef336b7dc56eeb9d105ca83450689f *electron.d.ts 37 | 0bcff2dfa28da5dc5924e990daf9e22ac243bc58b4fe7a6225beba6a6da276f3 *ffmpeg-v2.0.18-darwin-x64.zip 38 | 5cce7303e185602857292c216d00d5726e63c50f36c1527238b85b9e80713d2f *ffmpeg-v2.0.18-linux-arm.zip 39 | e9acaac5dce3f6eae23abb89121f8bafaed7595d8678700c0975643929b8dd4b *ffmpeg-v2.0.18-linux-arm64.zip 40 | 5cce7303e185602857292c216d00d5726e63c50f36c1527238b85b9e80713d2f *ffmpeg-v2.0.18-linux-armv7l.zip 41 | 32aa39326628207ddf5f0889b73f130e8e7c6c66a308e9a45156a7dc09045e60 *ffmpeg-v2.0.18-linux-ia32.zip 42 | 550c628a55670ceff174ff39826272c9501560ec7fa15eb4e0779efc26a00727 *ffmpeg-v2.0.18-linux-x64.zip 43 | a577d484f257aa0e66f65a1f56b5e95cd3e5a7a97cb1a21a08a82fe8bb75dbc9 *ffmpeg-v2.0.18-mas-x64.zip 44 | af0236e9443833c91816901737cd752b5ac03848b986d805c89ffdc1fc591585 *ffmpeg-v2.0.18-win32-ia32.zip 45 | 047b2db1c178cf76b161eb320b58e038bc5435927b0abb4ce425af999947ac7a *ffmpeg-v2.0.18-win32-x64.zip 46 | d09a05aed8fd4ac7a91c9be250f66cb8bf8f1c22243f93f1bbbcc09b7b4edf50 *mksnapshot-v2.0.18-darwin-x64.zip 47 | d2a8120b4da737a507d6b4a9c2cc1a53c32c89b7ea606cd68effc76b8356830a *mksnapshot-v2.0.18-linux-arm-x64.zip 48 | 788431928a32098f1a83d2b89c7863fbc33bb0ee02d34f4eaf08098a33600864 *mksnapshot-v2.0.18-linux-arm.zip 49 | 2fbd5f590d038dcff7980a0954cdda4bb4f580bc318f6af7dd58b91471f4424a *mksnapshot-v2.0.18-linux-arm64-x64.zip 50 | 18a20f3ef09e701266474728a7d2a17fb2edff0170eaaf757097a4d7d47b0715 *mksnapshot-v2.0.18-linux-arm64.zip 51 | d2a8120b4da737a507d6b4a9c2cc1a53c32c89b7ea606cd68effc76b8356830a *mksnapshot-v2.0.18-linux-armv7l-x64.zip 52 | 788431928a32098f1a83d2b89c7863fbc33bb0ee02d34f4eaf08098a33600864 *mksnapshot-v2.0.18-linux-armv7l.zip 53 | fed7fddc94c27d3232c63e76ea7b1248618712708fa9936cf492b4147227d739 *mksnapshot-v2.0.18-linux-ia32.zip 54 | f83f0433d0e2f4d62db59892d4105c5693242da867cb76f3ec42ce020868bf98 *mksnapshot-v2.0.18-linux-x64.zip 55 | 68255b3aecd3f743a07338b6fc964d6e5fcd9ad37cdbd696b5ebcf7d57f27fd2 *mksnapshot-v2.0.18-mas-x64.zip 56 | 1dae0fc78ea703032833c0a2d535155ffbfd0b24ec5b894aa0e8aff2bcd9a1a8 *mksnapshot-v2.0.18-win32-ia32.zip 57 | 7cb445597f95c202a480b737b0df1d39eab044e37e090484494616017eb36a22 *mksnapshot-v2.0.18-win32-x64.zip 58 | " 59 | `; 60 | 61 | exports[`GotDownloader > download() > should download to a deep uncreated path 1`] = ` 62 | "06fbdc5cdebf63b6fc579092d572878304125286b0db3ed0d6b1d506dc75e286 *chromedriver-v2.0.1-darwin-x64.zip 63 | 7347204e847fd7403dfac7de0a92a345932cc99a60b3279ca9e6d307483a190a *chromedriver-v2.0.1-linux-arm.zip 64 | 0a5807a4f26bc42de70d008e6ec775fed54a66f55d517b68071b801c3b306b4c *chromedriver-v2.0.1-linux-arm64.zip 65 | 7347204e847fd7403dfac7de0a92a345932cc99a60b3279ca9e6d307483a190a *chromedriver-v2.0.1-linux-armv7l.zip 66 | c9060d1c384bcd3dcec43b56f43ef1f6668c789833d93175a85d9d4d79c176fa *chromedriver-v2.0.1-linux-ia32.zip 67 | 522d870210e453a1856ad2eb5db185749a8738e0ad63f6bbe2f0d03a4974fe6a *chromedriver-v2.0.1-linux-x64.zip 68 | 813db89ca0a8c449d1cc3571bbeb7dbef03e277620a5841c75fb816e1d300f6e *chromedriver-v2.0.1-mas-x64.zip 69 | 8b561b17b0917a50fa15a0c67271d93a899f5d60c9229bec59ddb94492c56f8c *chromedriver-v2.0.1-win32-ia32.zip 70 | 7c09430f50e7e578cf76cd355b47e74d031215e97d3d7b73135b1fd0601d52fa *chromedriver-v2.0.1-win32-x64.zip 71 | 1ffc7c7a51aae73783e6c7f8112fb1746033de47957ac672831a98f267cee727 *electron-api.json 72 | 0519c4d413be3dd5a482d4a02aef6025edcbe1fc55ce9bd26e4d0d688be10db1 *electron-v2.0.1-darwin-x64-dsym.zip 73 | d6a87486f36dac598487b73ede4c4d8b257f87e4b9d69e50d1cc36b5a2f07eb2 *electron-v2.0.1-darwin-x64-symbols.zip 74 | 93037deeb1f0c4fe90913e8c4360c323badfc223d9c00d8a4e28dc9a474c60d2 *electron-v2.0.1-darwin-x64.zip 75 | fc06151c32d221ec65a404a56e0a6caac9790fdf63d5422f3fe6c268877ff106 *electron-v2.0.1-linux-arm-symbols.zip 76 | c31d48bc4a8a845c5edc300aad2c4114f174b5b828b8d5093850b62f5b86d0c1 *electron-v2.0.1-linux-arm.zip 77 | e74e3465c925339d19ce7448fce0040c06efe3fba386583a77a959d6f48f3e5a *electron-v2.0.1-linux-arm64-symbols.zip 78 | a61fb0a1f4cdb1126a3f43d90fbfb40c80c263f0091f548611fd91f0a22ab19b *electron-v2.0.1-linux-arm64.zip 79 | fc06151c32d221ec65a404a56e0a6caac9790fdf63d5422f3fe6c268877ff106 *electron-v2.0.1-linux-armv7l-symbols.zip 80 | c31d48bc4a8a845c5edc300aad2c4114f174b5b828b8d5093850b62f5b86d0c1 *electron-v2.0.1-linux-armv7l.zip 81 | db2637d6cedd278e7f890bb6fffdeba630eab59726edbc072a0af64ff5df5019 *electron-v2.0.1-linux-ia32-symbols.zip 82 | c6bfaaee82cbddf490288bb3dc3ac1e26be8c363899508027159411d818fa681 *electron-v2.0.1-linux-ia32.zip 83 | 76c1273ed5e2a2e4b0eb0de37c4e22a75106df5b683bc1581f554d632bade9ba *electron-v2.0.1-linux-x64-symbols.zip 84 | c95e44ff13eb86bde49e909d64972794e23e124d00060d0e3516174448146049 *electron-v2.0.1-linux-x64.zip 85 | b06e6c864c2a81e848d3f92222b56a451c4c1ce5759b792df8ba944cd769c6a9 *electron-v2.0.1-mas-x64-dsym.zip 86 | 815122b13828b07bffe7eedf0afc6f8aca71d03bc712e61b61689d56de1407e5 *electron-v2.0.1-mas-x64-symbols.zip 87 | f31012bed957b688e3b74ed533bafe24622fab6435e1b4c17a90943fc4acf940 *electron-v2.0.1-mas-x64.zip 88 | 6856affa65de4ee43b6f1daec1d525ab8743807a6ac6224084597658f730281a *electron-v2.0.1-win32-ia32-pdb.zip 89 | d5ac8f04f71b80ab9c0ac84bbed04622e5393aa44d85e29d8af83e224619ef53 *electron-v2.0.1-win32-ia32-symbols.zip 90 | dac3116900550f72b92192ba3191b6b62968307d635167fd16f4ca0cb6df167e *electron-v2.0.1-win32-ia32.zip 91 | b8501a49f573977a398d3133f46bebeff5b4e3727c1c708465c775023f268849 *electron-v2.0.1-win32-x64-pdb.zip 92 | d3e4136a4db1c58c47fdb8cc3e5ea731cd48f60c372b43a216ead9aaf2c11324 *electron-v2.0.1-win32-x64-symbols.zip 93 | d754696b75e75b54b18d08bf70ba4bd5b04c3330a44c4ddc371b8e39ad51e180 *electron-v2.0.1-win32-x64.zip 94 | d7dff9f5df46dd0459a2203b2ce043f37c70369eec122b81631d5f49316a8e2b *electron.d.ts 95 | 8778c206570b68455a79844b569ec3a6017387d479bea9329672224f9c78b410 *ffmpeg-v2.0.1-darwin-x64.zip 96 | aebfa31700b637594aaebd18cfb810953998195bfc1b0b6f3e55e7712699afd6 *ffmpeg-v2.0.1-linux-arm.zip 97 | c41546e75b4e23526d4eb040ebe2d24620dc9feacbc2f1999da327e7acd0a0b2 *ffmpeg-v2.0.1-linux-arm64.zip 98 | aebfa31700b637594aaebd18cfb810953998195bfc1b0b6f3e55e7712699afd6 *ffmpeg-v2.0.1-linux-armv7l.zip 99 | 9d4b233d7fbf7d49ba2b85eb5f7e86ff2487616d27388ac84673f24e20aae275 *ffmpeg-v2.0.1-linux-ia32.zip 100 | 711c36c0a845b7e5901580f5b6ddbef21082faaa03f103bd423787dcab74347f *ffmpeg-v2.0.1-linux-x64.zip 101 | 05536cb36883a9eb10d112e18b201b09d097274d88c41d44fa54a5cbd5f3c638 *ffmpeg-v2.0.1-mas-x64.zip 102 | 9f49ad8e266677bd7c8a077967900d3b01fd88e4a438f13eb04e8dc24769ac8a *ffmpeg-v2.0.1-win32-ia32.zip 103 | 4c17b21f05e85a30e7d4e7e7d25471caeacde33b6300cbb08141d02fd827d7b5 *ffmpeg-v2.0.1-win32-x64.zip 104 | 34fb1abb38aa286a95ae74ce83dc48c71ddd83d2fdcc2201bdc6890abfb175ee *mksnapshot-v2.0.1-darwin-x64.zip 105 | ad510fb748dcdf8b2b189f220a8720bd358580c27e8a80cec6377bd56f63faac *mksnapshot-v2.0.1-linux-arm-x64.zip 106 | 6f4e3bb996ab9355914bc96aa3fc69afd2c5145525006898fbae633b94206ac1 *mksnapshot-v2.0.1-linux-arm.zip 107 | dc96949c678732e25212aea2e784f31a75d4f03f1e1fdede6122a27412b7d3e8 *mksnapshot-v2.0.1-linux-arm64-x64.zip 108 | dae0d49148910b07a563e01408611f767e50c1d979dbc77b67426be429c6e9fd *mksnapshot-v2.0.1-linux-arm64.zip 109 | ad510fb748dcdf8b2b189f220a8720bd358580c27e8a80cec6377bd56f63faac *mksnapshot-v2.0.1-linux-armv7l-x64.zip 110 | 6f4e3bb996ab9355914bc96aa3fc69afd2c5145525006898fbae633b94206ac1 *mksnapshot-v2.0.1-linux-armv7l.zip 111 | ef075a31c85c41fb1ab1628a8f811f90354769652b17f0e01d40fa6e0678b558 *mksnapshot-v2.0.1-linux-ia32.zip 112 | d555686b04db329b67d2cf133ff83e4d566e0f2a99388bdf3eb2d0b73b5d34bd *mksnapshot-v2.0.1-linux-x64.zip 113 | 5e6799a4516044d7d77988b9668e27d5c00516ff87dd382b7a9bdf9501e7b7ee *mksnapshot-v2.0.1-mas-x64.zip 114 | 4de5336d82e67b0db803b9273ff617637248a67b0980a4d6263b4945ccf56557 *mksnapshot-v2.0.1-win32-ia32.zip 115 | 964de300a37438cd5259a7de494fdee07d3faec0b694b438d27519b6677d9961 *mksnapshot-v2.0.1-win32-x64.zip 116 | " 117 | `; 118 | -------------------------------------------------------------------------------- /test/__snapshots__/index.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Public API > download() > should accept a custom downloader 1`] = `"https://github.com/electron/electron/releases/download/v2.0.3/electron-v2.0.3-platform-arch.zip"`; 4 | -------------------------------------------------------------------------------- /test/artifact-utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { getArtifactFileName, getArtifactRemoteURL } from '../src/artifact-utils'; 4 | 5 | describe('artifact-utils', () => { 6 | describe('getArtifactFileName()', () => { 7 | it('should return just the artifact name for generic artifacts', () => { 8 | expect( 9 | getArtifactFileName({ 10 | isGeneric: true, 11 | artifactName: 'test.zip', 12 | version: 'v1', 13 | }), 14 | ).toMatchInlineSnapshot(`"test.zip"`); 15 | }); 16 | 17 | it('should return the correct hypenated artifact names for other artifacts', () => { 18 | expect( 19 | getArtifactFileName({ 20 | isGeneric: false, 21 | artifactName: 'chromedriver', 22 | version: 'v1.0.1', 23 | platform: 'android', 24 | arch: 'ia32', 25 | }), 26 | ).toMatchInlineSnapshot(`"chromedriver-v1.0.1-android-ia32.zip"`); 27 | }); 28 | 29 | it('should return the correct hypenated artifact names for artifacts with a suffix', () => { 30 | expect( 31 | getArtifactFileName({ 32 | isGeneric: false, 33 | artifactName: 'electron', 34 | version: 'v1.0.1', 35 | platform: 'darwin', 36 | arch: 'x64', 37 | artifactSuffix: 'symbols', 38 | }), 39 | ).toMatchInlineSnapshot(`"electron-v1.0.1-darwin-x64-symbols.zip"`); 40 | }); 41 | }); 42 | 43 | describe('getArtifactRemoteURL', () => { 44 | it('should generate a default URL correctly', async () => { 45 | expect( 46 | await getArtifactRemoteURL({ 47 | arch: 'x64', 48 | artifactName: 'electron', 49 | platform: 'linux', 50 | version: 'v6.0.0', 51 | }), 52 | ).toMatchInlineSnapshot( 53 | `"https://github.com/electron/electron/releases/download/v6.0.0/electron-v6.0.0-linux-x64.zip"`, 54 | ); 55 | }); 56 | 57 | it('should replace the base URL when mirrorOptions.mirror is set', async () => { 58 | expect( 59 | await getArtifactRemoteURL({ 60 | arch: 'x64', 61 | artifactName: 'electron', 62 | mirrorOptions: { 63 | mirror: 'https://mirror.example.com/', 64 | }, 65 | platform: 'linux', 66 | version: 'v6.0.0', 67 | }), 68 | ).toMatchInlineSnapshot(`"https://mirror.example.com/v6.0.0/electron-v6.0.0-linux-x64.zip"`); 69 | }); 70 | 71 | it('should allow for custom URL resolution with mirrorOptions.resolveAssetURL', async () => { 72 | expect( 73 | await getArtifactRemoteURL({ 74 | arch: 'x64', 75 | artifactName: 'electron', 76 | mirrorOptions: { 77 | mirror: 'https://mirror.example.com', 78 | customDir: 'v1.2.3', 79 | customFilename: 'custom-built-electron.zip', 80 | resolveAssetURL: (opts): Promise => { 81 | return opts.mirrorOptions.mirror || ''; 82 | }, 83 | }, 84 | platform: 'linux', 85 | version: 'v6.0.0', 86 | }), 87 | ).toMatchInlineSnapshot(`"https://mirror.example.com"`); 88 | }); 89 | 90 | it('should replace the nightly base URL when mirrorOptions.nightlyMirror is set', async () => { 91 | expect( 92 | await getArtifactRemoteURL({ 93 | arch: 'x64', 94 | artifactName: 'electron', 95 | mirrorOptions: { 96 | mirror: 'https://mirror.example.com/', 97 | nightlyMirror: 'https://nightly.example.com/', 98 | }, 99 | platform: 'linux', 100 | version: 'v6.0.0-nightly', 101 | }), 102 | ).toMatchInlineSnapshot( 103 | `"https://nightly.example.com/v6.0.0-nightly/electron-v6.0.0-nightly-linux-x64.zip"`, 104 | ); 105 | }); 106 | 107 | it('should replace the nightly base URL when mirrorOptions.nightly_mirror is set', async () => { 108 | expect( 109 | await getArtifactRemoteURL({ 110 | arch: 'x64', 111 | artifactName: 'electron', 112 | mirrorOptions: { 113 | mirror: 'https://mirror.example.com/', 114 | nightlyMirror: 'https://nightly.example.com/', 115 | }, 116 | platform: 'linux', 117 | version: 'v6.0.0-nightly', 118 | }), 119 | ).toMatchInlineSnapshot( 120 | `"https://nightly.example.com/v6.0.0-nightly/electron-v6.0.0-nightly-linux-x64.zip"`, 121 | ); 122 | }); 123 | 124 | it('should replace the version dir when mirrorOptions.customDir is set', async () => { 125 | expect( 126 | await getArtifactRemoteURL({ 127 | arch: 'x64', 128 | artifactName: 'electron', 129 | mirrorOptions: { 130 | customDir: 'all', 131 | }, 132 | platform: 'linux', 133 | version: 'v6.0.0', 134 | }), 135 | ).toMatchInlineSnapshot( 136 | `"https://github.com/electron/electron/releases/download/all/electron-v6.0.0-linux-x64.zip"`, 137 | ); 138 | }); 139 | 140 | it('should replace {{ version }} when mirrorOptions.customDir is set', async () => { 141 | expect( 142 | await getArtifactRemoteURL({ 143 | arch: 'x64', 144 | artifactName: 'electron', 145 | mirrorOptions: { 146 | customDir: 'foo{{ version }}bar', 147 | }, 148 | platform: 'linux', 149 | version: 'v1.2.3', 150 | }), 151 | ).toMatchInlineSnapshot( 152 | `"https://github.com/electron/electron/releases/download/foo1.2.3bar/electron-v1.2.3-linux-x64.zip"`, 153 | ); 154 | }); 155 | 156 | it('should replace the filename when mirrorOptions.customFilename is set', async () => { 157 | expect( 158 | await getArtifactRemoteURL({ 159 | arch: 'x64', 160 | artifactName: 'electron', 161 | mirrorOptions: { 162 | customFilename: 'custom-built-electron.zip', 163 | }, 164 | platform: 'linux', 165 | version: 'v6.0.0', 166 | }), 167 | ).toMatchInlineSnapshot( 168 | `"https://github.com/electron/electron/releases/download/v6.0.0/custom-built-electron.zip"`, 169 | ); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /test/checksums.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'graceful-fs'; 2 | import os from 'node:os'; 3 | import path from 'node:path'; 4 | 5 | import { afterEach, beforeEach, describe, expect, it } from 'vitest'; 6 | 7 | import { FixtureDownloader } from './FixtureDownloader'; 8 | import { downloadArtifact } from '../src'; 9 | 10 | describe('Hard-coded checksums', () => { 11 | const downloader = new FixtureDownloader(); 12 | 13 | let cacheRoot: string; 14 | beforeEach(async () => { 15 | cacheRoot = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-download-spec-')); 16 | }); 17 | 18 | afterEach(async () => { 19 | await fs.promises.rm(cacheRoot, { recursive: true, force: true }); 20 | }); 21 | 22 | describe('download()', () => { 23 | it('should succeed with valid checksums', async () => { 24 | const zipPath = await downloadArtifact({ 25 | cacheRoot, 26 | downloader, 27 | platform: 'darwin', 28 | arch: 'x64', 29 | version: '2.0.9', 30 | artifactName: 'electron', 31 | checksums: { 32 | 'electron-v2.0.9-darwin-x64.zip': 33 | 'ff0bfe95bc2a351e09b959aab0bdab893cb33c203bfff83413c3f0989858c684', 34 | }, 35 | }); 36 | expect(typeof zipPath).toEqual('string'); 37 | expect(fs.existsSync(zipPath)).toEqual(true); 38 | expect(path.extname(zipPath)).toEqual('.zip'); 39 | }); 40 | 41 | it('should be rejected with invalid checksums', async () => { 42 | await expect( 43 | downloadArtifact({ 44 | cacheRoot, 45 | downloader, 46 | platform: 'darwin', 47 | arch: 'x64', 48 | version: '2.0.9', 49 | artifactName: 'electron', 50 | checksums: { 51 | 'electron-v2.0.9-darwin-x64.zip': 52 | 'f28e3f9d2288af6abc31b19ca77a9241499fcd0600420197a9ff8e5e06182701', 53 | }, 54 | }), 55 | ).rejects.toMatchInlineSnapshot( 56 | `[Error: Generated checksum for "electron-v2.0.9-darwin-x64.zip" did not match expected checksum.]`, 57 | ); 58 | }); 59 | 60 | it('should be rejected with an empty checksums object', async () => { 61 | await expect( 62 | downloadArtifact({ 63 | cacheRoot, 64 | downloader, 65 | platform: 'darwin', 66 | arch: 'x64', 67 | version: '2.0.9', 68 | artifactName: 'electron', 69 | checksums: {}, 70 | }), 71 | ).rejects.toMatchInlineSnapshot( 72 | `[Error: Provided "checksums" object is empty, cannot generate a valid SHASUMS256.txt]`, 73 | ); 74 | }); 75 | 76 | it('should be rejected with missing checksums', async () => { 77 | await expect( 78 | downloadArtifact({ 79 | cacheRoot, 80 | downloader, 81 | platform: 'darwin', 82 | arch: 'x64', 83 | version: '2.0.9', 84 | artifactName: 'electron', 85 | checksums: { 86 | 'electron-v2.0.9-linux-x64.zip': 87 | 'f28e3f9d2288af6abc31b19ca77a9241499fcd0600420197a9ff8e5e06182701', 88 | }, 89 | }), 90 | ).rejects.toMatchInlineSnapshot( 91 | `[Error: No checksum found in checksum file for "electron-v2.0.9-darwin-x64.zip".]`, 92 | ); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/fixtures/SHASUMS256.txt: -------------------------------------------------------------------------------- 1 | 92fdadf6c3927cea731be8ebeb37566d09926a3e3c83d65fb031e4424d3203e6 *chromedriver-v2.0.9-darwin-x64.zip 2 | 9bd7a71f465f6881bf8b238f269c90668ac217161776ea0ed348c8e70bab2440 *electron.d.ts 3 | 3cb443479f075d014ace1b944e4dc9f12b7b54771a7e0dc62fb4bf3f97792cad *electron-v2.0.10-darwin-x64.zip 4 | a9709f80ad7f52db2fd8b88b97ae26d94235cf7656ed39e00630c0df91ed0d04 *electron-v2.0.10-linux-x64.zip 5 | 09e1e07b769d57c99993e1173c6ac565a43cfe2fd0ea53aeb4b2cd1839579a3c *electron-v2.0.10-win32-x64.zip 6 | 80c36dea2bd2373da93192797d21eb762b26f196d3ec616082df1be74e855bd2 *electron-v2.0.3-darwin-x64.zip 7 | eac0266403045f0d22d4e8c864326cbf43f1cca1b6ed1ac037fcb06f6ec46522 *electron-v2.0.3-linux-x64.zip 8 | 3f1e243fce8747c125cb61ab1834b86505b0f9f476141eedd5cfb43b4b1b8c2a *electron-v2.0.3-win32-x64.zip 9 | ff0bfe95bc2a351e09b959aab0bdab893cb33c203bfff83413c3f0989858c684 *electron-v2.0.9-darwin-x64.zip 10 | 87c5b7a966444ab1d97d7f6f5be9ffc44e2305a763bb74c1b8a940d9adbffa43 *electron-v2.0.9-linux-armv7l.zip 11 | ec951354a58f63e5194ec47e10c493cf8d23c46c0caa9724bdba6d9a88d3695d *electron-v2.0.9-linux-x64.zip 12 | ae7b827f0649c5ac519080ead2d507479da90f9fbf4941d696bc3e1ae984d6b3 *electron-v2.0.9-win32-x64.zip 13 | ebb7a042d4885870cc82f510c5cb09c586ed75983a613931779bed01cfb7c657 *electron-v6.0.0-nightly.20190213-darwin-x64.zip 14 | fb9010e2301ebc24824db1842b97478a61935df741a8b716e9ae278cf234d1e2 *electron-v6.0.0-nightly.20190213-linux-x64.zip 15 | f28e3f9d2288af6abc31b19ca77a9241499fcd0600420197a9ff8e5e06182701 *electron-v6.0.0-nightly.20190213-win32-x64.zip 16 | -------------------------------------------------------------------------------- /test/fixtures/chromedriver-v2.0.9-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | chromedriver-v2.0.9-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.0.0-darwin-arm64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.0.0-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.0.0-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.0.0-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.0.0-linux-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.0.0-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.0.0-win32-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.0.0-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.3-darwin-arm64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.2-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.3-darwin-x64 copy.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.3-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.3-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.2-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.3-linux-x64 copy.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.3-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.3-linux-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.3-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.3-win32-x64 copy.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.3-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.3-win32-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.3-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.5-darwin-arm64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.5-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.5-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.5-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.5-linux-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.5-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v1.3.5-win32-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v1.3.5-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.10-darwin-arm64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.10-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.10-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.10-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.10-linux-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.10-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.10-win32-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.10-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.3-darwin-arm64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.3-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.3-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.3-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.3-linux-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.3-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.3-win32-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.3-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.9-darwin-arm64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.9-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.9-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.9-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.9-linux-armv7l.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.9-linux-armv7l.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.9-linux-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.9-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v2.0.9-win32-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v2.0.9-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v6.0.0-nightly.20190213-darwin-arm64.zip: -------------------------------------------------------------------------------- 1 | electron-v6.0.0-nightly.20190213-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v6.0.0-nightly.20190213-darwin-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v6.0.0-nightly.20190213-darwin-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v6.0.0-nightly.20190213-linux-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v6.0.0-nightly.20190213-linux-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron-v6.0.0-nightly.20190213-win32-x64.zip: -------------------------------------------------------------------------------- 1 | electron-v6.0.0-nightly.20190213-win32-x64.zip 2 | -------------------------------------------------------------------------------- /test/fixtures/electron.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Electron {} 2 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import path from 'node:path'; 3 | import util from 'node:util'; 4 | 5 | import fs from 'graceful-fs'; 6 | import sumchecker from 'sumchecker'; 7 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 8 | 9 | import { FixtureDownloader } from './FixtureDownloader'; 10 | import { download, downloadArtifact } from '../src'; 11 | import { DownloadOptions, ElectronDownloadCacheMode } from '../src/types'; 12 | 13 | vi.mock('sumchecker'); 14 | 15 | describe('Public API', () => { 16 | const downloader = new FixtureDownloader(); 17 | 18 | let cacheRoot: string; 19 | beforeEach(async () => { 20 | cacheRoot = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-download-spec-')); 21 | }); 22 | 23 | afterEach(async () => { 24 | await fs.promises.rm(cacheRoot, { recursive: true, force: true }); 25 | }); 26 | 27 | describe('download()', () => { 28 | it('should return a valid path to a downloaded zip file', async () => { 29 | const zipPath = await download('2.0.10', { 30 | cacheRoot, 31 | downloader, 32 | }); 33 | expect(typeof zipPath).toEqual('string'); 34 | expect(fs.existsSync(zipPath)).toEqual(true); 35 | expect(path.extname(zipPath)).toEqual('.zip'); 36 | expect(path.dirname(zipPath).startsWith(cacheRoot)).toEqual(true); 37 | expect(fs.readdirSync(cacheRoot).length).toBeGreaterThan(0); 38 | }); 39 | 40 | it('should return a valid path to a downloaded zip file for nightly releases', async () => { 41 | const zipPath = await download('6.0.0-nightly.20190213', { 42 | cacheRoot, 43 | downloader, 44 | }); 45 | expect(typeof zipPath).toEqual('string'); 46 | expect(fs.existsSync(zipPath)).toEqual(true); 47 | expect(path.extname(zipPath)).toEqual('.zip'); 48 | }); 49 | 50 | it('should accept a custom downloader', async () => { 51 | const zipPath = await download('2.0.3', { 52 | cacheRoot, 53 | unsafelyDisableChecksums: true, 54 | downloader: { 55 | async download(url: string, targetPath: string): Promise { 56 | expect( 57 | url.replace(process.platform, 'platform').replace(process.arch, 'arch'), 58 | ).toMatchSnapshot(); 59 | await util.promisify(fs.writeFile)(targetPath, 'faked from downloader'); 60 | }, 61 | }, 62 | }); 63 | expect(await util.promisify(fs.readFile)(zipPath, 'utf8')).toEqual('faked from downloader'); 64 | }); 65 | 66 | it('should pass download options to a custom downloader', async () => { 67 | const downloadOpts = { 68 | magic: 'option', 69 | trick: 'shot', 70 | }; 71 | await download('2.0.3', { 72 | cacheRoot, 73 | unsafelyDisableChecksums: true, 74 | downloader: { 75 | async download(url: string, targetPath: string, opts?: DownloadOptions): Promise { 76 | expect(opts).toStrictEqual(downloadOpts); 77 | await util.promisify(fs.writeFile)(targetPath, 'file'); 78 | }, 79 | }, 80 | downloadOptions: downloadOpts, 81 | }); 82 | }); 83 | 84 | it('should download a custom version of a zip file', async () => { 85 | process.env.ELECTRON_CUSTOM_VERSION = '2.0.10'; 86 | const zipPath = await download('2.0.3', { 87 | cacheRoot, 88 | downloader, 89 | }); 90 | expect(typeof zipPath).toEqual('string'); 91 | expect(fs.existsSync(zipPath)).toEqual(true); 92 | expect(path.basename(zipPath)).toMatch(/v2.0.10/); 93 | expect(path.extname(zipPath)).toEqual('.zip'); 94 | process.env.ELECTRON_CUSTOM_VERSION = ''; 95 | }); 96 | 97 | it('should not put artifact in cache when cacheMode=ReadOnly', async () => { 98 | const zipPath = await download('2.0.10', { 99 | cacheRoot, 100 | downloader, 101 | cacheMode: ElectronDownloadCacheMode.ReadOnly, 102 | }); 103 | expect(typeof zipPath).toEqual('string'); 104 | expect(fs.existsSync(zipPath)).toEqual(true); 105 | expect(path.extname(zipPath)).toEqual('.zip'); 106 | expect(path.dirname(zipPath).startsWith(cacheRoot)).toEqual(false); 107 | expect(fs.readdirSync(cacheRoot).length).toEqual(0); 108 | }); 109 | 110 | it('should use cache hits when cacheMode=ReadOnly', async () => { 111 | const zipPath = await download('2.0.9', { 112 | cacheRoot, 113 | downloader, 114 | }); 115 | await util.promisify(fs.writeFile)(zipPath, 'cached content'); 116 | const zipPath2 = await download('2.0.9', { 117 | cacheRoot, 118 | downloader, 119 | cacheMode: ElectronDownloadCacheMode.ReadOnly, 120 | }); 121 | expect(zipPath2).not.toEqual(zipPath); 122 | expect(path.dirname(zipPath2).startsWith(cacheRoot)).toEqual(false); 123 | expect(await util.promisify(fs.readFile)(zipPath2, 'utf8')).toEqual('cached content'); 124 | }); 125 | }); 126 | 127 | describe('downloadArtifact()', () => { 128 | it('should work for electron.d.ts', async () => { 129 | const dtsPath = await downloadArtifact({ 130 | cacheRoot, 131 | downloader, 132 | isGeneric: true, 133 | version: '2.0.9', 134 | artifactName: 'electron.d.ts', 135 | }); 136 | expect(fs.existsSync(dtsPath)).toEqual(true); 137 | expect(path.basename(dtsPath)).toEqual('electron.d.ts'); 138 | expect(await util.promisify(fs.readFile)(dtsPath, 'utf8')).toContain( 139 | 'declare namespace Electron', 140 | ); 141 | expect(path.dirname(dtsPath).startsWith(cacheRoot)).toEqual(true); 142 | expect(fs.readdirSync(cacheRoot).length).toBeGreaterThan(0); 143 | }); 144 | 145 | it('should work with the default platform/arch', async () => { 146 | const artifactPath = await downloadArtifact({ 147 | downloader, 148 | version: '2.0.3', 149 | artifactName: 'electron', 150 | }); 151 | expect(artifactPath).toContain(`electron-v2.0.3-${process.platform}-${process.arch}.zip`); 152 | }); 153 | 154 | it('should download the same artifact for falsy platform/arch as default platform/arch', async () => { 155 | const defaultPath = await downloadArtifact({ 156 | version: '2.0.3', 157 | artifactName: 'electron', 158 | }); 159 | 160 | const undefinedPath = await downloadArtifact({ 161 | version: '2.0.3', 162 | artifactName: 'electron', 163 | platform: undefined, 164 | arch: undefined, 165 | }); 166 | 167 | expect(defaultPath).toEqual(undefinedPath); 168 | }); 169 | 170 | it('should download linux/armv7l when linux/arm is passed as platform/arch', async () => { 171 | const zipPath = await downloadArtifact({ 172 | cacheRoot, 173 | downloader, 174 | artifactName: 'electron', 175 | version: '2.0.9', 176 | platform: 'linux', 177 | arch: 'arm', 178 | }); 179 | expect(path.basename(zipPath)).toMatchInlineSnapshot(`"electron-v2.0.9-linux-armv7l.zip"`); 180 | }); 181 | 182 | it('should work for chromedriver', async () => { 183 | const driverPath = await downloadArtifact({ 184 | cacheRoot, 185 | downloader, 186 | version: '2.0.9', 187 | artifactName: 'chromedriver', 188 | platform: 'darwin', 189 | arch: 'x64', 190 | }); 191 | expect(fs.existsSync(driverPath)).toEqual(true); 192 | expect(path.basename(driverPath)).toMatchInlineSnapshot( 193 | `"chromedriver-v2.0.9-darwin-x64.zip"`, 194 | ); 195 | expect(path.extname(driverPath)).toEqual('.zip'); 196 | }); 197 | 198 | it('should download a custom version of a zip file', async () => { 199 | process.env.ELECTRON_CUSTOM_VERSION = '2.0.10'; 200 | const zipPath = await downloadArtifact({ 201 | artifactName: 'electron', 202 | cacheRoot, 203 | downloader, 204 | platform: 'darwin', 205 | arch: 'x64', 206 | version: '2.0.3', 207 | }); 208 | expect(typeof zipPath).toEqual('string'); 209 | expect(fs.existsSync(zipPath)).toEqual(true); 210 | expect(path.basename(zipPath)).toMatchInlineSnapshot(`"electron-v2.0.10-darwin-x64.zip"`); 211 | expect(path.extname(zipPath)).toEqual('.zip'); 212 | process.env.ELECTRON_CUSTOM_VERSION = ''; 213 | }); 214 | 215 | it('should download a custom version specified via mirror options', async () => { 216 | const zipPath = await downloadArtifact({ 217 | artifactName: 'electron', 218 | cacheRoot, 219 | downloader, 220 | platform: 'darwin', 221 | arch: 'x64', 222 | version: '2.0.3', 223 | mirrorOptions: { 224 | customVersion: '2.0.10', 225 | }, 226 | }); 227 | expect(typeof zipPath).toEqual('string'); 228 | expect(path.basename(zipPath)).toMatchInlineSnapshot(`"electron-v2.0.10-darwin-x64.zip"`); 229 | }); 230 | 231 | it('should not put artifact in cache when cacheMode=ReadOnly', async () => { 232 | const driverPath = await downloadArtifact({ 233 | cacheRoot, 234 | downloader, 235 | version: '2.0.9', 236 | artifactName: 'chromedriver', 237 | platform: 'darwin', 238 | arch: 'x64', 239 | cacheMode: ElectronDownloadCacheMode.ReadOnly, 240 | }); 241 | expect(fs.existsSync(driverPath)).toEqual(true); 242 | expect(path.basename(driverPath)).toMatchInlineSnapshot( 243 | `"chromedriver-v2.0.9-darwin-x64.zip"`, 244 | ); 245 | expect(path.extname(driverPath)).toEqual('.zip'); 246 | expect(path.dirname(driverPath).startsWith(cacheRoot)).toEqual(false); 247 | expect(fs.readdirSync(cacheRoot).length).toEqual(0); 248 | }); 249 | 250 | it('should use cache hits when cacheMode=ReadOnly', async () => { 251 | const driverPath = await downloadArtifact({ 252 | cacheRoot, 253 | downloader, 254 | version: '2.0.9', 255 | artifactName: 'chromedriver', 256 | platform: 'darwin', 257 | arch: 'x64', 258 | }); 259 | await util.promisify(fs.writeFile)(driverPath, 'cached content'); 260 | const driverPath2 = await downloadArtifact({ 261 | cacheRoot, 262 | downloader, 263 | version: '2.0.9', 264 | artifactName: 'chromedriver', 265 | platform: 'darwin', 266 | arch: 'x64', 267 | cacheMode: ElectronDownloadCacheMode.ReadOnly, 268 | }); 269 | expect(driverPath2).not.toEqual(driverPath); 270 | expect(path.dirname(driverPath2).startsWith(cacheRoot)).toEqual(false); 271 | expect(await util.promisify(fs.readFile)(driverPath2, 'utf8')).toEqual('cached content'); 272 | }); 273 | 274 | describe('sumchecker', () => { 275 | beforeEach(() => { 276 | vi.clearAllMocks(); 277 | }); 278 | 279 | it('should use the default constructor for versions from v1.3.5 onward', async () => { 280 | await downloadArtifact({ 281 | artifactName: 'electron', 282 | downloader, 283 | cacheMode: ElectronDownloadCacheMode.WriteOnly, 284 | version: 'v1.3.5', 285 | }); 286 | await downloadArtifact({ 287 | artifactName: 'electron', 288 | downloader, 289 | cacheMode: ElectronDownloadCacheMode.WriteOnly, 290 | version: 'v2.0.3', 291 | }); 292 | 293 | expect(sumchecker).toHaveBeenCalledTimes(2); 294 | expect(sumchecker.ChecksumValidator).not.toHaveBeenCalled(); 295 | }); 296 | 297 | it('should use ChecksumValidator for v1.3.2 - v.1.3.4', async () => { 298 | await downloadArtifact({ 299 | artifactName: 'electron', 300 | downloader, 301 | cacheMode: ElectronDownloadCacheMode.WriteOnly, 302 | version: 'v1.3.3', 303 | }); 304 | 305 | expect(sumchecker).not.toHaveBeenCalled(); 306 | expect(sumchecker.ChecksumValidator).toHaveBeenCalledTimes(1); 307 | }); 308 | 309 | it('should not be called for versions prior to v1.3.2', async () => { 310 | await downloadArtifact({ 311 | artifactName: 'electron', 312 | downloader, 313 | cacheMode: ElectronDownloadCacheMode.WriteOnly, 314 | version: 'v1.0.0', 315 | }); 316 | 317 | expect(sumchecker).not.toHaveBeenCalled(); 318 | expect(sumchecker.ChecksumValidator).not.toHaveBeenCalled(); 319 | }); 320 | }); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'graceful-fs'; 2 | 3 | import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; 4 | 5 | import { 6 | normalizeVersion, 7 | uname, 8 | withTempDirectory, 9 | getHostArch, 10 | ensureIsTruthyString, 11 | isOfficialLinuxIA32Download, 12 | getEnv, 13 | setEnv, 14 | TempDirCleanUpMode, 15 | } from '../src/utils'; 16 | 17 | describe('utils', () => { 18 | describe('normalizeVersion()', () => { 19 | it('should do nothing if the version starts with a v', () => { 20 | expect(normalizeVersion('v1.2.3')).toEqual('v1.2.3'); 21 | }); 22 | 23 | it('should auto prefix a version with a v if it does not already start with a v', () => { 24 | expect(normalizeVersion('3.2.1')).toEqual('v3.2.1'); 25 | }); 26 | }); 27 | 28 | describe.skipIf(process.platform === 'win32')('uname()', () => { 29 | it('should return the correct arch for your system', () => { 30 | const arch = process.arch === 'arm64' ? 'arm64' : 'x86_64'; 31 | expect(uname()).toEqual(arch); 32 | }); 33 | }); 34 | 35 | describe('withTempDirectory()', () => { 36 | it('should generate a new and empty directory', async () => { 37 | await withTempDirectory(async (dir) => { 38 | expect(fs.existsSync(dir)).toEqual(true); 39 | expect(await fs.promises.readdir(dir)).toEqual([]); 40 | }, TempDirCleanUpMode.CLEAN); 41 | }); 42 | 43 | it('should return the value the function returns', async () => { 44 | expect(await withTempDirectory(async () => 1234, TempDirCleanUpMode.CLEAN)).toEqual(1234); 45 | }); 46 | 47 | it('should delete the directory when the function terminates', async () => { 48 | const mDir = await withTempDirectory(async (dir) => { 49 | return dir; 50 | }, TempDirCleanUpMode.CLEAN); 51 | expect(mDir).not.toBeUndefined(); 52 | expect(fs.existsSync(mDir)).toEqual(false); 53 | }); 54 | 55 | it('should delete the directory and reject correctly even if the function throws', async () => { 56 | let mDir: string | undefined; 57 | await expect( 58 | withTempDirectory(async (dir) => { 59 | mDir = dir; 60 | throw 'my error'; 61 | }, TempDirCleanUpMode.CLEAN), 62 | ).rejects.toEqual('my error'); 63 | expect(mDir).not.toBeUndefined(); 64 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 65 | expect(fs.existsSync(mDir!)).toEqual(false); 66 | }); 67 | 68 | it('should not delete the directory if told to orphan the temp dir', async () => { 69 | const mDir = await withTempDirectory(async (dir) => { 70 | return dir; 71 | }, TempDirCleanUpMode.ORPHAN); 72 | expect(mDir).not.toBeUndefined(); 73 | try { 74 | expect(fs.existsSync(mDir)).toEqual(true); 75 | } finally { 76 | await fs.promises.rm(mDir, { recursive: true, force: true }); 77 | } 78 | }); 79 | }); 80 | 81 | describe('getHostArch()', () => { 82 | it('should return process.arch on x64', () => { 83 | vi.spyOn(process, 'arch', 'get').mockReturnValue('x64'); 84 | expect(getHostArch()).toEqual('x64'); 85 | }); 86 | 87 | it('should return process.arch on ia32', () => { 88 | vi.spyOn(process, 'arch', 'get').mockReturnValue('ia32'); 89 | expect(getHostArch()).toEqual('ia32'); 90 | }); 91 | 92 | it('should return process.arch on unknown arm', () => { 93 | vi.spyOn(process, 'arch', 'get').mockReturnValue('arm'); 94 | vi.spyOn(process, 'config', 'get').mockReturnValue({ 95 | ...process.config, 96 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 97 | variables: {} as any, 98 | }); 99 | expect(getHostArch()).toEqual('armv7l'); 100 | }); 101 | 102 | it.skipIf(process.platform === 'win32')('should return uname on arm 6', () => { 103 | vi.spyOn(process, 'arch', 'get').mockReturnValue('arm'); 104 | vi.spyOn(process, 'config', 'get').mockReturnValue({ 105 | ...process.config, 106 | variables: { 107 | //@ts-expect-error - `arm_version` actually exists 108 | arm_version: '6', 109 | }, 110 | }); 111 | expect(getHostArch()).toEqual(uname()); 112 | }); 113 | 114 | it('should return armv7l on arm 7', () => { 115 | vi.spyOn(process, 'arch', 'get').mockReturnValue('arm'); 116 | vi.spyOn(process, 'config', 'get').mockReturnValue({ 117 | ...process.config, 118 | variables: { 119 | //@ts-expect-error - `arm_version` actually exists 120 | arm_version: '7', 121 | }, 122 | }); 123 | expect(getHostArch()).toEqual('armv7l'); 124 | }); 125 | }); 126 | 127 | describe('ensureIsTruthyString()', () => { 128 | it('should not throw for a valid string', () => { 129 | expect(() => ensureIsTruthyString({ a: 'string' }, 'a')).not.toThrow(); 130 | }); 131 | 132 | it('should throw for an invalid string', () => { 133 | expect(() => ensureIsTruthyString({ a: 1234 }, 'a')).toThrow(); 134 | }); 135 | }); 136 | 137 | describe('isOfficialLinuxIA32Download()', () => { 138 | it('should be true if an official linux/ia32 download with correct version specified', () => { 139 | expect(isOfficialLinuxIA32Download('linux', 'ia32', 'v4.0.0')).toEqual(true); 140 | }); 141 | 142 | it('should be false if mirrorOptions specified', () => { 143 | expect( 144 | isOfficialLinuxIA32Download('linux', 'ia32', 'v4.0.0', { mirror: 'mymirror' }), 145 | ).toEqual(false); 146 | }); 147 | 148 | it('should be false if too early version specified', () => { 149 | expect(isOfficialLinuxIA32Download('linux', 'ia32', 'v3.0.0')).toEqual(false); 150 | }); 151 | 152 | it('should be false if wrong platform/arch specified', () => { 153 | expect(isOfficialLinuxIA32Download('win32', 'ia32', 'v4.0.0')).toEqual(false); 154 | expect(isOfficialLinuxIA32Download('linux', 'x64', 'v4.0.0')).toEqual(false); 155 | }); 156 | }); 157 | 158 | describe('getEnv()', () => { 159 | const [prefix, envName] = ['TeSt_EnV_vAr_', 'eNv_Key']; 160 | const prefixEnvName = `${prefix}${envName}`; 161 | const [hasPrefixValue, noPrefixValue] = ['yes_prefix', 'no_prefix']; 162 | 163 | beforeAll(() => { 164 | process.env[prefixEnvName] = hasPrefixValue; 165 | process.env[envName] = noPrefixValue; 166 | }); 167 | 168 | afterAll(() => { 169 | delete process.env[prefixEnvName]; 170 | delete process.env[envName]; 171 | }); 172 | 173 | it('should return prefixed environment variable if prefixed variable found', () => { 174 | const env = getEnv(prefix); 175 | expect(env(envName)).toEqual(hasPrefixValue); 176 | expect(env(envName.toLowerCase())).toEqual(hasPrefixValue); 177 | expect(env(envName.toUpperCase())).toEqual(hasPrefixValue); 178 | }); 179 | 180 | it('should return non-prefixed environment variable if no prefixed variable found', () => { 181 | expect(getEnv()(envName)).toEqual(noPrefixValue); 182 | expect(getEnv()(envName.toLowerCase())).toEqual(noPrefixValue); 183 | expect(getEnv()(envName.toUpperCase())).toEqual(noPrefixValue); 184 | }); 185 | 186 | it('should return undefined if no match', () => { 187 | const randomStr = 'AAAAA_electron_'; 188 | expect(getEnv()(randomStr)).toEqual(undefined); 189 | expect(getEnv()(randomStr.toLowerCase())).toEqual(undefined); 190 | expect(getEnv()(randomStr.toUpperCase())).toEqual(undefined); 191 | }); 192 | }); 193 | }); 194 | 195 | describe('setEnv()', () => { 196 | it("doesn't set the environment variable if the value is undefined", () => { 197 | const [key, value] = ['Set_AAA_electron', undefined]; 198 | setEnv(key, value); 199 | expect(process.env[key]).toEqual(void 0); 200 | }); 201 | 202 | it('successfully sets the environment variable when the value is defined', () => { 203 | const [key, value] = ['Set_BBB_electron', 'Test']; 204 | setEnv(key, value); 205 | expect(process.env[key]).toEqual(value); 206 | }); 207 | 208 | it('successfully sets the environment variable when the value is falsey', () => { 209 | const [key, value] = ['Set_AAA_electron', false]; 210 | // @ts-expect-error - we want to ensure that the boolean gets converted to string accordingly 211 | setEnv(key, value); 212 | expect(process.env[key]).toEqual('false'); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "outDir": "dist", 6 | "types": ["node"], 7 | "declaration": true 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["src/index.ts"], 4 | "excludeInternal": true, 5 | "externalSymbolLinkMappings": { 6 | "got": { 7 | "Options": "https://github.com/sindresorhus/got/tree/v11.8.5?tab=readme-ov-file#options", 8 | "Progress": "https://github.com/sindresorhus/got/tree/v11.8.5?tab=readme-ov-file#downloadprogress" 9 | } 10 | }, 11 | "navigation": true, 12 | "categoryOrder": ["Download Electron", "Download Artifact", "Downloader", "Utility", "*"], 13 | "categorizeByGroup": false, 14 | "sort": ["kind", "required-first", "alphabetical"], 15 | "kindSortOrder": [ 16 | "Reference", 17 | "Project", 18 | "Module", 19 | "Namespace", 20 | "Enum", 21 | "EnumMember", 22 | "Function", 23 | "Class", 24 | "Interface", 25 | "TypeAlias", 26 | "Constructor", 27 | "Property", 28 | "Variable", 29 | "Accessor", 30 | "Method", 31 | "Parameter", 32 | "TypeParameter", 33 | "TypeLiteral", 34 | "CallSignature", 35 | "ConstructorSignature", 36 | "IndexSignature", 37 | "GetSignature", 38 | "SetSignature" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------