diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c9de68d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,56 @@ +# git +.git* + +# OSX +.DS_Store + +# Node.js + +# ignore compiled lib files +lib/* +app/lib/* +built-tests +dist +app/dist + +# Docs +docs +*.md + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules +app/node_modules + +# IntelliJ project files +.idea +*.iml +out +gen + +.vscode diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e9f3a67 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py}] +charset = utf-8 +indent_style = space +indent_size = 2 + +# 2 space indentation +[*.{html,css,less,scss,yml,json}] +indent_style = space +indent_size = 2 + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab + +trim_trailing_whitespace = true diff --git a/.env b/.env new file mode 100644 index 0000000..51b263c --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..ee46abd --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing to Nativefier + +## Issues + +Please include the following in your new issue: + +- Version of Nativefier (run `$ nativefier --version`) +- Version of Node.js (run `$ node --version`) +- Command line parameters +- OS and architecture you are running Nativefier from +- Stack trace from the error message (if any) +- Instructions to reproduce the issue + +## Pull Requests + +See [here](https://github.com/nativefier/nativefier/blob/master/HACKING.md) for instructions on how to set up a development environment. + +We follow the [Airbnb Style Guide](https://github.com/airbnb/javascript), please make sure tests and lints pass when you submit your pull request. + +The following commands might be helpful: + +```bash +# Run specs only +npm run test + +# Run linter only +npm run lint +``` + +Thank you so much for your contribution! diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..3d68736 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,93 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report 🙂! Help us help you, **fill this form thoroughly**. An incomplete bug report is a useless bug report. + - type: checkboxes + id: homework + attributes: + label: Homework + options: + - label: I took the time to write a good, descriptive issue title + required: true + - label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md). + required: true + - label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds. + required: true + - label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my bug is new. + required: true + - label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases). + required: true + - type: input + id: nativefier-command + attributes: + label: Nativefier command + description: "Your ***full*** nativefier command, on a ***public*** site." + placeholder: nativefier --verbose --some-option https://mysite.com + validations: + required: true + - type: textarea + id: steps-to-repro + attributes: + label: Steps to reproduce + placeholder: | + 1. I did this... + 2. And then that... + 3. Finally, I clicked here. + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + placeholder: What you expected to happen. + validations: + required: true + - type: textarea + id: actual-behavior + attributes: + label: Actual behavior + placeholder: What happened instead. + validations: + required: true + - type: textarea + id: debug-info + attributes: + label: Debug info + placeholder: | + - Logs of your full build command, with the `--verbose` flag. Put them in a ```code block``` ! + - If the bug happens at app run time, the in-app DevTools console logs (open it with F12) + - Error messages, screenshots, screencasts, anything relevant! + validations: + required: false + - type: input + id: nativefier-version + attributes: + label: Nativefier version + placeholder: "nativefier --version" + validations: + required: true + - type: input + id: node-version + attributes: + label: Node.js version + placeholder: "node --version" + validations: + required: true + - type: input + id: npm-version + attributes: + label: npm version + placeholder: "npm --version" + validations: + required: true + - type: input + id: os + attributes: + label: OS + placeholder: "For example: Windows 10 build 1809" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..95dad3d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,45 @@ +name: Feature request +description: Suggest an idea for Nativefier +labels: ["feature-request"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request 🙂! Help us help you, **fill this form thoroughly**. An incomplete feature request is a useless feature request. + - type: checkboxes + id: homework + attributes: + label: Homework + options: + - label: I took the time to write a good, descriptive issue title + required: true + - label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md), no existing option fits my needs. + required: true + - label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds. + required: true + - label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my feature request is new. + required: true + - label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases). Yes, the feature I'm requesting isn't in it. + required: true + validations: + required: true + - type: textarea + id: problem-statement + attributes: + label: Problem statement + description: A clear and concise description of what your feature would be. + placeholder: | + For example: + Nativefier should XYZ, ... details details details... + Existing option --something is not what I want, because ... + validations: + required: true + - type: textarea + id: motivation-and-context + attributes: + label: Motivation & context + placeholder: | + What makes you want this feature? + Where does it come from? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..5cec78b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,78 @@ +name: Question +description: Ask for help +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report 🙂! Help us help you, **fill this form thoroughly**. A cryptic question is a question unlikely to be answered. + - type: checkboxes + id: homework + attributes: + label: Homework + options: + - label: I took the time to write a good, descriptive issue title + required: true + - label: I read `nativefier --help` and [API.md](https://github.com/nativefier/nativefier/blob/master/API.md). + required: true + - label: I checked [CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md) for community suggestions & workarounds. + required: true + - label: I searched [existing issues, open & closed](https://github.com/nativefier/nativefier/issues?q=is%3Aissue). Yes, my question is new. + required: true + - label: I'm running the [latest version](https://github.com/nativefier/nativefier/releases). + required: true + validations: + required: false + - type: textarea + id: question + attributes: + label: Your question + description: Your question, expressed clearly and concisely. + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: "If you already have a Nativefier command you're struggling with, paste ***full*** nativefier command and its logs, with the ***`--verbose` flag***, on a ***public*** site:" + value: | + ``` + nativefier --verbose --some-option https://mysite.com + + ``` + validations: + required: false + - type: textarea + id: debug-info + attributes: + label: Debug info + placeholder: | + Error messages, screenshots, screencasts, anything relevant! + - type: input + id: nativefier-version + attributes: + label: Nativefier version + placeholder: "nativefier --version" + validations: + required: true + - type: input + id: node-version + attributes: + label: Node.js version + placeholder: "node --version" + validations: + required: true + - type: input + id: npm-version + attributes: + label: npm version + placeholder: "npm --version" + validations: + required: true + - type: input + id: os + attributes: + label: OS + placeholder: "For example: Windows 10 build 1809" + validations: + required: true diff --git a/.github/dock-screenshot.png b/.github/dock-screenshot.png new file mode 100644 index 0000000..a81ca6d Binary files /dev/null and b/.github/dock-screenshot.png differ diff --git a/.github/generate-changelog b/.github/generate-changelog new file mode 100755 index 0000000..333a91a --- /dev/null +++ b/.github/generate-changelog @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# Updates the changelog and version in the package.json +# Will also create a commit with these changes locally +# +# Usage: +# ./.github/generate-changelog -- "7.0.0" +# +# Prerequisites: +# - On master branch +# - No uncommitted changes +# +# Dependencies: +# - git-extras: https://github.com/tj/git-extras/blob/master/Installation.md +# - jq: https://stedolan.github.io/jq/download/ + +set -eo pipefail + +echo 'HEY YOU. Before you release, here is a report of outdated dependencies.' +echo ' - Red upgrades fulfill semver and do *not* need any action' +echo ' - Yellow upgrades *do* need looking at changelogs for breaking changes, and updating package.json' +echo +echo 'CLI:' +npm out || true +echo +echo 'App:' +cd app; npm out || true; cd .. +echo +echo 'Okay with this, or care to do/plan a few upgrades?' +echo 'Press any key to continue, or Ctrl+C to abort' +read -r + +echo 'HEY YOU, again. Did you run the quick pre-release smoke test? ( npm run test:manual )' +echo 'Press any key to continue, or Ctrl+C to abort' +read -r + +# Checks if we are on the master branch +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [[ "$BRANCH" != 'master' ]]; then + echo 'ERROR: not on master branch' >&2 + exit 1; +fi + +# Checks if there are uncommitted changes +git diff-index --quiet HEAD -- || (echo 'ERROR: there are uncommitted changes' >&2 && exit 1) + +VERSION="$1" + +# Validates the $VERSION +SEMVER_REGEX='^([0-9]+\.){2}([0-9]+)$' +if ! [[ $VERSION =~ $SEMVER_REGEX ]]; then + echo "ERROR: Version '$VERSION' is invalid " >&2 + exit 1 +fi + +# 1. Update the version in the package.json +cat package.json | jq ".version = \"$VERSION\"" > package.json.tmp +mv package.json.tmp package.json # workaround for in-place jq editing + +# 2. Compile new commits from CHANGELOG.md, and open it in your EDITOR for cleanup +git changelog CHANGELOG.md --tag "$VERSION" + +# 3. Commit the changes +git add CHANGELOG.md +git add package.json +git commit -m "Update changelog for \`v$VERSION\`" + +# 4. Create an annotated tag +git tag -a "v$VERSION" -m "v$VERSION" + +# 5. List remaining work +echo +echo 'Please verify commit & tag look fine in Git, then:' +echo ' 1. Push: git push --follow-tags origin master' +echo ' 2. Create a GitHub Release at https://github.com/nativefier/nativefier/releases ,' +echo " using created tag v$VERSION and with title \"Nativefier v$VERSION\" (yes, with a \"v\")." +echo +echo 'GitHub Action "publish" will react on the new release, and publish it to npm.' +echo 'The new version will be visible on npm within a few minutes/hours.' diff --git a/.github/manual-test b/.github/manual-test new file mode 100755 index 0000000..5b5c350 --- /dev/null +++ b/.github/manual-test @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Manual test to validate some hard-to-programmatically-test features work. +set -eo pipefail + +missingDeps=false +if ! command -v mktemp > /dev/null; then echo "Missing mktemp"; missingDeps=true; fi +if ! command -v uname > /dev/null; then echo "Missing uname"; missingDeps=true; fi +if ! command -v node > /dev/null; then echo "Missing node"; missingDeps=true; fi +if [ "$missingDeps" = true ]; then exit 1; fi + +function launch_app() { + printf '\n*** Running app\n' + if [ "$(uname -s)" = "Darwin" ]; then + open -a "$1/$2-darwin-x64/$2.app" + elif [ "$(uname -o)" = "Msys" ]; then + "$1/$2-win32-x64/$2.exe" + else + "$1/$2-linux-x64/$2" + fi +} + +function do_cleanup() { + if [ -n "$1" ]; then + printf '\n***** Deleting test dir %s *****\n' "$1" + rm -rf "$1" + printf '\n' + fi +} + +function request_feedback() { + printf '\nDid everything work as expected? [yN] ' + read -r response + + do_cleanup "$1" + + if [ "$response" != 'y' ]; then + echo "Back to fixing" + exit 1 + fi + echo "Yayyyyyyyyyyy" +} + +printf "\n***** SMOKE TEST 1: Setting up test and building app... *****\n" +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +nativefier_dir="$script_dir/.." +pushd "$nativefier_dir" +tmp_dir=$(mktemp -d -t nativefier-manual-test-XXXXX) +name="nativefier-smoke-test-1" +resources_dir="$tmp_dir/resources" +mkdir "$resources_dir" +injected_css="$resources_dir/inject.css" +injected_js="$resources_dir/inject.js" +echo '* { background-color: blue; }' > "$injected_css" +echo 'alert("hello world from inject");' > "$injected_js" + +node ./lib/cli.js 'https://npmjs.com/' \ + --inject "$injected_css" \ + --inject "$injected_js" \ + --name "$name" \ + "$tmp_dir" + +printf '\n***** SMOKE TEST 1: Test checklist ***** +- Context menu -> Open Link In New Window works +- MAC ONLY: Context menu -> Open Link In New Tab works +- Keyboard shortcuts: {back, forward, zoom in/out/zero} work +- Console: no Electron runtime deprecation warnings/error logged' +launch_app "$tmp_dir" "$name" +request_feedback "$tmp_dir" + +# ------------------------------------------------------------------------------ + +printf '\n***** SMOKE TEST 2: Setting up test and building app... *****\n' +tmp_dir=$(mktemp -d -t nativefier-manual-test-tray-XXXXX) +name='nativefier-smoke-test-2' +node ./lib/cli.js 'https://google.com/' \ + --name "$name" \ + --tray \ + "$tmp_dir" + +printf '\n***** SMOKE TEST 2: Test checklist ***** +- Should have an app with a tray icon +- Console: no Electron runtime deprecation warnings/error logged' + +launch_app "$tmp_dir" "$name" +request_feedback "$tmp_dir" + +# ------------------------------------------------------------------------------ + +printf '\n***** SMOKE TEST 3: Setting up test and building app... *****\n' +tmp_dir=$(mktemp -d -t nativefier-manual-test-start-in-tray-XXXXX) +name='nativefier-smoke-test-3' +node ./lib/cli.js 'https://google.com/' \ + --name "$name" \ + --tray start-in-tray \ + "$tmp_dir" + +printf '\n***** SMOKE TEST 3: Test checklist ***** +- Should have an app that does not show a window initially, + but will have a tray icon that will show the window. +- Console: no Electron runtime deprecation warnings/error logged' + +launch_app "$tmp_dir" "$name" +request_feedback "$tmp_dir" + +# ------------------------------------------------------------------------------ + +printf '\n***** SMOKE TEST 4: Setting up test and building app... *****\n' +tmp_dir=$(mktemp -d -t nativefier-manual-test-get-media-devices) +name='nativefier-smoke-test-4' +node ./lib/cli.js 'https://meet.jit.si/nativefier-test' \ + --name "$name" \ + "$tmp_dir" + +printf '\n***** SMOKE TEST 4: Test checklist ***** +- Join the Jitsi meeting and try to share your screen + (third button from the left in the bottom bar) +- An overlay should appear where you can select a screen/window to share + This presently does not work in MacOS as you would have to give the app + "Screen Recording" permissions, but you can''t for an app in the temp directory. +- After selecting a screen, a thumbnail of the shared screen should appear on + the top right +- Console: no Electron runtime deprecation warnings/error logged' + +launch_app "$tmp_dir" "$name" +request_feedback "$tmp_dir" diff --git a/.github/nativefier-walkthrough.gif b/.github/nativefier-walkthrough.gif new file mode 100644 index 0000000..6db55a2 Binary files /dev/null and b/.github/nativefier-walkthrough.gif differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..26bb4a1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: ci + +on: + push: + branches: + - master + pull_request: + branches: + - master + +# - Bumping the *minimum* required Node version? You must bump: +# 1. package.json -> engines.node +# 2. package.json -> devDependencies.@types/node +# 3. tsconfig.json -> {target, lib} +# 4. .github/workflows/ci.yml -> node-version +# - Bumping the *maximum* tested Node version? You must bump also: publish.yml +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 20 + uses: actions/setup-node@v2 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + npm-shrinkwrap.json + app/npm-shrinkwrap.json + package-lock.json + app/package-lock.json + - env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build + - run: npm run lint + playwright: + runs-on: windows-latest # Doesn't work on headless ubuntu, and is slow on mac + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 20 + uses: actions/setup-node@v2 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: | + npm-shrinkwrap.json + app/npm-shrinkwrap.json + package-lock.json + app/package-lock.json + - env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build + - run: npm run test:playwright + timeout-minutes: 5 + # Useful to debug PlayWright tests failing in CI + # env: + # DEBUG: pw:browser* + tests: + strategy: + matrix: + node-version: + - '20' + - '16' # the oldest we require in package.json -> engines.node, to check we run on this minimum + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: | + npm-shrinkwrap.json + app/npm-shrinkwrap.json + package-lock.json + app/package-lock.json + - env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build + - run: npm run test:noplaywright diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e3e34b4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,51 @@ +name: publish +on: + release: + types: + - created +jobs: + playwright: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm + with: + node-version: '20' # Align the version of Node here with ci.yml. + registry-url: 'https://registry.npmjs.org' + - run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build + - run: npm run test:playwright + timeout-minutes: 5 + + build: + needs: playwright + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm + with: + node-version: '20' # Align the version of Node here with ci.yml. + registry-url: 'https://registry.npmjs.org' + - run: npm ci --no-fund # Will also (via `prepare` hook): 1. install ./app, 2. build + - run: npm run test:noplaywright + - run: npm run lint + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + docker: + needs: [ playwright, build ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build Docker image + run: docker build . --file Dockerfile --tag "nativefier/nativefier:latest" + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Tag and push Docker image + run: | + docker tag "nativefier/nativefier:latest" "nativefier/nativefier:${GITHUB_REF_NAME}" + docker push "nativefier/nativefier:latest" + docker push "nativefier/nativefier:${GITHUB_REF_NAME}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af1a2ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# OSX +.DS_Store + +# ignore compiled lib files +lib* +app/lib/* +app/dist/* +built-tests + +# commit a placeholder to keep the app/lib directory +app/inject +!app/inject/_placeholder +!app/lib/.placeholder + +dist +package-lock.json +app/package-lock.json + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Python virtual environment in case it's created for the Castlabs code signing tool +venv + +# IntelliJ project files +.idea +*.iml +.run +out +gen + +# Builds when testing npm pack +nativefier*.tgz + +.vscode + +# https://github.com/nektos/act +.actrc + +tsconfig.tsbuildinfo +scripts diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ed9c821 --- /dev/null +++ b/.npmignore @@ -0,0 +1,23 @@ +/* +!lib/ +!icon-scripts +!npm-shrinkwrap.json +.DS_Store +src/ +*eslintrc.js +*eslintrc.yml +*tsconfig.tsbuildinfo +*tsconfig.json +*jestSetupFiles* +*-test.js +*-test.js.map +*.test.d.ts +*.test.js +*.test.js.map +app/* +!app/lib/ +!app/inject/ +!app/nativefier.json +!app/package.json +!app/npm-shrinkwrap.json +.vscode/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..b6a7d89 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16 diff --git a/API.md b/API.md new file mode 100644 index 0000000..8d0952a --- /dev/null +++ b/API.md @@ -0,0 +1,1303 @@ +# API + +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [Packaging Squirrel-based installers](#packaging-squirrel-based-installers) +- [Command Line](#command-line) + - [Target Url](#target-url) + - [[dest]](#dest) + - [Help](#help) + - [Version](#version) + - [App Creation Options](#app-creation-options) + - [[arch]](#arch) + - [[conceal]](#conceal) + - [[electron-version]](#electron-version) + - [[global-shortcuts]](#global-shortcuts) + - [[icon]](#icon) + - [[name]](#name) + - [[no-overwrite]](#no-overwrite) + - [[platform]](#platform) + - [[portable]](#portable) + - [[upgrade]](#upgrade) + - [[widevine]](#widevine) + - [App Window Options](#app-window-options) + - [[always-on-top]](#always-on-top) + - [[background-color]](#background-color) + - [[bookmarks-menu]](#bookmarks-menu) + - [[browserwindow-options]](#browserwindow-options) + - [[disable-context-menu]](#disable-context-menu) + - [[disable-dev-tools]](#disable-dev-tools) + - [[full-screen]](#full-screen) + - [[height]](#height) + - [[hide-window-frame]](#hide-window-frame) + - [[max-height]](#max-height) + - [[max-width]](#max-width) + - [[maximize]](#maximize) + - [[min-height]](#min-height) + - [[min-width]](#min-width) + - [[process-envs]](#process-envs) + - [[show-menu-bar]](#show-menu-bar) + - [[single-instance]](#single-instance) + - [[title-bar-style]](#title-bar-style) + - [[tray]](#tray) + - [[width]](#width) + - [[x]](#x) + - [[y]](#y) + - [[zoom]](#zoom) + - [Internal Browser Options](#internal-browser-options) + - [[file-download-options]](#file-download-options) + - [[inject]](#inject) + - [[lang]](#lang) + - [[user-agent]](#user-agent) + - [[user-agent-honest]](#user-agent-honest) + - [Internal Browser Cache Options](#internal-browser-cache-options) + - [[clear-cache]](#clear-cache) + - [[disk-cache-size]](#disk-cache-size) + - [URL Handling Options](#url-handling-options) + - [[block-external-urls]](#block-external-urls) + - [[internal-urls]](#internal-urls) + - [[internal-login-pages]](#internal-login-pages) + - [[strict-internal-urls]](#strict-internal-urls) + - [[proxy-rules]](#proxy-rules) + - [Auth Options](#auth-options) + - [[basic-auth-username] and [basic-auth-password]](#basic-auth-username-and-basic-auth-password) + - [Graphics Options](#graphics-options) + - [[disable-gpu]](#disable-gpu) + - [[enable-es3-apis]](#enable-es3-apis) + - [[ignore-gpu-blacklist]](#ignore-gpu-blacklist) + - [(In)Security Options](#insecurity-options) + - [[disable-old-build-warning-yesiknowitisinsecure]](#disable-old-build-warning-yesiknowitisinsecure) + - [[ignore-certificate]](#ignore-certificate) + - [[insecure]](#insecure) + - [Platform Specific Options](#platform-specific-options) + - [[app-copyright]](#app-copyright) + - [[app-version]](#app-version) + - [[bounce]](#bounce) + - [[build-version]](#build-version) + - [[counter]](#counter) + - [[darwin-dark-mode-support]](#darwin-dark-mode-support) + - [[fast-quit]](#fast-quit) + - [[win32metadata]](#win32metadata) + - [Debug Options](#debug-options) + - [[crash-reporter]](#crash-reporter) + - [[verbose]](#verbose) + - [[quiet]](#quiet) + - [Flash Options (Deprecated)](#flash-options-deprecated) + - [[flash] and [flash-path] (DEPRECATED)](#flash-and-flash-path-deprecated) +- [Programmatic API](#programmatic-api) +- [Accessing The Electron Session](#accessing-the-electron-session) + - [Important Note On funcArgs](#important-note-on-funcargs) + - [session-interaction-reply](#session-interaction-reply) + - [Errors](#errors) + - [Complex Return Values](#complex-return-values) + - [Example](#example) + +## Packaging Squirrel-based installers + +See [PR #744 - Support packaging nativefier applications into Squirrel-based installers](https://github.com/nativefier/nativefier/pull/744) + +## Command Line + +```bash +nativefier [options] [targetUrl] [dest] +``` + +You must provide: + +- Either a `targetUrl` to generate a new app from it. +- Or option `--upgrade ` to upgrade an existing app. + +Command line options are listed below. + +#### Target Url + +The url to point the application at. + +#### [dest] + +Specifies the destination directory to build the app to. +If no parameter is passed, defaults to the current working directory, +or _[New in 46.0.5]_ the `NATIVEFIER_APPS_DIR` environment variable if set. + +**Tip:** Add `export NATIVEFIER_APPS_DIR=~/Applications/` to your +`~/.bashrc` (or `~/.zshrc` or similar) to set the default app destination +if none is passed. This lets you simply run `nativefier example.com` and +have the app automatically built in your Applications folder. + +#### Help + +``` +-h, --help +``` + +Prints the usage information. + +#### Version + +``` +-v, --version +``` + +Prints the version of your `nativefier` install. + +### App Creation Options + +#### [arch] + +``` +-a, --arch +``` + +The processor architecture to target when building. + +- Default: the architecture of the installed version of node (usually the architecture of the build-time machine). + - To test your default architecture you can run + ``` + node -p "process.arch" + ``` + (See https://nodejs.org/api/os.html#os_os_arch) + - Please note: On M1 Macs, unless an arm64 version of brew is used to install nodejs, the version installed will be an `x64` version run through Rosetta, and will result in an `x64` app being generated. If this is not desired, either specify `-a arm64` to build for M1, or re-install node with an arm64 version of brew. See https://github.com/nativefier/nativefier/issues/1089 +- Can be overridden by specifying one of: `x64`, `armv7l`, `arm64`, or `universal` +- When specifying `universal` you must be building for the `darwin`, `mas`, `mac`, or `osx` platforms. This will generate a universal (M1 and x86) app. + +Note: careful to not conflate _platform_ with _architecture_. If you want for example a Linux or Mac build, it's `--platform` you are looking for. See its documentation for details. + +#### [conceal] + +``` +-c, --conceal +``` + +Specifies if the source code within the nativefied app should be packaged into an archive, defaults to false, [read more](https://www.electronjs.org/docs/latest/glossary#asar). + +#### [electron-version] + +``` +-e, --electron-version +``` + +Electron version without the `v`, see https://github.com/atom/electron/releases. + +#### [icon] + +``` +-i, --icon +``` + +Notes: + +- When packaging for Windows, must be a path to a `.ico` file. +- When packaging for Linux, must be a path to a `.png` file. +- When packaging for macOS, must be a `.icns` or a `.png` file if the [optional dependencies](../README.md#optional-dependencies) are installed. + If your `PATH` has our image-conversion dependencies (`iconutil`, and either ImageMagick `convert` + `identify`, or GraphicsMagick `gm`), Nativefier will automatically convert the `.png` to a `.icns` for you. + +Alternative to macOS users: [iConvertIcons](https://iconverticons.com/online/) can be used to convert `.pngs`, though it can be quite cumbersome. + +To retrieve the `.icns` file from the downloaded file, extract it first and press File > Get Info. Then select the icon in the top left corner of the info window and press `⌘-C`. Open Preview and press File > New from clipboard and save the `.icns` file. It took me a while to figure out how to do that and question why a `.icns` file was not simply provided in the downloaded archive. + +#### [name] + +``` +-n, --name +``` + +The name of the application, which will affect strings in titles and the icon. + +Note to Linux users: do not put spaces if you define the app name yourself with `--name`, as this will cause problems (tested on Ubuntu 14.04) when pinning a packaged app to the launcher. + +#### [no-overwrite] + +``` +--no-overwrite +``` + +Specifies if the destination directory should be not overwritten, defaults to false. + +#### [platform] + +``` +-p, --platform +``` + +- Default: current operating system. + - To test your default platform you can run + ``` + node -p "process.platform" + ``` + (See https://nodejs.org/api/os.html#os_os_platform) +- Can be overwritten by specifying either `linux`, `windows`, `osx` or `mas` for a Mac App Store specific build. + +Note: careful to not conflate _platform_ with _architecture_. If you want for example a 32bit build or an ARM build, it's `--arch` you are looking for. See its documentation for details. + +For backwards compatibility, less-clear values `win32` (for Windows) and `darwin`, `mac` (for macOS) are supported. + +#### [portable] + +``` +--portable +``` + +_[New in 43.1.0]_ Make your app store its user data (cookies, cache, etc) inside the app folder, making it "portable" in the sense popularized by [PortableApps.com](https://portableapps.com/): you can carry it around e.g. on a USB key, and it will work the same with your data. + +_IMPORTANT SECURITY NOTICE_: when creating a portable app, all data accumulated after running the app (including login information, cache, cookies), will be saved in the app folder. If this app is then shared with others, THEY WILL HAVE THAT ACCUMULATED DATA, POTENTIALLY INCLUDING ACCESS TO ANY ACCOUNTS YOU LOGGED INTO. + +→ Best practice to _distribute apps_ using this flag: + +1. Create your application with this flag +2. Test it +3. Delete your application and containing folder +4. Recreate it in the same way you did in step 1 +5. Distribute the app without opening it + +#### [upgrade] + +``` +--upgrade +``` + +_[New in 43.1.0]_ This option will attempt to extract all existing options from the old app, and upgrade it using the current Nativefier CLI. + +_Important data safety note_: This action is an in-place upgrade, and will REPLACE the current application. In case this feature does not work as intended or as the user may wish, it is advised to make a backup of the app to be upgraded before using, or specify an alternate directory as you would when creating a new file.\*\* + +The provided path must be the "executable" of an application packaged with a previous version of Nativefier, and to be upgraded to the latest version of Nativefier. "Executable" means: the `.exe` file on Windows, the executable on Linux, or the `.app` on macOS. The executable must be living in the original context where it was generated (i.e., on Windows and Linux, the exe file must still be in the folder containing the generated `resources` directory). + +#### [widevine] + +``` +--widevine +``` + +_[New in 11.0.2]_ Use a Widevine-enabled version of Electron for DRM playback, see https://github.com/castlabs/electron-releases. + +Note: some sites using Widevine (like Udemy or HBO Max) may still refuse to load videos, and require EVS-signing your Nativefier app to work. Try signing your app using CastLabs tools. See https://github.com/castlabs/electron-releases/wiki/EVS and [#1147](https://github.com/nativefier/nativefier/issues/1147#issuecomment-828750362). TL;DR: + +```bash +# Install CastLabs tools: +pip install --upgrade castlabs-evs + +# Sign up: +python3 -m castlabs_evs.account signup + +# Sign your app +python -m castlabs_evs.vmp sign-pkg Udemy-win32-x64 +``` + +### App Window Options + +#### [always-on-top] + +``` +--always-on-top +``` + +_[New in 7.6.0]_ Enable always on top for the packaged application. + +#### [background-color] + +``` +--background-color +``` + +_[New in 7.7.0]_ See https://electronjs.org/docs/api/browser-window#setting-backgroundcolor + +#### [bookmarks-menu] + +``` +--bookmarks-menu +``` + +_[New in 43.1.0]_ Path to a JSON file defining a bookmarks menu. In addition to containing a list of bookmarks, this file customizes the name of the menu and (optionally) allows assigning keyboard shortcuts to bookmarks. + +This menu is a simple list; folders are not supported. + +Your `menuLabel` can be bound to a `Alt + letter` shortcut using the letter `&` before the `letter` you want. Be careful to not conflict with the letter of other menus! + +Keyboard shortcuts can use the modifier keys `Cmd`, `Ctrl`, `CmdOrCtrl`, `Alt`, `Option`, `AltGr`, `Shift`, and `Super`. See [the Electron documentation](https://www.electronjs.org/docs/api/accelerator) for more information. + +Example of such a JSON file: + +```json +{ + "menuLabel": "&Music", + "bookmarks": [ + { + "title": "lofi.cafe", + "url": "https://lofi.cafe/", + "type": "link", + "shortcut": "CmdOrCtrl+1" + }, + { + "title": "beats to relax/study to", + "url": "https://www.youtube.com/watch?v=5qap5aO4i9A", + "type": "link", + "shortcut": "CmdOrCtrl+2" + }, + { + "type": "separator" + }, + { + "title": "RÜFÜS DU SOL Live from Joshua Tree", + "type": "link", + "url": "https://www.youtube.com/watch?v=Zy4KtD98S2c" + } + ] +} +``` + +#### [browserwindow-options] + +``` +--browserwindow-options +``` + +_[New in 7.7.0]_ A JSON string that will be sent directly into Electron BrowserWindow options. +See [Electron's BrowserWindow API Documentation](https://electronjs.org/docs/api/browser-window#new-browserwindowoptions) for the complete list of options. + +Example: + +```bash +nativefier --browserwindow-options '{ "webPreferences": { "defaultFontFamily": { "standard": "Comic Sans MS", "serif": "Comic Sans MS" } } }' +``` + +#### [disable-context-menu] + +``` +--disable-context-menu +``` + +Disable the context menu + +#### [disable-dev-tools] + +``` +--disable-dev-tools +``` + +Disable the Chrome developer tools + +#### [full-screen] + +``` +--full-screen +``` + +Makes the packaged app start in full screen. + +#### [height] + +``` +--height +``` + +Height of the packaged application, defaults to `800px`. + +#### [hide-window-frame] + +``` +--hide-window-frame +``` + +Disable window frame and controls. + +#### [max-height] + +``` +--max-height +``` + +Maximum height of the packaged application, default is no limit. + +#### [max-width] + +``` +--max-width +``` + +Maximum width of the packaged application, default is no limit. + +#### [maximize] + +``` +--maximize +``` + +Makes the packaged app start maximized. + +#### [min-height] + +``` +--min-height +``` + +Minimum height of the packaged application, defaults to `0`. + +#### [min-width] + +``` +--min-width +``` + +Minimum width of the packaged application, defaults to `0`. + +#### [process-envs] + +``` +--process-envs +``` + +a JSON string of key/value pairs to be set as environment variables before any browser windows are opened. + +Example: + +```bash +nativefier --process-envs '{"GOOGLE_API_KEY": ""}' +``` + +#### [show-menu-bar] + +``` +-m, --show-menu-bar +``` + +Specifies if the menu bar should be shown. + +#### [single-instance] + +``` +--single-instance +``` + +Prevents application from being run multiple times. If such an attempt occurs the already running instance is brought to front. + +#### [title-bar-style] + +``` +--title-bar-style +``` + +_[New in 7.6.4]_ (macOS only) Sets the style for the app's title bar. See more details at electron's [Frameless Window](https://www.electronjs.org/pt/docs/latest/api/frameless-window) documentation. + +Consider injecting a custom CSS (via `--inject`) for better integration. Specifically, the CSS should specify a draggable region. For instance, if the target website has a `
` element, you can make it draggable like so. + +```css +/* site.css */ + +/* header is draggable... */ +header { + -webkit-app-region: drag; +} + +/* but any buttons inside the header shouldn't be draggable */ +header button { + -webkit-app-region: no-drag; +} + +/* perhaps move some items out of way for the traffic light */ +header div:first-child { + margin-left: 100px; + margin-top: 25px; +} +``` + +```sh +nativefier http://google.com --inject site.css --title-bar-style 'hiddenInset' +``` + +#### [tray] + +``` +--tray [start-in-tray] +``` + +_[New in 7.5.0]_ Application will stay as an icon in the system tray. Prevents application from being closed from clicking the window close button. + +When the optional argument `start-in-tray` is provided, i.e. the application is started using `--tray start-in-tray`, the main window will not be shown on first start. + +Limitation: when creating a macOS app using option `--tray`, from a non-macOS build machine, the tray icon (in the menu bar) will be invisible. + +#### [width] + +``` +--width +``` + +Width of the packaged application, defaults to `1280px`. + +#### [x] + +``` +--x +``` + +X location of the packaged application window. + +#### [y] + +``` +--y +``` + +_[New in 7.6.0]_ Y location of the packaged application window. + +#### [zoom] + +``` +--zoom +``` + +_[New in 7.6.0]_ Sets a default zoom factor to be used when the app is opened, defaults to `1.0`. + +### Internal Browser Options + +#### [file-download-options] + +``` +--file-download-options +``` + +_[New in 7.6.0]_ A JSON string of key/value pairs to be set as file download options. See [electron-dl](https://github.com/sindresorhus/electron-dl) for available options. + +Example: + +```bash +nativefier --file-download-options '{"saveAs": true}' +``` + +#### [global-shortcuts] + +``` +--global-shortcuts shortcuts.json +``` + +_[New in 7.6.9]_ Register global shortcuts which will trigger input events like key presses or pointer events in the application. + +You may define multiple global shortcuts which can trigger a series of input events. It has the following structure: + +```js +[ + { + // Key is passed as first argument to globalShortcut.register + key: 'CommandOrControl+Shift+Z', + // The input events exactly match the event config in Electron for contents.sendInputEvent(event) + inputEvents: [ + { + // Available event types: mouseDown, mouseUp, mouseEnter, mouseLeave, contextMenu, mouseWheel, mouseMove, keyDown, keyUp or char + type: 'keyDown', + // Further config depends on your event type. See docs at: https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentssendinputeventevent + keyCode: 'Space', + }, + ], + }, +]; +``` + +_Note regarding modifier keys:_ If you want to trigger key events which include a modifier (Ctrl, Shift,...), you need to keyDown the modifier key first, then keyDown the actual key _including_ the modifier key as modifier property and then keyUp both keys again. No idea what this means? See the example for `MediaPreviousTrack` below! For more details, please see the Electron documentation: + +- List of available keys: https://github.com/electron/electron/blob/master/docs/api/accelerator.md +- Details about how to create input event objects: https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentssendinputeventevent + +_Note about Global Shortcuts on macOS_ + +On MacOS 10.14+, if you have set a global shortcut that includes a Media key, the user will need to be prompted for permissions to enable these keys in System Preferences > Security & Privacy > Accessibility. + +Example `shortcuts.json` for `https://deezer.com` & `https://soundcloud.com` to get your play/pause/previous/next media keys working: + +```json +[ + { + "key": "MediaPlayPause", + "inputEvents": [ + { + "type": "keyDown", + "keyCode": "Space" + } + ] + }, + { + "key": "MediaPreviousTrack", + "inputEvents": [ + { + "type": "keyDown", + "keyCode": "Shift" + }, + { + "type": "keyDown", + "keyCode": "Left", + "modifiers": ["shift"] + }, + { + "type": "keyUp", + "keyCode": "Left", + "modifiers": ["shift"] + }, + { + "type": "keyUp", + "keyCode": "Shift" + } + ] + }, + { + "key": "MediaNextTrack", + "inputEvents": [ + { + "type": "keyDown", + "keyCode": "Shift" + }, + { + "type": "keyDown", + "keyCode": "Right", + "modifiers": ["shift"] + }, + { + "type": "keyUp", + "keyCode": "Right", + "modifiers": ["shift"] + }, + { + "type": "keyUp", + "keyCode": "Shift" + } + ] + } +] +``` + +#### [inject] + +``` +--inject +``` + +Allows you to inject JavaScript or CSS files. This command can be repeated multiple times to inject multiple files. + +_Note about JS injection:_ injected JS is loaded _after_ `DOMContentLoaded`, so you can assume the DOM is complete & available. + +_Note about CSS injection:_ to override existing CSS rules, you need to use the `!important` CSS keyword. Example: `#id_to_hide { display: none !important; }` , not just `#id_to_hide { display: none; }` . + +Example: + +```bash +nativefier http://google.com --inject ./some-js-injection.js --inject ./some-css-injection.css ~/Desktop +``` + +#### [lang] + +``` +--lang +``` + +Set the language or locale to render the web site as (e.g., "fr", "en-US", "es", etc.) + +#### [user-agent] + +``` +-u, --user-agent +``` + +Set the user agent to run the created app with. Use `--user-agent-honest` to use the true Electron user agent. + +_[New in 44.0.0]_ The following short codes are also supported to generate a user agent: `edge`, `firefox`, `safari`. + +- `edge` will generate a Microsoft Edge user agent matching the Chrome version of Electron being used +- `firefox` will generate a Mozilla Firefox user agent matching the latest stable release of that browser +- `safari` will generate an Apple Safari user agent matching the latest stable release of that browser + +#### [user-agent-honest] + +``` +--user-agent-honest, --honest +``` + +By default, Nativefier uses a preset user agent string for your OS and masquerades as a regular Google Chrome browser, so that for some sites, it will not say that the current browser is unsupported. + +If this flag is passed, it will not override the user agent, and use Electron's default generated one for your app. + +### Internal Browser Cache Options + +#### [clear-cache] + +``` +--clear-cache +``` + +_[New in 7.6.11]_ Prevents the application from preserving cache between launches. + +#### [disk-cache-size] + +``` +--disk-cache-size +``` + +_[New in 7.4.1]_ Forces the maximum disk space to be used by the disk cache. Value is given in bytes. + +### URL Handling Options + +#### [block-external-urls] + +``` +--block-external-urls +``` + +Forbid navigation to URLs not considered "internal" (see '--internal-urls'). Instead of opening in an external browser, attempts to navigate to external URLs will be blocked, and an error message will be shown. Default: false + +Example: + +```bash +nativefier https://google.com --internal-urls ".*?\.google\.*?" --block-external-urls +``` + +Blocks navigation to any URLs except Google and its subdomains. + +#### [internal-urls] + +``` +--internal-urls +``` + +Regular expression of URLs to consider "internal" while following a hyperlink. +Internal URLs will open in Nativefier, other URLs will open in your preferred browser. + +Defaults to view as "internal" two URLs that share the same base domain, +once stripped of `www.`. For example, by default, + +- URLs from/to `foo.com`, `app.foo.com`, `www.foo.com` are considered internal. +- URLs from/to `abc.com` and `xyz.com` are considered external. + +Example of `--internal-urls` causing all links to Google to be considered internal: + +```bash +nativefier https://google.com --internal-urls ".*?\.google\.*?" +``` + +To turn off base domain matching, use [`--strict-internal-urls`](#strict-internal-urls). Or, if you never expect Nativefier to open an "external" page in your OS browser, + +```bash +nativefier https://google.com --internal-urls ".*?" +``` + +##### Internal Login Pages + +_[New in 43.0.0]_ Finally, URLs for known login pages +are considered internal. This does not replace `internal-urls`, it complements +it, and happens _before_ your `internal-urls` rule is applied. So, if you +already set the flag to let such auth pages open internally, you don't need to +change it but it might be unnecessary. + +Current known internal login pages: + +- `amazon.com/signin` +- `appleid.apple.com/auth/authorize` +- `id.atlassian.com` , `auth.atlassian.com` +- `facebook.com/login` +- `github.com/login` , `github.com/session` +- `accounts.google.com` , `mail.google.com/accounts/SetOSID` +- `linkedin.com/uas/login` +- `login.live.com` , `login.microsoftonline.com` +- `okta.com` +- `twitter.com/oauth/authenticate` +- `workspaceair.com` +- `securid.com` + +Note: While .com is specified, for most of these we try to match even on non-US +based domains such as `.co.uk` as well + +If you think this list is missing a login page that you think should be internal, feel free to submit an [issue](https://github.com/nativefier/nativefier/issues/new?assignees=&labels=bug&template=bug_report.md&title=[New%20internal%20login%20page%20request]%20Your%20login%20page%20here) or even better a pull request! + +#### [strict-internal-urls] + +``` +--strict-internal-urls +``` + +Disables base domain matching when determining if a link is internal. Only the `--internal-urls` regex and login pages will be matched against, so `app.foo.com` will be external to `www.foo.com` unless it matches the `--internal-urls` regex. + + +#### [proxy-rules] + +``` +--proxy-rules +``` + +_[New in 7.7.1]_ See [Electron proxyRules](https://electronjs.org/docs/api/session?q=proxy#sessetproxyconfig-callback) for more details. + +Example: + +```bash +nativefier https://google.com --proxy-rules http://127.0.0.1:1080 +``` + +### Auth Options + +#### [[basic-auth-username] and [basic-auth-password]] + +``` +--basic-auth-username --basic-auth-password +``` + +_[New in 7.5.0]_ Set basic http(s) auth via the command line to have the app automatically log you in to a protected site. Both fields are required if one is set. + +### Graphics Options + +#### [disable-gpu] + +``` +--disable-gpu +``` + +_[New in 7.6.2]_ Disable hardware acceleration for the packaged application. + +#### [enable-es3-apis] + +``` +--enable-es3-apis +``` + +_[New in 7.4.1]_ Passes the enable-es3-apis flag to the Chrome engine, to force the activation of WebGl 2.0. + +#### [ignore-gpu-blacklist] + +``` +--ignore-gpu-blacklist +``` + +_[New in 7.4.1]_ Passes the ignore-gpu-blacklist flag to the Chrome engine, to allow for WebGl apps to work on non supported graphics cards. + +### (In)Security Options + +#### [disable-old-build-warning-yesiknowitisinsecure] + +Disables the warning shown when opening a Nativefier app made a long time ago, using an old and probably insecure Electron. Nativefier uses the Chrome browser (through Electron), and remaining on an old version is A. performance sub-optimal and B. dangerous. + +However, there are legitimate use cases to disable such a warning. For example, if you are using Nativefier to ship a kiosk app exposing an internal site (over which you have control). Under those circumstances, it is reasonable to disable this warning that you definitely don't want end-users to see. + +More description about the options for `nativefier` can be found at the above [section](#command-line). + +#### [ignore-certificate] + +``` +--ignore-certificate +``` + +Forces the packaged app to ignore certificate errors. + +#### [insecure] + +``` +--insecure +``` + +Forces the packaged app to ignore web security errors, such as [Mixed Content](https://developer.mozilla.org/en-US/docs/Security/Mixed_content) errors when receiving HTTP content on a HTTPS site. + +### Platform Specific Options + +#### [app-copyright] + +``` +--app-copyright +``` + +_[New in 7.5.0]_ The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata property on Windows, and `NSHumanReadableCopyright` on OS X. + +#### [app-version] + +``` +--app-version +``` + +_[New in 7.5.0]_ (macOS and Windows only) The release version of the application. By default the `version` property in the `package.json` is used but it can be overridden with this argument. If neither are provided, the version of Electron will be used. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on OS X. + +#### [bounce] + +``` +--bounce +``` + +_[New in 7.6.2]_ (macOS only) When the counter increases, the dock icon will bounce for one second. This only works if the `--counter` option is active. + +#### [build-version] + +``` +--build-version +``` + +_[New in 7.5.0]_ (macOS and Windows only) The build version of the application. Maps to the `FileVersion` metadata property on Windows, and `CFBundleVersion` on OS X. + +#### [counter] + +``` +--counter +``` + +(macOS only) Use a counter that persists even with window focus for the application badge for sites that use an "(X)" format counter in the page title (i.e. Gmail). + +#### [darwin-dark-mode-support] + +``` +--darwin-dark-mode-support +``` + +(macOS only) Enables Dark Mode support on macOS 10.14+. + +#### [fast-quit] + +``` +-f, --fast-quit +``` + +(macOS only) Specifies to quit the app after closing all windows, defaults to false. + +### Debug Options + +#### [crash-reporter] + +``` +--crash-reporter +``` + +Enables crash reporting and set the URL to submit crash reports to + +Example: + +```bash +nativefier http://google.com --crash-reporter https://electron-crash-reporter.appspot.com/PROJECT_ID/create/ +``` + +#### [verbose] + +``` +--verbose +``` + +Shows detailed logs in the console. + +#### [quiet] + +``` +--quiet +``` + +Suppress all log output. If both `verbose` and `quiet` are passed to the CLI, `verbose` will take precedence. + +#### [win32metadata] + +``` +--win32metadata +``` + +a JSON string of key/value pairs of application metadata (ProductName, InternalName, FileDescription) to embed into the executable (Windows only). + +Example: + +```bash +nativefier --win32metadata '{"ProductName": "Your Product Name", "InternalName", "Your Internal Name", "FileDescription": "Your File Description"}' +``` + +### Flash Options (DEPRECATED) + +#### [flash] and [flash-path] (DEPRECATED) + +_DEPRECATED as of 2021-03-10, will be removed at some point_: There's nothing Nativefier can do to stop this treadmill, so here it goes. +Flash is triply dead upstream: at Adobe, in Chrome, and now in Electron. +Nativefier 43.0.0 was just released, and defaults to Electron 12, which +[removes support for Flash](https://www.electronjs.org/blog/electron-12-0#breaking-changes): + +> Removed Flash support: Chromium has removed support for Flash, which was also +> removed in Electron 12. See [Chromium's Flash Roadmap](https://www.chromium.org/flash-roadmap). + +Your best bet now is on [Ruffle, "a Flash Player emulator built in Rust"](https://ruffle.rs/). +It's usable to play `.swf`s, and that's [what Archive.org does](https://blog.archive.org/2020/11/19/flash-animations-live-forever-at-the-internet-archive/). +It's an emulator, so it's not the real perfect deal, but it already works well +for many swfs, and will get better with time. + +You _might_ still be able to use Nativefier's existing Flash flags while they work, +by adding a `--electron-version 11.3.0` to your flags, but it's only downhill +from here and our Flash flags will be removed at some point in the future, +when maintaining compatibility with old Electrons becomes impossible. + +``` +--flash +``` + +If `--flash` is specified, Nativefier will automatically try to determine the +location of your Google Chrome flash binary. Take note that the version of Chrome +on your computer should be the same as the version used by the version of Electron +for the Nativefied package. + +Note that if this flag is specified, the `--insecure` flag will be added automatically, +to prevent Mixed Content errors on sites such as [Twitch.tv](https://www.twitch.tv/). + +``` +--flash-path +``` + +You can also specify the path to the Chrome flash plugin directly with this flag. +The path can be found at [chrome://plugins](chrome://plugins), under +`Adobe Flash Player` > `Location`. This flag automatically enables the `--flash` flag. + +## Programmatic API + +In addition to CLI flags, Nativefier offers a programmatic Node.js API. + +```bash +# install and save to package.json +npm install --save nativefier +``` + +In your `.js` file: + +```javascript +var nativefier = require('nativefier').default; + +// possible options, defaults unless specified otherwise +var options = { + name: 'Web WhatsApp', // will be inferred if not specified + targetUrl: 'http://web.whatsapp.com', // required + platform: 'darwin', // defaults to the current system + arch: 'x64', // defaults to the current system + version: '0.36.4', + out: '.', + overwrite: false, + asar: false, // see conceal + icon: '~/Desktop/icon.png', + counter: false, + bounce: false, + width: 1280, + height: 800, + showMenuBar: false, + fastQuit: false, + userAgent: 'Mozilla ...', // will infer a default for your current system + ignoreCertificate: false, + ignoreGpuBlacklist: false, + enableEs3Apis: false, + internalUrls: '.*?', + blockExternalUrls: false, + insecure: false, + honest: false, + zoom: 1.0, + singleInstance: false, + clearCache: false, + fileDownloadOptions: { + saveAs: true, // always show "Save As" dialog + }, + processEnvs: { + GOOGLE_API_KEY: '', + }, +}; + +nativefier(options, function (error, appPath) { + if (error) { + console.error(error); + return; + } + console.log('App has been nativefied to', appPath); +}); +``` + +## Accessing The Electron Session + +Sometimes there are Electron features that are exposed via the [Electron `session` API](https://www.electronjs.org/docs/api/session), that may not be exposed via Nativefier options. These can be accessed with an injected javascript file (via the `--inject` command line argument when building your application). Within that javascript file, you may send an ipcRenderer `session-interaction` event, and listen for a `session-interaction-reply` event to get any result. Session properties and functions can be accessed via this event. This event takes an object as an argument with the desired interaction to be performed. + +**Warning**: using this feature in an `--inject` script means using Electron's `session` API, which is not a standard web API and subject to potential [Breaking Changes](https://www.electronjs.org/docs/breaking-changes) at each major Electron upgrade. + +To get a `session` property: + +```javascript +const electron = require('electron'); + +const request = { + property: 'availableSpellCheckerLanguages', +}; +electron.ipcRenderer.send('session-interaction', request); +``` + +To set a `session` property: + +```javascript +const electron = require('electron'); + +const request = { + property: 'spellCheckerEnabled', + propertyValue: true, +}; +electron.ipcRenderer.send('session-interaction', request); +``` + +To call a `session` function: + +```javascript +const electron = require('electron'); + +const request = { + func: 'clearCache', +}; +electron.ipcRenderer.send('session-interaction', request); +``` + +To call a `session` function, with arguments: + +```javascript +const electron = require('electron'); + +const request = { + func: 'setDownloadPath', + funcArgs: [`/home/user/downloads`], +}; +electron.ipcRenderer.send('session-interaction', request); +``` + +If neither a `func` nor a `property` is provided in the event, an error will be returned. + +### Important Note On funcArgs + +PLEASE NOTE: `funcArgs` is ALWAYS an array of arguments to be passed to the function, even if it is just one argument. If `funcArgs` is omitted from a request with a `func` provided, no arguments will be passed. + +### session-interaction-reply + +The results of the call, if desired, can be accessed one of two ways. Either you can listen for a `session-interaction-reply` event, and access the resulting value like so: + +```javascript +const electron = require('electron'); + +const request = { + property: 'availableSpellCheckerLanguages', +}; +electron.ipcRenderer.send('session-interaction', request); + +electron.ipcRenderer.on('session-interaction-reply', (event, result) => { + console.log('session-interaction-reply', event, result.value); +}); +``` + +Or the result can be retrieved synchronously, though this is not recommended as it may cause slowdowns and freezes in your apps while the app stops and waits for the result to be returned. Heed this [warning from Electron](https://www.electronjs.org/docs/api/ipc-renderer): + +> ⚠️ WARNING: Sending a synchronous message will block the whole renderer process until the reply is received, so use this method only as a last resort. It's much better to use the asynchronous version. + +```javascript +const electron = require('electron'); + +const request = { + property: 'availableSpellCheckerLanguages', +}; +console.log( + electron.ipcRenderer.sendSync('session-interaction', request).value, +); +``` + +### Request IDs + +If desired, an id for the request may be provided to distinguish between event replies: + +```javascript +const electron = require('electron'); + +const request = { + id: 'availableSpellCheckerLanguages', + property: 'availableSpellCheckerLanguages', +}; +electron.ipcRenderer.send('session-interaction', request); + +electron.ipcRenderer.on('session-interaction-reply', (event, result) => { + console.log('session-interaction-reply', event, result.id, result.value); +}); +``` + +### Errors + +If an error occurs while handling the interaction, it will be returned in the `session-interaction-reply` event inside the result: + +```javascript +const electron = require('electron'); + +electron.ipcRenderer.on('session-interaction-reply', (event, result) => { + console.log('session-interaction-reply', event, result.error); +}); + +electron.ipcRenderer.send('session-interaction', { + func: 'thisFunctionDoesNotExist', +}); +``` + +### Complex Return Values + +Due to the nature of how these events are transmitted back and forth, session functions and properties that return full classes or class instances are not supported. + +For example, the following code will return an error instead of the expected value: + +```javascript +const electron = require('electron'); + +const request = { + id: 'cookies', + property: 'cookies', +}; +electron.ipcRenderer.send('session-interaction', request); + +electron.ipcRenderer.on('session-interaction-reply', (event, result) => { + console.log('session-interaction-reply', event, result); +}); +``` + +### Example + +This javascript, when injected as a file via `--inject`, will attempt to call the `isSpellCheckerEnabled` function to make sure the spell checker is enabled, enables it via the `spellCheckerEnabled` property, gets the value of the `availableSpellCheckerLanguages` property, and finally will call `setSpellCheckerLanguages` to set the `fr` language as the preferred spellcheck language if it's supported. + +```javascript +const electron = require('electron'); + +electron.ipcRenderer.on('session-interaction-reply', (event, result) => { + console.log('session-interaction-reply', event, result); + switch (result.id) { + case 'isSpellCheckerEnabled': + console.log('SpellChecker enabled?', result.value); + if (result.value === true) { + console.log('Getting supported languages...'); + electron.ipcRenderer.send('session-interaction', { + id: 'availableSpellCheckerLanguages', + property: 'availableSpellCheckerLanguages', + }); + } else { + console.log('SpellChecker disabled. Enabling...'); + electron.ipcRenderer.send('session-interaction', { + id: 'setSpellCheckerEnabled', + property: 'spellCheckerEnabled', + propertyValue: true, + }); + } + break; + case 'setSpellCheckerEnabled': + console.log( + 'SpellChecker has now been enabled. Getting supported languages...', + ); + electron.ipcRenderer.send('session-interaction', { + id: 'availableSpellCheckerLanguages', + property: 'availableSpellCheckerLanguages', + }); + break; + case 'availableSpellCheckerLanguages': + console.log('Avaliable spellChecker languages:', result.value); + if (result.value.indexOf('fr') > -1) { + electron.ipcRenderer.send('session-interaction', { + id: 'setSpellCheckerLanguages', + func: 'setSpellCheckerLanguages', + funcArgs: [['fr']], + }); + } else { + console.log( + "Not changing spellChecker language. 'fr' is not supported.", + ); + } + break; + case 'setSpellCheckerLanguages': + console.log('SpellChecker language was set.'); + break; + default: + console.error('Unknown reply id:', result.id); + } +}); + +electron.ipcRenderer.send('session-interaction', { + id: 'isSpellCheckerEnabled', + func: 'isSpellCheckerEnabled', +}); +``` diff --git a/CATALOG.md b/CATALOG.md new file mode 100644 index 0000000..c0278d6 --- /dev/null +++ b/CATALOG.md @@ -0,0 +1,300 @@ +# Build Commands Catalog + +Below you'll find a list of build commands contributed by the Nativefier community. They are here as examples, to help you nativefy "complicated" apps that need a bit of elbow grease to work. We need your help to enrich it, as long as you follow these two guidelines: + +1. Only add sites that require something special! No need to document here that `simplesite.com` works with a simple `nativefier simplesite.com` 🙂. +2. Please add commands with the _strict necessary_ to make an app work. For example, + - Yes to mention that `--widevine` or some `--browserwindow-options` are necessary... + - ... but don't add other flags that are pure personal preference (e.g. `--disable-dev-tools` or `--disk-cache-size`). + +--- + +## General recipes + +### Videos don’t play + +Some sites like [HBO Max](https://github.com/nativefier/nativefier/issues/1153) and [Udemy](https://github.com/nativefier/nativefier/issues/1147) host videos using [DRM](https://en.wikipedia.org/wiki/Digital_rights_management). + +For those, try passing the [`--widevine`](API.md#widevine) option. + +### Settings cached between app rebuilds + +You might be surprised to see settings persist after rebuilding your app. +This occurs because the app cache lives separately from the app. + +Try deleting your app's cache, found at `-nativefier-` in your OS’s "App Data" directory (Linux: `$XDG_CONFIG_HOME` or `~/.config` , MacOS: `~/Library/Application Support/` , Windows: `%APPDATA%` or `C:\Users\yourprofile\AppData\Roaming`) + +### Window size and position + +This allows the last set window size and position to be remembered and applied +after your app is restarted. Note: PR welcome for a built-in fix for that :) . + +```sh +nativefier 'https://open.google.com/' + --inject window.js +``` + +Note: [Inject](https://github.com/nativefier/nativefier/blob/master/API.md#inject) +the following javascript as `windows.js` to prevent the window size and position to reset. +```javascript +function storeWindowPos() { + window.localStorage.setItem('windowX', window.screenX); + window.localStorage.setItem('windowY', window.screenY); +} +window.moveTo(window.localStorage.getItem('windowX'), window.localStorage.getItem('windowY')); +setInterval(storeWindowPos, 250); +``` + +--- + +## Site-specific recipes + +### Google apps + +Lying about the User Agent is required, else Google Login will notice your +"Chrome" isn't a real Chrome, and will: 1. Refuse login, 2. Break notifications. + +This example documents Google Sheets, but is applicable to other Google apps, +e.g. Google Calendar, GMail, etc. If `firefox` doesn’t work, try `safari` . + +```sh +nativefier 'https://docs.google.com/spreadsheets' \ + --user-agent firefox +``` + +### Outlook + +```sh +nativefier 'https://outlook.office.com/mail' + --internal-urls '.*?(outlook.live.com|outlook.office365.com).*?' + --file-download-options '{"saveAs": true}' + --browserwindow-options '{"webPreferences": { "webviewTag": true, "nodeIntegration": true, "nodeIntegrationInSubFrames": true } }' +``` + +Note: `--browserwindow-options` is needed to allow pop-outs when creating/editing an email. + +### Udemy + +```sh +nativefier 'https://www.udemy.com/' + --internal-urls '.*?udemy.*?' + --file-download-options '{"saveAs": true}' + --widevine +``` + +Note: most videos will work, but to play some DRMed videos you must pass `--widevine` AND [sign the app](https://github.com/nativefier/nativefier/issues/1147#issuecomment-828750362). + +### HBO Max + +```sh +nativefier 'https://play.hbomax.com/' + --widevine + --enable-es3-apis +&& python -m castlabs_evs.vmp sign-pkg 'name_of_the_generated_hbo_app' +``` + +Note: as for Udemy, `--widevine` + [app signing](https://github.com/nativefier/nativefier/issues/1147#issuecomment-828750362) is necessary. + +### WhatsApp + +```sh +nativefier 'https://web.whatsapp.com/' + --inject whatsapp.js +``` + +With this `--inject` in `whatsapp.js` (and maybe more, see [#1112](https://github.com/nativefier/nativefier/issues/1112)): + +```javascript +if ('serviceWorker' in navigator) { + caches.keys().then(function (cacheNames) { + cacheNames.forEach(function (cacheName) { + caches.delete(cacheName); + }); + }); +} +``` + +Another option to see WhatsApp or WhatsApp Business more macOS-like (macos only): + +```sh +nativefier https://web.whatsapp.com --name 'WhatsApp Business' --counter true --darwin-dark-mode-support true --title-bar-style hidden --inject whatsappmacos.css +``` + +with this `whatsappmacos.css` to make the window draggable, and move the user avatar to the right: +```css +header > div:first-child { + flex: 0 0 auto; + margin-right: 15px; +} +div#app > div.os-mac > span:first-child { + position: fixed; + top: 0; + z-index: 1000; + width: 100%; + height: 59px; + pointer-events: none; + -webkit-app-region: drag; +} +``` + +### Spotify + +```sh +nativefier 'https://open.spotify.com/' + --widevine + --inject spotify.js + --inject spotify.css +``` + +Notes: + +- You might have to pass `--user-agent firefox` to circumvent Spotify's detection that your browser isn't a real Chrome. But [maybe not](https://github.com/nativefier/nativefier/issues/1195#issuecomment-855003776). +- [Inject](https://github.com/nativefier/nativefier/blob/master/API.md#inject) the following javascript as `spotify.js` to prevent "Unsupported Browser" messages. + +```javascript +function dontShowBrowserNoticePage() { + const browserNotice = document.getElementById('browser-support-notice'); + console.log({ browserNotice }); + if (browserNotice) { + // When Spotify displays the browser notice, it's not just the notice, + // but the entire page is focused on not allowing you to proceed. + // So in this case, we hide the body element (so nothing shows) + // until our JS deletes the service worker and reload (which will actually load the player) + document.getElementsByTagName('body')[0].style.display = 'none'; + } +} + +function reload() { + window.location.href = window.location.href; +} + +function nukeWorkers() { + dontShowBrowserNoticePage(); + if ('serviceWorker' in navigator) { + caches.keys().then(function (cacheNames) { + cacheNames.forEach(function (cacheName) { + console.debug('Deleting cache', cacheName); + caches.delete(cacheName); + }); + }); + navigator.serviceWorker.getRegistrations().then((registrations) => { + registrations.forEach((worker) => + worker + .unregister() + .then((u) => { + console.debug('Unregistered worker', worker); + reload(); + }) + .catch((e) => + console.error('Unable to unregister worker', error, { worker }), + ), + ); + }); + } +} + +document.addEventListener('DOMContentLoaded', () => { + nukeWorkers(); +}); + +if (document.readyState === 'interactive') { + nukeWorkers(); +} +``` + +- It is also required to [sign the app](https://github.com/nativefier/nativefier/blob/master/API.md#widevine), or many songs will not play. +- To hide all download links (as if you were in the actual app), [inject](https://github.com/nativefier/nativefier/blob/master/API.md#inject) the following CSS as `spotify.css`: + +```css +a[href='/download'] { + display: none; +} +``` + + +### Notion + +You can use Notion pages with Nativefier without much hassle, but Notion itself does not present an easy way to use HTML buttons. As such, if you want to use Notion Pages as a quick way to make dashboards and interactive panels, you will be restricted to only plain links and standard components. + +With Nativefier you can now extend Notion's functionality and possibilities by adding HTML buttons that can call other javascript functions, since it enables you to inject custom Javascript and CSS. + +```sh +nativefier 'YOUR_NOTION_PAGE_SHARE_URL' + --inject notion.js + --inject notion.css +``` + +Notes: + +- You can inject the notion.js and notion.css files by copying them to the resources/app/inject folder of your nativefier app. +- In your Notion page, use [notionbutton]BUTTON_TEXT|BUTTON_ACTION[/notionbutton], where BUTTON_TEXT is the text contained in your button and BUTTON_ACTION is the action which will be called in your JS function. +```javascript +/* notion.js */ + +// First, we replace all placeholders in our Notion page to add our interactive buttons to it. +window.onload = + setTimeout(function(){ + let htmlCode = document.body.getElementsByTagName("*"); + for (let i = 0; i <= htmlCode.length; i++) { + if(htmlCode[i] && htmlCode[i].innerHTML){ + let match = htmlCode[i].innerHTML.match(/\[notionbutton\]([\s\S]*?)\[\/notionbutton\]/); + if (match && typeof match == 'object'){ + let btnarray = match['1'].split("|"); + let btn_text = btnarray[0]; + let btn_action = btnarray[1]; + htmlCode[i].innerHTML = htmlCode[i].innerHTML.replace(match['0'], ""); + } + } + } + let buttons = document.querySelectorAll(".btn-notion"); + for (let j=0; j <= buttons.length; j++){ + if(buttons[j].hasAttribute("btnaction")){ + buttons[j].onclick = function () { runAction(buttons[j].getAttribute("btnaction")) }; + } + } + }, 3000); + +// And then we define your action below, according to our needs +function runAction(action) { + switch(action){ + case '1': + alert('Nice One!'); + break; + default: + alert('Hello World!'); + } +} +``` + +After that, set your css file as follows: +```css +.notion-topbar{ /* hiding notion's default navigation bar for a more "app" feeling */ + display:none; +} +.btn-notion{ /* defining some style for our buttons */ + background-color:#FFC300; + color: #333333; +} +.notion-selectable.notion-page-block.notion-collection-item span{ + pointer-events: auto !important; /* notion prevents clicks on items inside databases. Use this to remove that. */ +} +``` + +### Microsoft Teams + +You can get an almost macOS look-alike using this: + +```sh +nativefier https://teams.microsoft.com --name 'Microsoft Teams' --counter true --darwin-dark-mode-support true --title-bar-style hidden --internal-urls "(.*)" --inject teamsapp.css +``` +Note that the `--internal-urls` argument is necessary to login. + +Inject the following `teamsapp.css` file to hide the download button at the bottom left and the Office 365 apps waffle button at the top left: +```css +get-app-button.ts-sym.app-bar-link { + display: none; +} +button#ts-waffle-button { + display: none; +} +``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8886d8d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1205 @@ + +52.0.0 / 2023-08-25 +=================== + **[BREAKING]** + * Update to Electron 25.7 (#1566) + * Update to Electron 25 (#1559) + +51.0.1 / 2023-08-04 +=================== + + * `npm i` in the Dockerfile to esnure we have what we need to build + test (#1557) + +51.0.0 / 2023-08-03 +=================== + **[BREAKING]** + * Update Electron to 21 + Node to 16 (#1550) + * Update link to Development Guide (#1544) + +50.1.1 / 2023-03-27 +=================== + + * Fix shrinkwrap versions back to lockfileVersion 1 (node 12) + * Fix typo "electon" -> "electron" (#1492) + +50.1.0 / 2023-03-24 +=================== + + * Update outdated shrinkwrap files + * Add getDisplayMedia and PipeWire support (#1477) + +50.0.1 / 2022-11-07 +=================== + +* Windows: Fix "Maximize window visual glitch" (fix #1447) (PR #1448) +* External URL protocols: add zoommtg as no-confirmation (PR #1463) +* CATALOG.md: MS Teams CSS inject (PR #1469), WhatsApp native macOS look CSS (PR #1468) +* Bump default Electron from 19.0.17 to 19.1.4, with security fixe +* CI: test on 12 and **19**, now that 19 is out +* Upgrade CLI & App dependencies + +50.0.0 / 2022-09-17 +=================== + +**[BREAKING]** Add validation to opening external URLs in desktop handler (fix #1459) +This will, for security, refuse loading of certain external of two kinds. +One: using dubious URL schemes, two: including nasty characters. +Blocking URLs will be accompanied by a window explaining what's going on, +and linking to a discussion thread where you can report false positives. +Hopefully not _BREAKING_ much (the behavior should now be aligned with +what browsers do), but web weirdness happens. Shout and we'll tweak. + +Also, + + * Fix double-navigation to pages (fix #1452) + * Upgrade cli+app dependencies + * Bump default Electron to 19.0.17 (from .14), with security fixes + - https://github.com/electron/electron/releases/tag/v19.0.15 + - https://github.com/electron/electron/releases/tag/v19.0.16 + - https://github.com/electron/electron/releases/tag/v19.0.17 + +49.0.1 / 2022-08-28 +=================== + +* Bump default Electron to 19.0.14 (from .10), with security fixes + - https://github.com/electron/electron/releases/tag/v19.0.11 + - https://github.com/electron/electron/releases/tag/v19.0.12 + - https://github.com/electron/electron/releases/tag/v19.0.13 + - https://github.com/electron/electron/releases/tag/v19.0.14 +* macOS: Move handling of "Universal" apps to electron-packager instead of our own thing (PR #1443) +* Upgrade cli+app dependencies + +49.0.0 / 2022-07-30 +=================== + +**[BREAKING]** 49.0.0 doesn't have more breaking changes than 48.0.0, but I'm +releasing a new major release anyway to signal one particularly noteworthy +breaking change in Electron 19 that I failed to pass along to you in 48.0.0: +**The `ia32` arch (a.k.a. `i386` or `x86/32bit`) is no longer supported.** + +People still running Nativefier apps on old ia32 machines, feel free to keep +passing a flag `--electron-version 18.x.y` *while it works*. Note however that +we won't be testing it, and future Nativefier versions may depend on upcoming +Electron APIs that will crash your electron18-app-packaged-by-future-Nativefier. +The deprecation is an upstream Electron decision, and there's nothing we will +do about it. Thx @TheCleric for the catch. + +Also, + +* macOS: Fix "main window cannot be activated" (fix #1415, PR #1417) +* Bump default Electron from 19.0.9 to [19.0.10](https://github.com/electron/electron/releases/tag/v19.0.10) +* Fix loud axios "fetch" warning (https://github.com/nativefier/gitcloud-client/pull/4) +* Fix playwright tests on Linux (#1440) +* Docker: upgraded base node-alpine image from 12 to LTS (currently 16) + +48.0.0 / 2022-07-24 +=================== + + * **[BREAKING]** Bump default Electron to 19.0.9 (from 18.3.5) + + As usual, we did our best to adapt to Electron breaking changes, but + patches welcome to fix regressions. If unable to submit a patch, + feel free to revert to Nativefier 47.2.1, or pass `-e 18.3.5` for a + _temporary_ downgrade (it will work for a while, but not forever). + Official release notes: https://www.electronjs.org/blog/electron-19-0 + + Detailed release notes: + + - https://github.com/electron/electron/releases/tag/v19.0.0 + - https://github.com/electron/electron/releases/tag/v19.0.1 + - https://github.com/electron/electron/releases/tag/v19.0.2 + - https://github.com/electron/electron/releases/tag/v19.0.3 + - https://github.com/electron/electron/releases/tag/v19.0.4 + - https://github.com/electron/electron/releases/tag/v19.0.5 + - https://github.com/electron/electron/releases/tag/v19.0.6 + - https://github.com/electron/electron/releases/tag/v19.0.7 + - https://github.com/electron/electron/releases/tag/v19.0.8 + - https://github.com/electron/electron/releases/tag/v19.0.9 + + * CATALOG.md: add a new recipe for using interactive buttons on Notion (PR #1430) + * GitHub Issues: switch from "Issue templates" to new & better "Issue forms" (fix #1258) (PR #1425) + * Maintenance: upgrade Jest, fix PlayWright tests + +47.2.1 / 2022-06-27 +=================== + + * macOS: fix incorrect "Back" keyboard shortcut (fix #1426) + * Bump default Electron to 18.3.5 (from 18.3.1), with security fixes: + https://github.com/electron/electron/releases/tag/v18.3.2 + https://github.com/electron/electron/releases/tag/v18.3.3 + https://github.com/electron/electron/releases/tag/v18.3.4 + https://github.com/electron/electron/releases/tag/v18.3.5 + * Update dependencies + +47.2.0 / 2022-05-30 +=================== + + * Handle `open-url` event: support "deep-linking" e.g. for mailto links (PR #1418, fix #1412) + * Bump default Electron to 18.3.1 (from 18.2.0), with security fixes: + https://github.com/electron/electron/releases/tag/v18.2.1 + https://github.com/electron/electron/releases/tag/v18.2.2 + https://github.com/electron/electron/releases/tag/v18.2.3 + https://github.com/electron/electron/releases/tag/v18.2.4 + https://github.com/electron/electron/releases/tag/v18.3.0 + https://github.com/electron/electron/releases/tag/v18.3.1 + * Update dependencies + * Docs: {API, README, CATALOG}.md cleanups + +47.1.3 / 2022-05-02 +=================== + + * Auto-internal URLs: add VMWare Workspace ONE + SecurID (PR #1391, fix #1390) + * `--counter`: accept colon character; useful for time-tracking apps with hour:min in title (PR #1378) + * Windows: correctly set notifications name - not electron.app.YOURAPPNAME (PR #1394) + * macOS: support "universal" architecture (fix #1384 #1398, PR #1386) + * macOS: fix "Open In New Tab" (fix #1260, PR #1385) + * macOS: Change "Paste and Match Style" shortcut to match Apple's HIG guidelines (PR #1387, fix #404) + * macOS: Bump minimum macOS version from 10.9 to 10.10 (see #1404) + This has been effectively been the case since a long time, it was just misdocumented. + Thus, not really a breaking change, and not major-bumping. + * CATALOG.md: add a new "General recipes" section, with one to restore app position/size (PR #1349) + * CI: Add integration testing to the app, using Playwright (PR #1397) + * CI: Speed it up by parallelize tasks + * CI: Bump max tested version of Node for CI/Publish from 17 to 18 + * Update dependencies + * Bump default Electron to 18.2.0 (from 18.0.3), with security fixes: + https://github.com/electron/electron/releases/tag/v18.0.4 + https://github.com/electron/electron/releases/tag/v18.1.0 + https://github.com/electron/electron/releases/tag/v18.2.0 + +47.0.0 / 2022-04-10 +=================== + + * **[BREAKING]** Bump default Electron to 18.0.3 (from 16.2.2) + + As usual, we did our best to adapt to Electron breaking changes in 17/18, + but patches welcome to fix regressions. If unable to submit a patch, then + feel free to revert to Nativefier 46.2.1 or simply pass `-e 16.2.2` . + Release notes with breaking changes: + + - https://www.electronjs.org/blog/electron-17-0 + - https://www.electronjs.org/blog/electron-18-0 + + Detailed release notes: + + - https://github.com/electron/electron/releases/tag/v17.0.0 + - https://github.com/electron/electron/releases/tag/v18.0.0 + - https://github.com/electron/electron/releases/tag/v18.0.1 + - https://github.com/electron/electron/releases/tag/v18.0.2 + - https://github.com/electron/electron/releases/tag/v18.0.3 + +46.2.1 / 2022-04-10 +=================== + + * Bump default Electron to 16.2.1 (from 16.1.0), with security fixes: + - https://github.com/electron/electron/releases/tag/v16.1.1 + - https://github.com/electron/electron/releases/tag/v16.2.0 + - https://github.com/electron/electron/releases/tag/v16.2.1 + - https://github.com/electron/electron/releases/tag/v16.2.2 + * Upgrade dependencies lockfiles + +46.2.0 / 2022-03-20 +=================== + + * Bugfix: Strip LRM and RLM in Linux names (fix #1361, PR #1365) + * Bugfix: Remove extra whitespace in UserAgent (fix #1357, PR #1367) + * Docs: Fix broken link in `API.md` for `conceal` flag (PR #1364) + * Bump default Electron to 16.1.0 (from 16.0.9), with security fixes: + - https://github.com/electron/electron/releases/tag/v16.1.0 + - https://github.com/electron/electron/releases/tag/v16.0.10 + * Upgrade dependencies lockfiles + +46.1.1 / 2022-02-14 +=================== + + * Feature: Add "copy as plain text" in edit menu (PR #1351 @abhi12299, fix #1144) + * Bump default Electron to 16.0.9 (from 16.0.8), with security fixes + - https://github.com/electron/electron/releases/tag/v16.0.9 + * Upgrade dependencies + +46.1.0 / 2022-02-06 +=================== + + * Add flag `--strict-internal-urls` to disable domain and subpath matching (PR #1340 @hbridge) + * Add flag `--quiet` flag to suppress all log output (PR #1342 @Nickersoft) + * Fix flag `--file-download-options` (PR #1350 @abhi12299, #1275) + * Allow setting default app destination with env. var. `NATIVEFIER_APPS_DIR` (PR #1339 @mattruzzi, #1336) + * Bump default Electron to 16.0.8, from 16.0.6 + - https://github.com/electron/electron/releases/tag/v16.0.7 + - https://github.com/electron/electron/releases/tag/v16.0.8 + * Upgrade dependencies + * Docs: + - CATALOG.md: Document GCal needs lying about useragent for working notifications (fix #1292) + - API.md: Fix broken "insecurity options" link (PR #1345 @ZacharyTalis) + - README.md: mention Snap & AUR repos + - HACKING.md: add triage guidelines + +46.0.4 / 2022-01-06 +=================== + + * CI: (Attempt to) push tag, not unreadable SHA + +46.0.3 / 2022-01-06 +=================== + + * CI: Push Docker image to our org, not my personal account + +46.0.2 / 2022-01-06 +=================== + + * CI: Fix Docker Hub image build & push (PR #1100, thx @snpranav) + +46.0.1 / 2022-01-06 +=================== + + * Fix `--widevine` broken since 46.0.0 (thx @loxK) + * Bump default Electron from 16.0.5 to 16.0.6 + - https://github.com/electron/electron/releases/tag/v16.0.6 + +46.0.0 / 2022-01-02 +=================== + + * **[BREAKING]** Upgrade Electron from 13.6.3 & Chrome 91 to 16.0.5 & Chrome 96 (PR #1288) + We did our best to adapt to [Electron breaking changes](https://www.electronjs.org/docs/latest/breaking-changes) in 14/15/16, but as usual, + patches welcome to address regressions. For detailed release notes, see + - https://github.com/electron/electron/releases/tag/v14.0.0 + - https://github.com/electron/electron/releases/tag/v15.0.0 + - https://github.com/electron/electron/releases/tag/v16.0.0 + - https://github.com/electron/electron/releases/tag/v16.0.1 + - https://github.com/electron/electron/releases/tag/v16.0.2 + - https://github.com/electron/electron/releases/tag/v16.0.3 + - https://github.com/electron/electron/releases/tag/v16.0.4 + - https://github.com/electron/electron/releases/tag/v16.0.5 + * Build/CI: use setup-node-v2 cache to speed up build + +45.0.8 / 2021-12-06 +=================== + + * Fix 45.0.7 broken because of missing "chalk" dep (fix #1324) + +45.0.7 / 2021-12-06 +=================== + + * Use userAgentFallback for user-agent injection (PR #1316) + * Fix `--upgrade` (PR #1286) + * Bump default Electron to 13.6.3 with fixes & security fixes + - 13.6.3: https://github.com/electron/electron/releases/tag/v13.6.3 + * Maintenance: documentation, scripts, dependencies bumps + * Display "we need your help" message when running CLI: + +``` +Hi! Nativefier is minimally maintained these days, and needs more hands. +If you have the time & motivation, help with bugfixes and maintenance is VERY welcome. +Please go to https://github.com/nativefier/nativefier and help how you can. Thanks. +``` + +45.0.6 / 2021-11-22 +=================== + + * Fix notifications (PR #1308) + * Fix icon conversion scripts broken on recent macOS (fix #1277) + * Bump default Electron to 13.6.2, with bug fixes & security fixes + - 13.6.2: https://github.com/electron/electron/releases/tag/v13.6.2 + * Maintenance: bump CI Nodejs to 17, Relock dependencies + +45.0.5 / 2021-11-01 +=================== + + * Bump default Electron to 13.6.1, with bug fixes & security fixes + - 13.5.2: https://github.com/electron/electron/releases/tag/v13.5.2 + - 13.6.0: https://github.com/electron/electron/releases/tag/v13.6.0 + - 13.6.1: https://github.com/electron/electron/releases/tag/v13.6.1 + * Maintenance: Fix auth manual tests (#1287), Bumps (axios, eslint), Doc & script nits + +45.0.4 / 2021-09-24 +=================== + + * Actually actually (TM) include lockfile in npm artifacts, duuuuuh + +45.0.3 / 2021-09-24 +=================== + + * Actually include package-lock.json in npm artifacts, duh + +45.0.2 / 2021-09-24 +=================== + + * Fix regressions in opening windows/tabs, update browser versions (PR #1284) + * Make macOS "bundle identifier" mention Nativefier (fix #866) (PR #1259) + * Maintenance: Work around yargs coerce issue (PR #1283) + +45.0.1 / 2021-09-20 +=================== + + * Auto-internal login pages: add `(id|auth).atlassian.com` (fix #1265) + * API.md: document need to use CSS `!important` keyword (fix #1264) + * Bump default Electron to 13.4.0 with security fixes + * Maintenance: deps bumps, fix build:watch script, re-introduce a lockfile + +45.0.0 / 2021-07-19 +=================== + + * **[BREAKING]** Bump default Electron to 13.1.7 with Chrome 91 (PR #1230) + See https://www.electronjs.org/blog/electron-13-0 + and https://www.electronjs.org/blog/electron-13-0#breaking-changes + +44.0.7 / 2021-07-10 +=================== + + * Fix badge/counter icon not being removed correctly (#1251, PR #1252) + * App context menu: add "Save Image", "Copy Image", "Copy Image Address" (PR #1256) + * Bump default Electron from 12.0.12 to 12.0.14. Changelogs: + [Electron 12.0.13](https://github.com/electron/electron/releases/tag/v12.0.13) + [Electron 12.0.14](https://github.com/electron/electron/releases/tag/v12.0.14) + * Maintenance: {API.md, HACKING.md} documentation, improve `generate-changelog` + +44.0.6 / 2021-06-26 +=================== + +Like 44.0.4, this release only contains one behind-the-scenes TS change with no user-visible changes. +It is here to let us narrow down on potential regressions that may have crept in. +It isn't especially scary, though. Do test it, use it, and report regressions! + + * App: Enable TypeScript `strict:true`, more typescript-eslint rules, shared TS project (#1231) + +44.0.5 / 2021-06-25 +=================== + + * Fix "Reset Zoom" menu item (#1241, PR #1243) + * Fix `--tray start-in-tray` (#1225, PR #1235) + * Fix external URLs opening in a new Nativefier tab (#1228, PR #1229) + * Bump default Electron from 12.0.11 to 12.0.12. See changelog: + [Electron 12.0.12](https://github.com/electron/electron/releases/tag/v12.0.12) + * Maintenance: documentation + +44.0.4 / 2021-06-15 +=================== + +This release only contains one behind-the-scenes TS change with no user-visible changes. +It is here to let us narrow down on potential regressions that may have crept in. +It isn't especially scary, though. Do test it, use it, and report regressions! + + * Enable TypeScript `strict:true`, and more typescript-eslint rules (#1223) + +44.0.3 / 2021-06-15 +=================== + + * Make CSS injection less brutal (#1222, #1227) + * Maintenance: README nits, Fix gitcloud 0.2.3 import + +44.0.2 / 2021-06-07 +=================== + + * Fix HTTP basic auth broken since 44.0.1 (fix #1219) (#1220) + * Fix tabs opening twice since 44.0.0 (fix #1209) (#1221) + +44.0.1 / 2021-06-07 +=================== + + * macOS: fix crash on activating main window (fix #1212) (PR #1213) + * macOS: fix fullscreen not working + menu refactor (fix #1206) (PR #1210) + +44.0.0 / 2021-06-04 +=================== + + * **[BREAKING]** Nativefier now requires node>=12.9 and npm>=6.9 (#1192) + We do our best to support the oldest Node we can (what Debian stable ships), + but we are also constrained by what our _own_ Node dependencies require. + Now is the time for a bump. + + * Feature: Provide easy-to-use user-agent shortcodes (e.g. `firefox`) (#1198) + * Feature: Organize CLI flags into groups (for better `--help` usability) (#1191) + + * Fix broken window popups (fix #1197, PR #1203) + * Fix allowing non-ascii app names like 微信读书 (fix #1056, PR #1207) + * Fix considering `login.microsoftonline.com` as internal login page (#1205) + * Bump default Electron from 12.0.7 to 12.0.10 with a couple of fixes. See changelogs for: + [12.0.8](https://github.com/electron/electron/releases/tag/v12.0.8), + [12.0.9](https://github.com/electron/electron/releases/tag/v12.0.9), + [12.0.10](https://github.com/electron/electron/releases/tag/v12.0.10). + + * Maintenance: docs, tests tooling, deps bumps + +43.1.3 / 2021-05-15 +=================== + +👋 dear users. Two announcements in this release: + +**This release (43.1.3) is the last release supporting Node 10.x** +We do our best to support the oldest Node we can (what Debian stable ships), +but we are also constrained by what our _own_ Node dependencies require. +It's time for a bump; the next release will be 44.0.0 and will require Node 12. + +Also, introducing **[CATALOG.md](https://github.com/nativefier/nativefier/blob/master/CATALOG.md),** +**a list of build commands contributed by the Nativefier community**, to help you +nativefy "complicated" apps that need a bit of elbow grease to work. +When stuck nativefying a specific site, go take a look, it might give you ideas :) . + + * Fix crash on tab close (only try to inject CSS for valid web requests) (#939, PR #1181) + * Fix considering `shop.foo.com` and `blog.foo.com` as internal (PR #1171) + * CATALOG.md: build command library (fix #1166) (PR #1178) + * Bump to Electron 12.0.7 + +43.1.2 / 2021-05-03 +=================== + + * Fix logging out users on upgrade / app recreate with same URL (fix #1176) (PR #1179) + +43.1.1 / 2021-05-02 +=================== + + * Fix crash in `preload.js` due to 3rd-party 'loglevel' (fix #1175, fix #1176) (PR #1177) + +43.1.0 / 2021-05-01 +=================== + +This is a chunky release! Warm thanks to all the contributors that helped shape it, +with a special shoutout to @TheCleric for a mountain of awesome work. + +Features! (nothing breaking) + + * Add a [`--upgrade`](https://github.com/nativefier/nativefier/blob/master/API.md#upgrade) option to easily upgrade an existing app (fix #1131) (PR #1138) + * Support defining a custom [`--bookmarks-menu`](https://github.com/nativefier/nativefier/blob/master/API.md#bookmarks-menu) (fix #1065) (PR #1155) + * Support setting apps [`--lang`](https://github.com/nativefier/nativefier/blob/master/API.md#lang)uage (fix #175) (PR #1173) + * Support creating self-contained "[`--portable`](https://github.com/nativefier/nativefier/blob/master/API.md#portable)" apps writing their app data to the app folder (fix #376) (PR #1168) + * Support opening URLs passed as arg to Nativefied apps (fix #405) (PR #1154) + +Bugfixes! + + * App: fix child windows not inheriting mainWindow properties (including userAgent), breaking some Google login pages (#1174) + * Fix `--inject`ing multiple css/js files (fix #458) (#1162) + * Fix `--widevine` by properly listening to `widevine-...` events (fix #1153) (PR #1164) + * Prompt to confirm when page is attempting to prevent unload (#1163) + * macOS: Fix crash when using `--tray` (fix #527) (PR #1156) + * macOS: Fix invisible icon (fix #942, fix #668) (PR #1156) + * Auto-internal login pages: add a missing Google login page (#1167) + +Maintenance! + + * Bump to [Electron 12.0.6](https://github.com/electron/electron/releases/tag/v12.0.6) with Chrome 89.0.4389.128 and security fixes + * Docs: add troubleshooting section for common issues (#1169), document signing `--widevine` apps like HBO Max & Udemy (#1147), misc fixes + * App: replace console.xyz calls with loglevel.xyz, with a level controlled by app argv `--verbose` (#1172) + * Auto-internal login pages: add test to ensure we don't regress on cases of SLDs + * CI: run in node 16, drop node 15. Run less node versions for faster CI; oldest supported / latest is enough + +43.0.2 / 2021-04-13 +=================== + + * Bump default Electron to 12.0.4 with Chrome 89.0.4389.114 + This includes the recent security fixes. + See https://github.com/electron/electron/releases/tag/v12.0.3 + and https://github.com/electron/electron/releases/tag/v12.0.4 + +43.0.1 / 2021-04-11 +=================== + + * Add a `session-interaction` event to allow injected js to interact + with apps Electron `session` object (PR #1132) + * Automatically-internal login pages: add Apple ID (PR #1146), GitHub 2FA (PR #1140) + * Bump default Electron from 12.0.1 to 12.0.2 (with Chrome 89.0.4389.90) + * Old build detection: bump old build threshold from 60 to 90 days + +43.0.0 / 2021-03-10 +=================== + + * **[BREAKING]** Bump to Electron 12.0.1 with Chrome 89.0.4389.82 + See https://www.electronjs.org/blog/electron-12-0 + and https://www.electronjs.org/docs/breaking-changes#planned-breaking-api-changes-120 + + Noteworthy to Nativefier users: + * As usual, new Chrome, with potential improvements/regressions to websites you use + * Removed Flash support. If you still need flash, pass a <12 version to the `-e` flag + * Removed support for older x86 CPUs that do not have SSE3 + + * **[BREAKING]** Automatically consider known login pages as internal (fix #706) (PR #1124) + URLs for known login pages (e.g. `accounts.google.com` or `login.live.com`) + are now automatically considered internal, to let you login in your + Nativefier app without having to fiddle with `--internal-urls`. + + This does not replace `internal-urls`, it complements it, and happens + _before_ your `internal-urls` rule is applied. So, if you already set + the flag to let such auth pages open internally, you can change it if + you want to clean it up, but it's not unnecessary. + + We think this is desirable behavior and are so far unaware of cases + where users might not want this. If you disagree, please chime in at + [PR #1124: App: Automatically consider known login pages as internal](https://github.com/nativefier/nativefier/pull/1124) + + * Various maintenance fixes: deps, scripts, slim down Docker size + +42.4.0 / 2021-03-04 +=================== + + * macOS: Prompt for accessibility permissions if needed by Global Shortcuts using Media Keys (fix #1120, PR #1121) + * Icon conversion: support GraphicsMagick in addition to ImageMagick (PR #1002) + * Docker: fix Windows builds, line endings, switch to Alpine (fix #997, PR #1122) + * Fix considering "same domain-ish" URLs as internal (PR #1126) + This was a regression introduced in 42.3.0 by dropping `wurl` in 6b266b781. + The new behavior is super close to 42.2.1. So, not considering it breaking. + * Various maintenance fixes: tooling, deps, CI + +42.3.0 / 2021-02-25 +=================== + + * Bump default Electron to 11.3.0 (with Chromium 87.0.4280.141). + macOS-segfault-causing icon bug #1101 should remain fixed. + * API docs: fix typo in option "-v" (PR #1114) + * Get rid of dep `shelljs` and abandoned app dep `wurl` + * Bump commander from 4 to 7 and eslint-config-prettier from 7 to 8 + +42.2.1 / 2021-01-30 +=================== + + * Move GitHub repository to [`nativefier/nativefier`](https://github.com/nativefier/nativefier) + * Temporarily increase timeout for network call in test + * Move TS @types from dependencies to devDependencies (PR #1102) + +42.2.0 / 2021-01-18 +=================== + + * Revert default Electron back to 11.1.1 (Chrome 87.0.4280.88) (fix #1101) + 11.2.0 was causing segfaults in macOS. + +42.1.0 / 2021-01-16 +=================== + + * Bump default Electron to 11.2.0 (with Chromium 87.0.4280.141) + * Maintenance: + * A bit more filename & appname sanitization + * Get rid of two direct deps: cheerio, lodash + * Fix error surfacing in full since recent changes in `page-icon` + * Publish TS types, for them to show up in npm + +42.0.2 / 2020-12-07 +=================== + + * Fix arg validation regression in #1080 with `--{x,y}` (fix #1084) + +42.0.1 / 2020-12-06 +=================== + + * Fix arg validation regression in #1080 (fix #1083) + +42.0.0 / 2020-12-06 +=================== + +This release includes several contributor patches. Thanks @sorhtyre @mattruzzi ! + + * **[BREAKING CHANGE] Warn on old Electron/Chrome (fix #556) (PR #1076)** + ⚠️ Users packaging kiosk apps running for a long time on internal websites, + see https://github.com/nativefier/nativefier/blob/master/API.md#disable-old-build-warning-yesiknowitisinsecure + * Check for improperly-formatted arguments (fix #885) (PR #1080) + * Correctly start in tray when both `--maximize` and `--tray start-in-tray` are passed (fix #1015) (PR #1079) + * Fix icon path error when passing asar `--conceal` flag (fix #975) (PR #1074) + * Migrate from Travis CI to GitHub Actions + * Bump default Electron to 11.0.3, bump dep eslint-config-prettier to 7.x + +Also, bumping npm version to something far away from current Electron version. + +Rationale for the nonsensical major version bump: around Nativefier 8.x, +versions of Nativefier and Electron aligned, by release schedule coincidence. +Since Nativefier has little breaking changes, it was great: as Electron +releases are breaking, Nativefier had no breaking changes, I bumped our +major version on new major Electron, and everything was good. + +Except *now*, as I have a breaking change, which would bump Nativefier to +12.x, being annoyingly confusing since we'd still default to Electron 11 :-/ . + +-> To keep respecting semver and reduce confusion, bumping Nativefier + version to something far ahead of Electron versions. No it doesn't + matter, version number are meaningless anyway (well, outside of + semver, whose respect is precisely the point here). + +11.0.2 / 2020-11-21 +=================== + + * **[BREAKING CHANGE] Bump default Electron to 11.0.2 / Chromium 87** + * Support using a Widevine-enabled Electron for DRM playback, see flag `--widevine` (fix #435) (PR #1073) + +10.1.5 / 2020-11-08 +=================== + + * Bump default Electron to 10.1.5 (with Chromium 85.0.4183.121) (#1066) + * Readme: suggest docker "-rm" flag to clean up containers after build (#1064) + * Deps bumps: webpack, ts-eslint + * CI: Add a node.js 15 build + +10.1.0 / 2020-08-29 +=================== + + * **[BREAKING CHANGE] Bump default Electron to 10.1.0 / Chromium 85** + * Support `arm64` architecture (PR #1037, fix #804) + * On successful build, better explain how to run the app and what to do with it (fix #1029) + * Bump to TypeScript 4.x + +9.2.0 / 2020-08-10 +================== + + * Add `--block-external-urls` flag to forbid external navigation attempts (fix #978, PR#1012) + * Restore Docker docs in README, now that Docker build-on-release has been fixed (fix #848) + * Emit TS type declarations, and type NativefierOptions (PR #1016) + * Emit a warning about incorrectly-named "Electron" process when building windows apps under non-Windows and without Wine (fix #1022) + * Add unified {build,test} watch mode, using `concurrently` (PR #1011) + * Bump default Electron to 9.2.0 + * Bump eslint to 7.x + +9.1.0 / 2020-07-18 +================== + + * Fix 'Image could not be created' app error on run (fix #992) + * Bump docker Node image version from 8 to 12 (#996) + * Bump default Electron to 9.1.0 and deps (electron-packager, ts-loader) + +9.0.0 / 2020-06-13 +================== + + * **[BREAKING CHANGE] Require Node.js >= 10 and npm >= 6** + * **[BREAKING CHANGE] Bump default Electron to 9.0.4** + * Bump deps (ts-loader, jest, electron-context-menu) + * --help: fix typo, clarify `--icon` helptext (PR #976) + * Fix notifications (#88, #956), processEnvs, using as git dep (PR #955) + +8.0.7 / 2020-04-22 +================== + + * Fix `Unable to load preload script` error at runtime (fix #934) + * Bump default Electron to 8.2.3, and bump app/electron-context-menu to 1.x + +8.0.6 / 2020-03-27 +================== + + * App: Fix unintentionally *global*/os-wide keyboard shortcuts (fix #930) + * App: Back & forward: expose standard shortcuts first & handle mac, + keep old weird (Ctrl+[ and Ctrl+]) shortcuts for backward compat + * App: Properly hide Developer Tools when asked by flag (fix #842) + * Log a helpful error when failing to parse JSON arg (fix #928) + * Bump default Electron to 8.2.0 + * Misc GitHub & Travis nits. + +8.0.4 / 2020-03-16 +================== + + * Fix failing to global-sudo-install due to postinstall script + (#923, maybe #924) + +8.0.3 / 2020-03-15 +================== + + * Attempt to fix failing to install due to app yarn install (#923) + See https://github.com/nativefier/nativefier/pull/898#issuecomment-583865045 . + +8.0.2 / 2020-03-15 +================== + + * CI nitty-gritty, nothing to see here. See 8.0.0 news below. + +8.0.1 / 2020-03-15 +================== + + * CI nitty-gritty, nothing to see here. See 8.0.0 news below. + +8.0.0 / 2020-03-15 +================== + +Revamp and move to TypeScript (#898) + +## Breaking changes + +- Require **Node >= 8.10.0 and npm 5.6.0** +- Move to **Electron 8.1.1**. +- That's it. Lots of care went into breaking CLI & programmatic behavior + as little as possible. **Please report regressions**. +- Known issue: build may fail behind a proxy. Get in touch if you use one: + https://github.com/nativefier/nativefier/issues/907#issuecomment-596144768 + +## Changes summary + +Nativefier didn't get much love recently, to the point that it's +becoming hard to run on recent Node, due to old dependencies. +Also, some past practices now seem weird, as better expressible +by modern JS/TS, discouraging contributions including mine. + +Addressing this, and one thing leading to another, came a +bigger-than-expected revamp, aiming at making Nativefier more +**lean, stable, future-proof, user-friendly and dev-friendly**, +while **not changing the CLI/programmatic interfaces**. Highlights: + +- **Require Node>=8**, as imposed by many of our dependencies. Node 8 + is twice LTS, and easily available even in conservative Linux distros. + No reason not to demand it. +- **Default to Electron 8**. +- **Bump** all dependencies to latest version, including electron-packager. +- **Move to TS**. TS is great. As of today, I see no reason not to use it, + and fight interface bugs at runtime rather than at compile time. + With that, get rid of everything Babel/Webpack. +- **Move away from Gulp**. Gulp's selling point is perf via streaming, + but for small builds like Nativefier, npm tasks are plenty good + and less dependency bloat. Gulp was the driver for this PR: broken + on Node 12, and I didn't feel like just upgrading and keeping it. +- Add tons of **verbose logs** everywhere it makes sense, to have a + fine & clear trace of the program flow. This will be helpful to + debug user-reported issues, and already helped me fix a few bugs. + - With better simple logging, get rid of the quirky and buggy + progress bar based on package `progress`. Nice logging (minimal + by default, the verbose logging mentioned above is only used + when passing `--verbose`) is better and one less dependency. +- **Dump `async` package**, a relic from old callback-hell early Node. + Also dump a few other micro-packages unnecessary now. +- A first pass of code **cleanup** thanks to modern JS/TS features: + fixes, simplifications, jsdoc type annotations to types, etc. +- **Remove GitHub integrations Hound & CodeClimate**, which are more + exotic than good'ol'linters, and whose signal-to-noise ratio is too low. +- Quality: **Add tests** and add **Windows + macOS CI builds**. + Also, add a **manual test script**, helping to quickly verify the + hard-to-programatically-test stuff before releases, and limit regressions. +- **Fix a very small number of existing bugs**. The goal of this PR was + *not* to fix bugs, but to get Nativefier in better shape to do so. + Bugfixes will come later. Still, these got addressed: + - Add common `Alt`+`Left`/`Right` for previous/next navigation. + - Improve #379: fix zoom with `Ctrl` + numpad `+`/`-` + - Fix pinch-to-zoom (see https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 ) + + +7.7.1 / 2020-01-23 +================== + + * Feature: proxy rules with `--proxy-rules` flag (PR #854) + * Fix weirdly platform-dependent folder naming logic (PR #850, issue #708) + * Fix filter exception when injecting CSS (PR #837) + * Fix Handle nativefier.json readonly access with options.maximize (PR #856) + * Fix/app: Application menu support on Electron 5.x (PR #876) + * Doc: Clarify `--background-color` arguments (PR #891) + * Doc: Fix duplicate word for `--bounce` doc (PR #883) + * Bump default Electron to 5.0.13 + +7.7.0 / 2019-08-22 +================== + + * Default to Electron 5.x (#796) + * Add `--darwin-dark-mode-support` to support macOS 10.4+ Dark Mode (PR #796) + * Add `--browserwindow-options` to completely expose Electron options (PR #835) + * Add `--background-color` to set background color (fixes #795) (PR #819) + * Restore login functionality broken since Electron 5.x (PR #826) + * Squirrel: resolve .quit() issue with missing ../screen (PR #784) + * Doc: improve doc for `--internal-urls` (PR #833) + +7.6.12 / 2019-03-25 +=================== + + * Fix crash when launching a second instance using option --single-instance (Fixes #664, PR #772) + * Prevent menu from opening on Alt+Shift, by defining Alt+... menu shortcuts (PR #768) + * Bump default Electron to 3.1.7 + +7.6.11 / 2019-02-10 +=================== + + * Add `--clear-cache` flag to cleanup session on start/exit ("incognito" mode) (Fixes #316, PR #747) + * Support packaging Nativefier applications as [Squirrel](https://github.com/Squirrel/)-based installers (PR #744) + * Icon conversion: don't crash if source/destination paths have spaces (PR #736) + * Bump default Electron to 3.1.3 + +7.6.10 / 2019-01-01 +=================== + + * Fix CSS & JavaScript injection (Fixes #703, Fixes #731, PR #732) + * Bump default Electron to 3.0.13 + +7.6.9 / 2018-12-01 +================== + + * Add `start-in-tray` CLI option to `--tray` flag to let app start in background (PR #564, Fixes #522) + * Add `--global-shortcuts` flag to trigger in-app input events (PR #698, Fixes #15) + * Fix CSS injection broken with Electron 3 (PR #709, Fixes #703) + * Bump default Electron to 3.0.10 + +7.6.8 / 2018-10-06 +================== + + * Bump default Electron to 3.0.3, based on Chrome 66 / Node 10 / V8 6.6 + * Show application window on notification click (#640) + * Update docs: Windows icon requirements (#663), badge option (#693) + * Bump deps (eslint-config-prettier, eslint-plugin-prettier) + +7.6.7 / 2018-07-31 +================== + + * Fix broken `--version` (issue #660) + * Bump default Electron to 2.0.6 + * **Upcoming compatibility break notice:** + **Nativefier may still work with node 4.x & 5.x, but <6.x is no longer tested.** + **Minimum node version will be enforced to >=6.x at some point soon.** + +7.6.6 / 2018-07-22 +================== + + * Nothing new, just a re-push to npm. + See changelog for 7.6.5. + +7.6.5 / 2018-07-21 +================== + + * Fix "Copy Current URL" causing `TypeError` (#633, PR #634) + * Add Unix/Mac-conventional `-v` version flag (PR #628) + * Bump default Electron to 2.0.5 + * Dev: Add `jest --watch` helper for npm scripts, Code cleanups, Add tests + +7.6.4 / 2018-05-31 +================== + + * Add `--title-bar-style` flag (macOS only) (PR #627) + * Make the `--counter` regexp allow punctuation (e.g. "1,234") (fix #610, PR #626) + * Fix sites that use about:blank redirect technique (PR #623) + * Always open external links externally (fix #621 - PRs #622 #624) + * Only override the default window opening behavior when necessary (fix #616 - PR #620) + * Don't run tests on node 4 and 5, due to Jest not supporting those. + In time, Nativefier will stop supporting them too and will enforce node>=6. + * Tests cleanups: use async/await, separate e2e tests, mocha -> jest. + * Use [prettier](https://github.com/prettier/prettier) for code formatting. + +7.6.3 / 2018-05-23 +================== + + * macOS: Add tabs, used automatically instead of windows (PR #579). + Provided by Electron and (so far) no available for Windows/Linux, contributions welcome. + * Fix #547 - Default to Electron 2.0.2 (including Chrome 61, Node 8.9.3, V8 6.1.534.41, GTK3 on Linux) (PR #587) + * Fix Gmail complaining window creation was prevented by a popup blocker (PR #603) + * Fix two build-time deprecation warnings + +7.6.2 / 2018-05-01 +================== + + * Fix #94, Fix #575 - Fix run-time crash due to insufficient permissions (PR #581) + * Fix #574 - Allow build to continue if icon conversion fails (PR#585) + * Fix #199 - On macOS, perform image conversion tasks using `sips` when available (PR #583). + * Fix #95, Fix #384 - Add cut/copy/paste context menu entries, using `electron-context-menu` (PR #588) + * Fix #364 - Add `--disable-gpu` flag to disable hardware acceleration (PR #584) + * Fix #474 - Remember custom zoom level (PR #582) + * Fix #590, Fix #439 - Ensure children windows have the same behavior as the mainWindow (PR #591) + * macOS: Add `--bounce` option for dock counter (PR #570) + * Default to latest stable electron 1.8.6 and update dependencies (electron-packager) + * Enforce staying on `npm@5.8.x` for a little while, as npm@6 breaks under Node 4, which we still support. + +7.6.1 / 2018-03-29 +================== + + * Fix CD with Travis #482 + +7.6.0 / 2018-03-29 +================== + + * Fix infer icon url #529 + * Fix #549: Add --always-on-top build flag (PR #551) + * Update deps, default to Electron 1.8.4 stable + * Document internal-urls option (PR #465) + * Support Mac App Store (--mas) builds (PR #532) + * Fix #499: Add options to control file download behavior (PR #526) + * Fix #325 - Add --x and --y window position flags (PR #515) + * Fix #480 - Move all console.* to loglevel.* calls, eslint-fail on console.* (PR #507) + * Fix #496 - Recommend .png for --icon on all platforms, even macOS (PR #502) + * Fix #486 : --tray flag crashes nativefied app under Windows (PR #495) + * Fix #462 - When minimized to tray and single-instance, re-running the app should activate and focus it (#490) + * Fix #461 & address #375 in Docker: move Dockerfile to Debian and use wine32 (#488) + +7.5.4 / 2017-11-24 +================== + + * Update Dockerfile to node8-alpine, fix typos + * Upgrade dependencies and default to latest Electron 1.7.9 (PR #483) + +7.5.0 / 2017-11-12 +================== + +* Add `--tray` flag to let app running in background on window close. Supports in-title counter. (Issue #304, PR #457) +* Add HTTP `--basic-auth-{username,password}` flags (Issue #275, PR #444) +* Add offline build detection and advice (Issue #448, PR #452) +* Add 'Paste and Match Style' to Edit menu (Issue #404, PR #447) +* Add setting environment variables (PR #419) +* Add `app-copyright`, `app-version`, `build-version`, `version-string` and `win32metadata` flags (Issue #226, PR #244) +* Fix: Make title counter regex match '+' after number, used by certain sites (PR #424) + +7.4.1 / 2017-08-06 +================== + + * Add support for `--disk-cache-size` Electron flag (PR #400) + * Add `--ignore-gpu-blacklist` and `--enable-es3-apis` flags to allow WebGL + apps to work on graphics cards unsupported/blacklisted by Chrome (PR #410) + * Fix #28 - Executable name being always `Electron` under Windows (PR #389) + * Fix #353 - `--crash-reporter` option crashing packaged app at startup + * Fix #402 - Force fullscreen even after first startup, as `electron-window-state` + does not appear to remember fullscreen in all cases (PR #403). + +7.4.0 / 2017-05-21 +================== + + * Add jq to docs as release dependency + * Run Nativefier with Docker (#311) + * Add hound config (#369) + * Add codeclimate config + * Promisfy and parallelise config generation, add unit tests + * Add ARM build support (#360) + +7.3.1 / 2017-04-30 +==================== + + * Add script to update version and changelog + * Update changelog for 7.3.0 + * Remove Windows tests + * Cleanup travis config + * Update eslint and use Airbnb style + * Change Mocha to not need a babel build to run (#349) + * Promisify inferTitle module + * Add autodeploy to NPM on tag + +7.2.0 / 2017-04-20 +================== + * Update dependencies, default to latest Electron 1.6.6 (#327, PR #341). **Feedback welcome in case of issues/regressions!** + * [Feature] Add `--single-instance` switch (PR #323) + * [Bug] Better honor `--zoom` option (#253, PR #347) + * [Bug] Allow mDNS addresses (ending with `local.`) during URL validation (#308, PR #346) + * [Docs] Readme and CLI cleanup + * [Misc] Remove duplicate dependencies (#337) + * [Misc] Rename 'Open in default browser' contextMenu to 'Open with default browser' (#338) + +7.1.0 / 2017-04-07 +================== + + * Feature: Add "Copy link location" context menu (#230) + * Feature: Add `--internal-urls ` option to customize what should open in external browser (#230) + * Feature: Add `--zoom` option for setting default zoom (#218) + * Bug: Fix context menu actions broken on elements containing nested markup (#263) + * Bug: Fix counter notifications (#256) + * Bug: Remove non-ascii characters or use default for app name (#217) + * Doc: various fixes, including clarifying optional OSX dependencies for generating icons + * CI: Fix Travis tests which require wine + * Dev: Add editorconfig to trim trailing whitespace + +7.0.1 / 2016-06-16 +================== + + * Fix/performance issues with FOUC (#214) + * Fix bug where convert icons script fails silently if dependency is not found + * Use original eslint module for linting instead of gulp + +7.0.0 / 2016-05-27 +================== + + * Only support node.js >=4 + * Implement downloading of files #185 + * Implement min/max window width and height #82 + * Implement disabling of developer tools #194 + * Update default electron version to stable v1.1.3 #206 + * Update electron-packager to v7.0.1 #193 + * Update validator to v5.2.0 + * Update shelljs to v0.7.0 + * Update cheerio to v0.20.0 + * Update axios to v0.11.1 + * Update eslint to v2.0.0 + * Increase timeout for test + * Fix bug where gitcloud matching of icons with multiple words is not supported + * Fix bug where inferred title is too long #195 + * Fix flash of unstyled content #159 + +6.14.0 / 2016-05-08 +=================== + + * Properly log errors with injected files + * Fix slowdown bug #191 + * Revert fix for FOUC with injected CSS files #202 + * Allow fast quit of app after window close on OSX #178 + * Allow hiding of window frame #188 + * Allow disabling the context menu #187 + * Rebind 'Copy Current URL' to 'CmdOrCtrl+L' to mimic 'Open Location' in browsers #181 + * Add walkthrough gif in readme + * No longer enable flash by default + * Bump default electron version to 0.37.2 + +6.13.0 / 2016-03-25 +=================== + + * Source files will not be included in the packaged app + * Fix bug where state of mainWindow is not managed properly + * Implement setting of verbose log level + * Implement infer of user agent from electron version + * Implement initial maximization of main window from cli + * Fix FOUC with inject CSS files + * No need to run CI test for gulp release + +6.12.1 / 2016-03-14 +=================== + + * Fix bugs retrieving icons from nativefier-icons + * Add resize flag to convertToIco convert so that large `.png` will not throw errors when converting to `.ico` + +6.12.0 / 2016-03-14 +=================== + + * Try to retrieve icons from `nativefier-icons` first before inferring + * Add progress bar + * Use `windows` and `osx` to specify platform + * Override output directory by default + * Add checks for icon format + * Implement conversion to `.ico` for windows target + * Support only node `0.12` onwards with `babel-polyfill` + * Organise documentation + +6.11.0 / 2016-03-11 +=================== + + * Use local page-icon dependency instead of bestIcon server to infer icons for a target url + * Add conversion of images from `.ico` to `.png` + * Implement conversion of images on Linux in addition to OSX + * Fix bug in setting icon on for a Windows app while on Windows OS + * Trim whitespace from inferred page title + * Remove non-ascii characters from app name to prevent weird Wine error + * Remove dependency on `sips` + * Fix bug where shell scripts fail silently + * Modularize `gulpfile` + +6.10.1 / 2016-02-26 +=================== + + * Fix #117 ENOENT when infering flash + +6.10.0 / 2016-02-25 +=================== + + * Fix bug in mocha where next task is executed before mocha callback + * Implement command line flag to start app in full screen, resolves #109 + * Implement injection of css and js + +6.9.1 / 2016-02-25 +================== + + * Do not npm ignore binaries + +6.9.0 / 2016-02-25 +================== + + * Preserve app data upon regeneration of app + * Add menu option to clear the app data + * Change flag usage + * `--ignore-certificate` to ignore invalid certificate errors, + * `--insecure` to disable web security to allow mixed content + * Add flag to allow mixed content over https + * Add preliminary flash support + * NPM ignore everything except compiled files + * Fix #146 Specifying `--electron-version` does not work + * Update example api usage for `require('nativefier').default` + * Add issue template + * #114 Allow [x] and {x} forms of notification count + * #112 Counter: Allow for [x] and {x} forms of notification count + * #90 Add keyboard shortcuts for back, forward + * Add note about not putting spaces in user defined app name + * Merge pull request #107 from zweicoder/fix/respect-user-choice + * Do not print done statement if app already exists and `--overwrite` is not passed + * Respect user choice for naming + * Allow npm publish to log to stdout + +6.8.0 / 2016-01-30 +================== + + * Use ES6 for placeholder app + * Massive refactor of cli code + * Rename `--app-name` to `--name` + * Fix #103 App name should not be capitalized + * Remove electron prebuilt as a dev dependency to speed up ci builds + * Fix test for non darwin platforms + * Implement check for wine before attempting to pass icon to electron packager + * Update gulpfile - Build tests in `gulp build` - Watch test files - Clean test files as well + * Implement automatic retrieval of png which resolves #16 + +6.7.0 / 2016-01-28 +================== + + * Allow using png to for icon on OSX + * Use manual compiling of mocha so that sourcemaps can be used + * Convert app name to capitalized camel case if building for linux to prevent dock problems + * Fix the icon parameter bug for linux and windows, fix #92, fix #53 + * Make Browserwindow always reference `app/icon.png` for the icon + +6.6.2 / 2016-01-26 +================== + + * Fix #87, Fix #89 - Sanitize app name before packaging + * Add command line flag to make the packaged app ignore certificate errors, fixes #69 + * Fix #32 Ability to copy and paste a URL + * Implement right click context menu for regular href links + * Allow es6 for app static files + +6.6.1 / 2016-01-25 +================== + + * Remove unused files + * Fix #76 where all placeholder app modules are treated as externals + * Add contributing + * Update gulp release to also run lints + +6.6.0 / 2016-01-25 +================== + * Add CI Integration with travis + * Add tests and lints + * Fixes bug where electron packager returns appPath as an array instead of a string + * Add sourcemaps support + * Exposes buildApp as a programmatic api for npm + * Remove shorthand command for height and width to fix conflicts with `-h`. Closes #30, closes #64 and closes #67 + * Automatically hide the menu bar by default on Windows. Users can press `alt` to show it + * Implement proper build system with ES6 support to facilitate development + * App window now remembers its previous position + * Fix #59 Fullscreen goes to a black screen when clicking close + * Set window title immediately when the window is created, fixes #54 + * Implement navigating backward and forward from the application menu + * Implement proper notification listeners to change the badge + * Refactor main.js into separate files, and put static files such as preload and login.html into `app/src/static` + * Implement changing of zoom which fixes #17 + +6.5.6 / 2016-01-22 +================== + + * Workaround for windows `mkdir -p`, fixes #57 + +6.5.5 / 2016-01-22 +================== + + * Implement script to set up dev environment + * Fix bug in invalid parameter for link in default browser + * App is now precompiled with browserify as a workaround for an extremely annoying npm issue + * Reorganised folder of app + +6.5.4 / 2016-01-22 +================== + + * Fix #46 Url is not defined + * Override user agent by default, disable with `--honest` flag + * Implement counter which closes #33, thanks to @jfouchard + * Improve automatic retrieval of app name by faking a user agent to make the request + +6.5.0 / 2016-01-22 +================== + + * Implement support for http authentication, fixes #19 + * Implement authentication that requires a new window to be opened (e.g. OAuth) + * Under the hood changes: + * Target web page no longer loads in a ``, the `BrowserWindow` loads the target url directly + +6.4.0 / 2016-01-21 +================== + + * Make debug script automatically open the packaged app on OSX + * Remove "About Electron" from app menu, add nativefier version to help, which fixes #18 + * Implement `--pretend` flag to easily simulate user agent strings, fixes #11 + * Merge branch 'master' of github.com:nativefier/nativefier + * Fix bug in error when response is undefined + * Add helper scripts to debug easily + * Hide app instead of exiting on OSX to fix #14 + * Update deprecated electron loadUrl usage Remove crash reporter Remove commented code + * Merge pull request #20 from mattchue/master + * Merge pull request #25 from PoziWorld/patch-1 + * Merge pull request #24 from himynameisdave/master + * Make app resource folder contain a short id string, fix #21 + * Minor copy fixes + * Fixes the issue with "/"'s in the page title + * Update documentation, no longer need to add the full url with the protocol + * Fix wrong bool + * Allow intranet URLs + * Update readme + * Hide the webview until it finishes loading diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4968720 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +FROM --platform=linux/amd64 node:lts-alpine +LABEL description="Alpine image to build Nativefier apps" + + +# Install dependencies and cleanup extraneous files +RUN apk update \ + && apk add bash wine imagemagick dos2unix \ + && rm -rf /var/cache/apk/* \ + && mkdir /nativefier && chown node:node /nativefier + +# Use node (1000) as default user not root +USER node + +ENV NPM_PACKAGES="/home/node/npm-packages" +ENV PATH="$PATH:$NPM_PACKAGES/bin" +ENV MANPATH="$MANPATH:$NPM_PACKAGES/share/man" + +# Setup a global packages location for "node" user so we can npm link +RUN mkdir $NPM_PACKAGES \ + && npm config set prefix $NPM_PACKAGES + +WORKDIR /nativefier + +# Add sources with node as the owner so that it has the power it needs to build in /nativefier +COPY --chown=node:node . . + +# Fix line endings that may have gotten mangled in Windows +RUN find ./icon-scripts ./src ./app -type f -print0 | xargs -0 dos2unix + +# Link (which will install and build) +# Run tests (to ensure we don't Docker build & publish broken stuff) +# Cleanup leftover files in this step to not waste Docker layer space +# Make sure nativefier is executable +RUN npm i \ + && npm link \ + && npm run test:noplaywright \ + && rm -rf /tmp/nativefier* ~/.npm/_cacache ~/.cache/electron \ + && chmod +x $NPM_PACKAGES/bin/nativefier + +# Run a {lin,mac,win} build +# 1. to check installation was sucessful +# 2. to cache electron distributables and avoid downloads at runtime +# Also delete generated apps so they don't get added to the Docker layer +# !Important! The `rm -rf` command must be in the same `RUN` command (using an `&&`), to not waste Docker layer space +RUN nativefier https://github.com/nativefier/nativefier /tmp/nativefier \ + && nativefier -p osx https://github.com/nativefier/nativefier /tmp/nativefier \ + && nativefier -p windows https://github.com/nativefier/nativefier /tmp/nativefier \ + && rm -rf /tmp/nativefier + + +RUN echo Generated Electron cache size: $(du -sh ~/.cache/electron) \ + && echo Final image size: $(du -sh / 2>/dev/null) + +ENTRYPOINT ["nativefier"] +CMD ["--help"] diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..ffdcae3 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,255 @@ +# Development Guide + +Welcome, soon-to-be contributor 🙂! This document sums up +what you need to know to get started hacking on Nativefier. + +## Guidelines + +1. **Before starting work on a huge change, gauge the interest** + of community & maintainers through a GitHub issue. For big changes, + create a **[RFC](https://en.wikipedia.org/wiki/Request_for_Comments)** + issue to enable a good peer review. + +2. Do your best to **avoid adding new Nativefier command-line options**. + If a new option is inevitable for what you want to do, sure, + but as much as possible try to see if you change works without. + Nativefier already has a ton of them, making it hard to use. + +3. Do your best to **limit breaking changes**. + Only introduce breaking changes when necessary, when required by deps, or when + not breaking would be unreasonable. When you can, support the old thing forever. + For example, keep maintaining old flags; to "replace" an flag you want to replace + with a better version, you should keep honoring the old flag, and massage it + to pass parameters to the new flag, maybe using a wrapper/adapter. + Yes, our code will get a tiny bit uglier than it could have been with a hard + breaking change, but that would be to ignore our users. + Introducing breaking changes willy nilly is a comfort to us developers, but is + disrespectful to end users who must constantly bend to the flow of breaking changes + pushed by _all their software_ who think it's "just one breaking change". + See [Rich Hickey - Spec-ulation](https://www.youtube.com/watch?v=oyLBGkS5ICk). + +4. **Avoid adding npm dependencies**. Each new dep is a complexity & security liability. + You might be thinking your extra dep is _"just a little extra dep"_, and maybe + you found one that is high-quality & dependency-less. Still, it's an extra dep, + and over the life of Nativefier we requested changes to _dozens_ of PRs to avoid + "just a little extra dep". Without this constant attention, Nativefier would be + more bloated, less stable for users, more annoying to maintainers. Now, don't go + rewriting zlib if you need a zlib dep, for sure use a dep. But if you can write a + little helper function saving us a dep for a mundane task, go for the helper :) . + Also, an in-tree helper will always be less complex than a dep, as inherently + more tailored to our use case, and less complexity is good. + +5. Use **types**, avoid `any`, write **tests**. + +6. **Document for users** in `API.md` + +7. **Document for other devs** in comments, jsdoc, commits, PRs. + Say _why_ more than _what_, the _what_ is your code! + +## Setup + +First, clone the project: + +```bash +git clone https://github.com/nativefier/nativefier.git +cd nativefier +``` + +Install dependencies (for both the CLI and the Electron app): + +```bash +npm ci +``` + +The above `npm ci` will build automatically (through the `prepare` hook). +When you need to re-build Nativefier, + +```bash +npm run build +``` + +Set up a symbolic link so that running `nativefier` calls your dev version with your changes: + +```bash +npm link +which nativefier +# -> Should return a path, e.g. /home/youruser/.node_modules/lib/node_modules/nativefier +# If not, be sure your `npm_config_prefix` env var is set and in your `PATH` +``` + +After doing so, you can run Nativefier with your test parameters: + +```bash +nativefier --your-awesome-new-flag 'https://your-test-site.com' +``` + +Then run your nativefier app _through the command line too_ (to see logs & errors): + +```bash +# Under Linux +./your-test-site-linux-x64/your-test-site + +# Under Windows +your-test-site-win32-x64/your-test-site.exe + +# Under macOS +./YourTestSite-darwin-x64/YourTestSite.app/Contents/MacOS/YourTestSite --verbose +``` + +## Linting & formatting + +Nativefier uses [Prettier](https://prettier.io/), which will shout at you for +not formatting code exactly like it expects. This guarantees a homogenous style, +but is painful to do manually. Do yourself a favor and install a +[Prettier plugin for your editor](https://prettier.io/docs/en/editors.html). + +## Tests + +- To run all tests, `npm t` +- To run only unit tests, `npm run test:unit` +- To run only integration tests, `npm run test:integration` +- Logging is suppressed by default in tests, to avoid polluting Jest output. + To get debug logs, `npm run test:withlog` or set the `LOGLEVEL` env. var. +- For a good live experience, open two terminal panes/tabs running code/tests watchers: + 1. Run a TSC watcher: `npm run build:watch` + 2. Run a Jest unit tests watcher: `npm run test:watch` + 3. Here is [a screencast of how the live-reload experience should look like](https://user-images.githubusercontent.com/522085/120407694-abdf3f00-c31b-11eb-9ab5-a531a929adb9.mp4) +- Alternatively, you can run both test processes in the same terminal by running: `npm run watch` + +## Maintainers corner + +### Deps: major-upgrading Electron + +When a new major [Electron release](https://github.com/electron/electron/releases) occurs, + +1. Wait a few weeks to let it stabilize. Never upgrade Nativefier to a `.0.0`. +2. Thoroughly digest the new version's [breaking changes](https://www.electronjs.org/docs/breaking-changes) + (also via the [Releases page](https://github.com/electron/electron/releases) and [the blog](https://www.electronjs.org/blog/), the content is different), + grepping our codebase for every changed API. + - If called for by the breaking changes, perform the necessary API changes +3. Bump + - `src/constants.ts` / `DEFAULT_ELECTRON_VERSION` & `DEFAULT_CHROME_VERSION` + - `package.json / devDeps / electron` + - `app / package.json / devDeps / electron` +4. On Windows, macOS, Linux, test for regression and crashes: + 1. With `npm test` and `npm run test:manual` + 2. With extra manual testing +5. When confident enough, release it in a regression-spelunking-friendly way: + 1. If `master` has unreleased commits, make a patch/minor release with them, but without the major Electron bump. + 2. Commit your Electron major bump and release it as a major new Nativefier version. Help users identify the breaking change by using a bold **[BREAKING]** marker in `CHANGELOG.md` and in the GitHub release. + +### Deps updates + +It is important to stay afloat of dependencies upgrades. +In packages ecosystems like npm, there's only one way: forward. +The best time to do package upgrades is now / progressively, because: + +1. Slacking on doing these upgrades means you stay behind, and it becomes + risky to do them. Upgrading a woefully out-of-date dep from 3.x to 9.x is + scarier than 3.x to 4.x, release, then 4.x to 5.x, release, etc... to 9.x. + +2. Also, dependencies applying security patches to old major versions are rare + in npm. So, by slacking on upgrades, it becomes more and more probable that + we get impacted by a vulnerability. And when this happens, it then becomes + urgent & stressful to A. fix the vulnerability, B. do the required major upgrades. + +So: do upgrade CLI & App deps regularly! Our release script will remind you about it. + +### Deps lockfile / shrinkwrap + +We do use lockfiles (`npm-shrinkwrap.json` & `app/npm-shrinkwrap.json`), for: + +1. Security (avoiding supply chain attacks) +2. Reproducibility +3. Performance + +It means you might have to update these lockfiles when adding a dependency. +`npm run relock` will help you with that. + +Note: we do use `npm-shrinkwrap.json` rather than `package-lock.json` because +the latter is tailored to libraries, and is not publishable. +As [documented](https://docs.npmjs.com/cli/v6/configuring-npm/shrinkwrap-json), +CLI tools like Nativefier should use shrinkwrap. + +### Release + +While on `master`, with no uncommitted changes, run: + +```bash +npm run changelog -- $VERSION +# With no 'v'. For example: npm run changelog -- '42.5.0' +``` + +Do follow semantic versioning, and give visibility to breaking changes +in release notes by prefixing their line with **[BREAKING]**. + +### Triage + +These are the guidelines we (try to) follow when triaging [issues](https://github.com/nativefier/nativefier/issues): + +1. Do your best to conciliate **empathy & efficiency, and keep your cool**. + It’s not always easy 😄😬😭🤬. Get away from triaging if you feel grouchy. + +2. **Rename** issues. Most issues are badly named, with titles ranging from + unclear to flat out wrong. A good backlog is a backlog of issues with clear + concise titles, understandable with only the title after you read them once. + Rename and clarify. + +3. **Ask for clarification & details** when needed, and add a `need-info` label. + + 1. In particular, if the issue isn’t reproducible (e.g. a non-trivial bug + happening on an internal site), express that we can’t work without a + repro scenario, and flag as `need-info`. + +4. **Label** issues with _category/sorting_ labels (e.g. `mac` / `linux` / `windows`, + `bug` / `feature-request` ...) and _status_ labels (e.g. `upstream`, `wontfix`, + `need-info`, `cannot-reproduce`). + +5. **Close if needed, but not too much**. We _do_ want to close what deserves it, + but closing _too_ ruthlessly frustrates and disappoints users, and does us a + disservice of not having a clear honest backlog available to us & users. So, + + 1. When in doubt, leave issues open and triaged as `bug` / `feature-request`. + It’s okay, reaching 0 open issues is _not_ an objective. Or if it is, + it deserves to be a development objective, not a triage one. + 2. That being said, do close what’s `upstream`, with a kind message. + 3. Also do close bugs that have been `need-info` or `cannot-reproduce` for + too long (weeks / months), with a kind message explaining we’re okay to + re-open if the requested info / scenario is provided. + 4. Finally, carefully close issues we do not want to address, e.g. requests + going against project goals, or bugs & feature requests that are so niche + or far-fetched that there’s zero chance of ever seeing them addressed. + But if in doubt, remain at point 1. above: leave open, renamed, labelled. + +6. **Close duplicates issues** and link to the original issue. + + 1. To be able to notice dups implies you must know the backlog (one more + reason to keep it tidy and palatable). Once in a blue moon, do a + "full pass" of the whole backlog from beginning to end, you’ll often + find lots of now-irrelevant bugs, and duplicates. + +7. **Use [GitHub saved replies](https://github.com/settings/replies)** to + automate asking for info and being nice on closing as noanswer / stale-needinfo. + +8. **Transform findings stemming from issues discussion** into documentation + (chiefly, [CATALOG.md](CATALOG.md) & [API.md](API.md)), or into code comments. + +9. **Don’t scold authors of lame "+1" comments**, this only adds to the noise + you’re trying to avoid. Instead, hide useless comments as `Off-topic`. + From personal experience, users do understand this signal, and such hidden + comments do avoid an avalanche of extra "+1" comments. + + 1. There are shades of lame. A literal `"+1"` comment is frankly useless and + is worth hiding. But a comment like `"same for me on Windows"` at least + brings an extra bit of information, so can remain visible. + + 2. In a perfect world, GitHub would let us add a note when hiding comments to + express _"Please use a 👍 reaction on the issue to vote for it instead of_ + _posting a +1 comment"_. In a perfecter world, GitHub would use their AI + skillz to automatically detect such comments, discourage them and nudge + towards a 👍 reaction. We’re not there yet, so “hidden as off-topic” will do. + +10. **Don’t let yourself be abused** by abrasive / entitled users. There are + plenty of articles documenting open-source burnout and trolls-induced misery. + Find an article that speaks to you, and point problematic users to it. + I like [Brett Cannon - The social contract of open source](https://snarky.ca/the-social-contract-of-open-source/). diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..795da4c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +The MIT License (MIT) +===================== + +Copyright © `2016` `Goh Jia Hao` + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..545c745 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +Note: Nativefier is unmaintained, please see https://github.com/nativefier/nativefier/issues/1577. + +# Nativefier + +![Example of Nativefier app in the macOS dock](.github/dock-screenshot.png) + +You want to make a native-looking wrapper for WhatsApp Web (or any web page). + +```bash +nativefier 'web.whatsapp.com' +``` + +![Walkthrough animation](.github/nativefier-walkthrough.gif) + +You're done. + +## Introduction + +Nativefier is a command-line tool to easily create a “desktop app” for any web site +with minimal fuss. Apps are wrapped by [Electron](https://www.electronjs.org/) +(which uses Chromium under the hood) in an OS executable (`.app`, `.exe`, etc) +usable on Windows, macOS and Linux. + +I built this because I grew tired of having to Alt-Tab to my browser and then search +through numerous open tabs when using Messenger or +Whatsapp Web ([HN thread](https://news.ycombinator.com/item?id=10930718)). Nativefier features: + +- Automatically retrieval of app icon / name +- Injection of custom JS & CSS +- Many more, see the [API docs](API.md) or `nativefier --help` + +## Installation + +Install Nativefier globally with `npm install -g nativefier` . Requirements: + +- macOS 10.13+ / Windows / Linux +- [Node.js](https://nodejs.org/) ≥ 16.9 and npm ≥ 7.10 + +Optional dependencies: + +- [ImageMagick](http://www.imagemagick.org/) or [GraphicsMagick](http://www.graphicsmagick.org/) to convert icons. + Be sure `convert` + `identify` or `gm` are in your `$PATH`. +- [Wine](https://www.winehq.org/) to build Windows apps from non-Windows platforms. + Be sure `wine` is in your `$PATH`. + +
+ Or install with Docker (click to expand) + + - Pull the image from [Docker Hub](https://hub.docker.com/r/nativefier/nativefier): `docker pull nativefier/nativefier` + - ... or build it yourself: `docker build -t local/nativefier .` + (in this case, replace `nativefier/` in the below examples with `local/`) + + By default, `nativefier --help` will be executed. + To build e.g. a Gmail app into `~/nativefier-apps`, + + ```bash + docker run --rm -v ~/nativefier-apps:/target/ nativefier/nativefier https://mail.google.com/ /target/ + ``` + + You can pass Nativefier flags, and mount volumes to pass local files. E.g. to use an icon, + + ```bash + docker run --rm -v ~/my-icons-folder/:/src -v $TARGET-PATH:/target nativefier/nativefier --icon /src/icon.png --name whatsApp -p linux -a x64 https://web.whatsapp.com/ /target/ + ``` +
+ +
+ Or install with Snap & AUR (click to expand) + + These repos are *not* managed by Nativefier maintainers; use at your own risk. + If using them, for your security, please inspect the build script. + + - [Snap](https://snapcraft.io/nativefier) + - [AUR](https://aur.archlinux.org/packages/nodejs-nativefier) +
+ +## Usage + +To create an app for medium.com, simply `nativefier 'medium.com'` + +Nativefier will try to determine the app name, and well as other options that you +can override. For example, to override the name, `nativefier --name 'My Medium App' 'medium.com'` + +**Read the [API docs](API.md) or run `nativefier --help`** +to learn about command-line flags and configure your app. + +## Troubleshooting + +**See [CATALOG.md](CATALOG.md) for site-specific ideas & workarounds contributed by the community**. + +If this doesn’t help, go look at our [issue tracker](https://github.com/nativefier/nativefier/issues). + +## Development + +Help welcome on [bugs](https://github.com/nativefier/nativefier/issues?q=is%3Aopen+is%3Aissue+label%3Abug) and +[feature requests](https://github.com/nativefier/nativefier/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request)! + +Docs: [Developer / build / hacking](HACKING.md), [API / flags](API.md), +[Changelog](CHANGELOG.md). + +License: [MIT](LICENSE.md). diff --git a/app/.eslintrc.js b/app/.eslintrc.js new file mode 100644 index 0000000..6474e8e --- /dev/null +++ b/app/.eslintrc.js @@ -0,0 +1,21 @@ +const base = require('../base-eslintrc'); + +// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md +module.exports = { + parser: base.parser, + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + plugins: base.plugins, + extends: base.extends, + rules: base.rules, + // https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files + ignorePatterns: [ + 'node_modules/**', + 'lib/**', + 'dist/**', + 'built-tests/**', + 'coverage/**', + ], +}; diff --git a/app/inject/_placeholder b/app/inject/_placeholder new file mode 100644 index 0000000..e69de29 diff --git a/app/nativefier.json b/app/nativefier.json new file mode 100644 index 0000000..2ef7879 --- /dev/null +++ b/app/nativefier.json @@ -0,0 +1,8 @@ +{ + "name": "Google", + "targetUrl": "http://google.com", + "badge": false, + "width": 1280, + "height": 800, + "showMenuBar": false +} diff --git a/app/npm-shrinkwrap.json b/app/npm-shrinkwrap.json new file mode 100644 index 0000000..07fd23c --- /dev/null +++ b/app/npm-shrinkwrap.json @@ -0,0 +1,1170 @@ +{ + "name": "nativefier-placeholder", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nativefier-placeholder", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "electron-context-menu": "^3.6.1", + "electron-dl": "^3.5.0", + "electron-squirrel-startup": "^1.0.0", + "electron-window-state": "^5.0.3", + "loglevel": "^1.8.1", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "electron": "^25.7.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", + "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.17.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", + "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==", + "dev": true + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true, + "optional": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "optional": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "optional": true + }, + "node_modules/electron": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.7.0.tgz", + "integrity": "sha512-P82EzYZ8k9J21x5syhXV7EkezDmEXwycReXnagfzS0kwepnrlWzq1aDIUWdNvzTdHobky4m/nYcL98qd73mEVA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-context-menu": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-3.6.1.tgz", + "integrity": "sha512-lcpO6tzzKUROeirhzBjdBWNqayEThmdW+2I2s6H6QMrwqTVyT3EK47jW3Nxm60KTxl5/bWfEoIruoUNn57/QkQ==", + "dependencies": { + "cli-truncate": "^2.1.0", + "electron-dl": "^3.2.1", + "electron-is-dev": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-dl": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.5.0.tgz", + "integrity": "sha512-Oj+VSuScVx8hEKM2HEvTQswTX6G3MLh7UoAz/oZuvKyNDfudNi1zY6PK/UnFoK1nCl9DF6k+3PFwElKbtZlDig==", + "dependencies": { + "ext-name": "^5.0.0", + "pupa": "^2.0.1", + "unused-filename": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-is-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz", + "integrity": "sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-squirrel-startup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.0.tgz", + "integrity": "sha512-Oce8mvgGdFmwr+DsAcXBmFK8jFfN6yaFAP9IvyhTfupM3nFkBku/7VS/mdtJteWumImkC6P+BKGsxScoDDkv9Q==", + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/electron-window-state": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/electron-window-state/-/electron-window-state-5.0.3.tgz", + "integrity": "sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg==", + "dependencies": { + "jsonfile": "^4.0.0", + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true, + "optional": true + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "optional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "optional": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "optional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "optional": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "optional": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/modify-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", + "integrity": "sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true, + "optional": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unused-filename": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-2.1.0.tgz", + "integrity": "sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==", + "dependencies": { + "modify-filename": "^1.1.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..19ec1a2 --- /dev/null +++ b/app/package.json @@ -0,0 +1,25 @@ +{ + "name": "nativefier-placeholder", + "version": "1.0.0", + "description": "Placeholder for the nativefier cli to override with a target url", + "main": "lib/main.js", + "author": "Jia Hao", + "license": "MIT", + "keywords": [ + "desktop", + "electron", + "placeholder" + ], + "scripts": {}, + "dependencies": { + "electron-context-menu": "^3.6.1", + "electron-dl": "^3.5.0", + "electron-squirrel-startup": "^1.0.0", + "electron-window-state": "^5.0.3", + "loglevel": "^1.8.1", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "electron": "^25.7.0" + } +} diff --git a/app/src/components/contextMenu.ts b/app/src/components/contextMenu.ts new file mode 100644 index 0000000..5966e53 --- /dev/null +++ b/app/src/components/contextMenu.ts @@ -0,0 +1,84 @@ +import { + BrowserWindow, + ContextMenuParams, + Event as ElectronEvent, +} from 'electron'; +import contextMenu from 'electron-context-menu'; + +import { nativeTabsSupported, openExternal } from '../helpers/helpers'; +import * as log from '../helpers/loggingHelper'; +import { setupNativefierWindow } from '../helpers/windowEvents'; +import { createNewWindow } from '../helpers/windowHelpers'; +import { + OutputOptions, + outputOptionsToWindowOptions, +} from '../../../shared/src/options/model'; + +export function initContextMenu( + options: OutputOptions, + window?: BrowserWindow, +): void { + log.debug('initContextMenu'); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + contextMenu({ + prepend: (actions: contextMenu.Actions, params: ContextMenuParams) => { + log.debug('contextMenu.prepend', { actions, params }); + const items = []; + if (params.linkURL && window) { + items.push({ + label: 'Open Link in Default Browser', + click: () => { + openExternal(params.linkURL).catch((err) => + log.error('contextMenu Open Link in Default Browser ERROR', err), + ); + }, + }); + items.push({ + label: 'Open Link in New Window', + click: () => + createNewWindow( + outputOptionsToWindowOptions(options, nativeTabsSupported()), + setupNativefierWindow, + params.linkURL, + // window, + ), + }); + if (nativeTabsSupported()) { + items.push({ + label: 'Open Link in New Tab', + click: () => + // // Fire a new window event for a foreground tab + // // Previously we called createNewTab directly, but it had incosistent and buggy behavior + // // as it was mostly designed for running off of events. So this will create a new event + // // for a foreground-tab for the event handler to grab and take care of instead. + // (window as BrowserWindow).webContents.emit( + // // event name + // 'new-window', + // // event object + // { + // // Leave to the default for a NewWindowWebContentsEvent + // newGuest: undefined, + // ...new Event('new-window'), + // }, // as NewWindowWebContentsEvent, + // // url + // params.linkURL, + // // frameName + // window?.webContents.mainFrame.name ?? '', + // // disposition + // 'foreground-tab', + // ), + window.emit('new-window-for-tab', { + ...new Event('new-window-for-tab'), + url: params.linkURL, + } as ElectronEvent<{ url: string }>), + }); + } + } + return items; + }, + showCopyImage: true, + showCopyImageAddress: true, + showSaveImage: true, + }); +} diff --git a/app/src/components/loginWindow.ts b/app/src/components/loginWindow.ts new file mode 100644 index 0000000..9a15775 --- /dev/null +++ b/app/src/components/loginWindow.ts @@ -0,0 +1,39 @@ +import * as path from 'path'; + +import { BrowserWindow, ipcMain } from 'electron'; + +import * as log from '../helpers/loggingHelper'; +import { nativeTabsSupported } from '../helpers/helpers'; + +export async function createLoginWindow( + loginCallback: (username?: string, password?: string) => void, + parent?: BrowserWindow, +): Promise { + log.debug('createLoginWindow', { + loginCallback, + parent, + }); + + const loginWindow = new BrowserWindow({ + parent: nativeTabsSupported() ? undefined : parent, + width: 300, + height: 400, + frame: false, + resizable: false, + webPreferences: { + nodeIntegration: true, // TODO work around this; insecure + contextIsolation: false, // https://github.com/electron/electron/issues/28017 + sandbox: false, // https://www.electronjs.org/blog/electron-20-0#default-changed-renderers-without-nodeintegration-true-are-sandboxed-by-default + }, + }); + await loginWindow.loadURL( + `file://${path.join(__dirname, 'static/login.html')}`, + ); + + ipcMain.once('login-message', (event, usernameAndPassword: string[]) => { + log.debug('login-message', { event, username: usernameAndPassword[0] }); + loginCallback(usernameAndPassword[0], usernameAndPassword[1]); + loginWindow.close(); + }); + return loginWindow; +} diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts new file mode 100644 index 0000000..708b60c --- /dev/null +++ b/app/src/components/mainWindow.ts @@ -0,0 +1,336 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { + desktopCapturer, + ipcMain, + BrowserWindow, + Event, + HandlerDetails, +} from 'electron'; +import windowStateKeeper from 'electron-window-state'; + +import { initContextMenu } from './contextMenu'; +import { createMenu } from './menu'; +import { + getAppIcon, + getCounterValue, + isOSX, + nativeTabsSupported, +} from '../helpers/helpers'; +import * as log from '../helpers/loggingHelper'; +import { IS_PLAYWRIGHT } from '../helpers/playwrightHelpers'; +import { onNewWindow, setupNativefierWindow } from '../helpers/windowEvents'; +import { + clearCache, + createNewTab, + getDefaultWindowOptions, + hideWindow, +} from '../helpers/windowHelpers'; +import { + OutputOptions, + outputOptionsToWindowOptions, +} from '../../../shared/src/options/model'; + +export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json'); + +type SessionInteractionRequest = { + id?: string; + func?: string; + funcArgs?: unknown[]; + property?: string; + propertyValue?: unknown; +}; + +type SessionInteractionResult = { + id?: string; + value?: T | Promise; + error?: Error; +}; + +/** + * @param {{}} nativefierOptions AppArgs from nativefier.json + * @param {function} setDockBadge + */ +export async function createMainWindow( + nativefierOptions: OutputOptions, + setDockBadge: (value: number | string, bounce?: boolean) => void, +): Promise { + const options = { ...nativefierOptions }; + + const mainWindowState = windowStateKeeper({ + defaultWidth: options.width || 1280, + defaultHeight: options.height || 800, + }); + + const mainWindow = new BrowserWindow({ + frame: !options.hideWindowFrame, + width: mainWindowState.width, + height: mainWindowState.height, + minWidth: options.minWidth, + minHeight: options.minHeight, + maxWidth: options.maxWidth, + maxHeight: options.maxHeight, + x: options.x, + y: options.y, + autoHideMenuBar: !options.showMenuBar, + icon: getAppIcon(), + fullscreen: options.fullScreen, + // Whether the window should always stay on top of other windows. Default is false. + alwaysOnTop: options.alwaysOnTop, + titleBarStyle: options.titleBarStyle ?? 'default', + // Maximize window visual glitch on Windows fix + // We want a consistent behavior on all OSes, but Windows needs help to not glitch. + // So, we manually mainWindow.show() later, see a few lines below + show: options.tray !== 'start-in-tray' && process.platform !== 'win32', + backgroundColor: options.backgroundColor, + ...getDefaultWindowOptions( + outputOptionsToWindowOptions(options, nativeTabsSupported()), + ), + }); + + // Just load about:blank to start, gives playwright something to latch onto initially for testing. + if (IS_PLAYWRIGHT) { + await mainWindow.loadURL('about:blank'); + } + + mainWindowState.manage(mainWindow); + + // after first run, no longer force maximize to be true + if (options.maximize) { + mainWindow.maximize(); + options.maximize = undefined; + saveAppArgs(options); + } + + if (options.tray === 'start-in-tray') { + mainWindow.hide(); + } else if (process.platform === 'win32') { + // See other "Maximize window visual glitch on Windows fix" comment above. + mainWindow.show(); + } + + const windowOptions = outputOptionsToWindowOptions( + options, + nativeTabsSupported(), + ); + createMenu(options, mainWindow); + createContextMenu(options, mainWindow); + setupNativefierWindow(windowOptions, mainWindow); + + // Note it is important to add these handlers only to the *main* window, + // else we run into weird behavior like opening tabs twice + mainWindow.webContents.setWindowOpenHandler((details: HandlerDetails) => { + return onNewWindow( + windowOptions, + setupNativefierWindow, + details, + mainWindow, + ); + }); + mainWindow.on('new-window-for-tab', (event?: Event<{ url?: string }>) => { + log.debug('mainWindow.new-window-for-tab', { event }); + createNewTab( + windowOptions, + setupNativefierWindow, + event?.url ?? options.targetUrl, + true, + // mainWindow, + ); + }); + + if (options.counter) { + setupCounter(options, mainWindow, setDockBadge); + } else { + setupNotificationBadge(options, mainWindow, setDockBadge); + } + + ipcMain.on('notification-click', () => { + log.debug('ipcMain.notification-click'); + mainWindow.show(); + }); + + setupSessionInteraction(mainWindow); + setupSessionPermissionHandler(mainWindow); + + if (options.clearCache) { + await clearCache(mainWindow); + } + + setupCloseEvent(options, mainWindow); + + return mainWindow; +} + +function createContextMenu( + options: OutputOptions, + window: BrowserWindow, +): void { + if (!options.disableContextMenu) { + initContextMenu(options, window); + } +} + +export function saveAppArgs(newAppArgs: OutputOptions): void { + try { + fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs, null, 2)); + } catch (err: unknown) { + log.warn( + `WARNING: Ignored nativefier.json rewrital (${(err as Error).message})`, + ); + } +} + +function setupCloseEvent(options: OutputOptions, window: BrowserWindow): void { + window.on('close', (event: Event) => { + log.debug('mainWindow.close', event); + if (window.isFullScreen()) { + if (nativeTabsSupported()) { + window.moveTabToNewWindow(); + } + window.setFullScreen(false); + window.once('leave-full-screen', (event: Event) => + hideWindow( + window, + event, + options.fastQuit ?? false, + options.tray ?? 'false', + ), + ); + } + hideWindow( + window, + event, + options.fastQuit ?? false, + options.tray ?? 'false', + ); + + if (options.clearCache) { + clearCache(window).catch((err) => log.error('clearCache ERROR', err)); + } + }); +} + +function setupCounter( + options: OutputOptions, + window: BrowserWindow, + setDockBadge: (value: number | string, bounce?: boolean) => void, +): void { + window.on('page-title-updated', (event, title) => { + log.debug('mainWindow.page-title-updated', { event, title }); + const counterValue = getCounterValue(title); + if (counterValue) { + setDockBadge(counterValue, options.bounce); + } else { + setDockBadge(''); + } + }); +} + +function setupSessionPermissionHandler(window: BrowserWindow): void { + window.webContents.session.setPermissionCheckHandler(() => { + return true; + }); + window.webContents.session.setPermissionRequestHandler( + (_webContents, _permission, callback) => { + callback(true); + }, + ); + ipcMain.handle('desktop-capturer-get-sources', () => { + return desktopCapturer.getSources({ + types: ['screen', 'window'], + }); + }); +} + +function setupNotificationBadge( + options: OutputOptions, + window: BrowserWindow, + setDockBadge: (value: number | string, bounce?: boolean) => void, +): void { + ipcMain.on('notification', () => { + log.debug('ipcMain.notification'); + if (!isOSX() || window.isFocused()) { + return; + } + setDockBadge('•', options.bounce); + }); + window.on('focus', () => { + log.debug('mainWindow.focus'); + setDockBadge(''); + }); +} + +function setupSessionInteraction(window: BrowserWindow): void { + // See API.md / "Accessing The Electron Session" + ipcMain.on( + 'session-interaction', + (event, request: SessionInteractionRequest) => { + log.debug('ipcMain.session-interaction', { event, request }); + + const result: SessionInteractionResult = { id: request.id }; + let awaitingPromise = false; + try { + if (request.func !== undefined) { + // If no funcArgs provided, we'll just use an empty array + if (request.funcArgs === undefined || request.funcArgs === null) { + request.funcArgs = []; + } + + // If funcArgs isn't an array, we'll be nice and make it a single item array + if (typeof request.funcArgs[Symbol.iterator] !== 'function') { + request.funcArgs = [request.funcArgs]; + } + + // Call func with funcArgs + // @ts-expect-error accessing a func by string name + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + result.value = window.webContents.session[request.func]( + ...request.funcArgs, + ); + + if (result.value !== undefined && result.value instanceof Promise) { + // This is a promise. We'll resolve it here otherwise it will blow up trying to serialize it in the reply + (result.value as Promise) + .then((trueResultValue) => { + result.value = trueResultValue; + log.debug('ipcMain.session-interaction:result', result); + event.reply('session-interaction-reply', result); + }) + .catch((err) => + log.error('session-interaction ERROR', request, err), + ); + awaitingPromise = true; + } + } else if (request.property !== undefined) { + if (request.propertyValue !== undefined) { + // Set the property + // @ts-expect-error setting a property by string name + window.webContents.session[request.property] = + request.propertyValue; + } + + // Get the property value + // @ts-expect-error accessing a property by string name + result.value = window.webContents.session[request.property]; + } else { + // Why even send the event if you're going to do this? You're just wasting time! ;) + throw new Error( + 'Received neither a func nor a property in the request. Unable to process.', + ); + } + + // If we are awaiting a promise, that will return the reply instead, else + if (!awaitingPromise) { + log.debug('session-interaction:result', result); + event.reply('session-interaction-reply', result); + } + } catch (err: unknown) { + log.error('session-interaction:error', err, event, request); + result.error = err as Error; + result.value = undefined; // Clear out the value in case serializing the value is what got us into this mess in the first place + event.reply('session-interaction-reply', result); + } + }, + ); +} diff --git a/app/src/components/menu.test.ts b/app/src/components/menu.test.ts new file mode 100644 index 0000000..9f8e654 --- /dev/null +++ b/app/src/components/menu.test.ts @@ -0,0 +1,167 @@ +import { BrowserWindow, MenuItemConstructorOptions } from 'electron'; + +jest.mock('../helpers/helpers'); +import { isOSX } from '../helpers/helpers'; +import { generateMenu } from './menu'; + +describe('generateMenu', () => { + let window: BrowserWindow; + const mockIsOSX: jest.SpyInstance = isOSX as jest.Mock; + let mockIsFullScreen: jest.SpyInstance; + let mockIsFullScreenable: jest.SpyInstance; + let mockIsSimpleFullScreen: jest.SpyInstance; + let mockSetFullScreen: jest.SpyInstance; + let mockSetSimpleFullScreen: jest.SpyInstance; + + beforeEach(() => { + window = new BrowserWindow(); + mockIsOSX.mockReset(); + mockIsFullScreen = jest + .spyOn(window, 'isFullScreen') + .mockReturnValue(false); + mockIsFullScreenable = jest + .spyOn(window, 'isFullScreenable') + .mockReturnValue(true); + mockIsSimpleFullScreen = jest + .spyOn(window, 'isSimpleFullScreen') + .mockReturnValue(false); + mockSetFullScreen = jest.spyOn(window, 'setFullScreen'); + mockSetSimpleFullScreen = jest.spyOn(window, 'setSimpleFullScreen'); + }); + + afterAll(() => { + mockIsFullScreen.mockRestore(); + mockIsFullScreenable.mockRestore(); + mockIsSimpleFullScreen.mockRestore(); + mockSetFullScreen.mockRestore(); + mockSetSimpleFullScreen.mockRestore(); + }); + + test('does not have fullscreen if not supported', () => { + mockIsOSX.mockReturnValue(false); + mockIsFullScreenable.mockReturnValue(false); + + const menu = generateMenu( + { + nativefierVersion: '1.0.0', + zoom: 1.0, + disableDevTools: false, + }, + window, + ); + + const editMenu = menu.filter((item) => item.label === '&View'); + + const fullscreen = ( + editMenu[0].submenu as MenuItemConstructorOptions[] + ).filter((item) => item.label === 'Toggle Full Screen'); + + expect(fullscreen).toHaveLength(1); + expect(fullscreen[0].enabled).toBe(false); + expect(fullscreen[0].visible).toBe(false); + + expect(mockIsOSX).toHaveBeenCalled(); + expect(mockIsFullScreenable).toHaveBeenCalled(); + }); + + test('has fullscreen no matter what on mac', () => { + mockIsOSX.mockReturnValue(true); + mockIsFullScreenable.mockReturnValue(false); + + const menu = generateMenu( + { + nativefierVersion: '1.0.0', + zoom: 1.0, + disableDevTools: false, + }, + window, + ); + + const editMenu = menu.filter((item) => item.label === '&View'); + + const fullscreen = ( + editMenu[0].submenu as MenuItemConstructorOptions[] + ).filter((item) => item.label === 'Toggle Full Screen'); + + expect(fullscreen).toHaveLength(1); + expect(fullscreen[0].enabled).toBe(true); + expect(fullscreen[0].visible).toBe(true); + + expect(mockIsOSX).toHaveBeenCalled(); + expect(mockIsFullScreenable).toHaveBeenCalled(); + }); + + test.each([true, false])( + 'has a fullscreen menu item that toggles fullscreen', + (isFullScreen) => { + mockIsOSX.mockReturnValue(false); + mockIsFullScreenable.mockReturnValue(true); + mockIsFullScreen.mockReturnValue(isFullScreen); + + const menu = generateMenu( + { + nativefierVersion: '1.0.0', + zoom: 1.0, + disableDevTools: false, + }, + window, + ); + + const editMenu = menu.filter((item) => item.label === '&View'); + + const fullscreen = ( + editMenu[0].submenu as MenuItemConstructorOptions[] + ).filter((item) => item.label === 'Toggle Full Screen'); + + expect(fullscreen).toHaveLength(1); + expect(fullscreen[0].enabled).toBe(true); + expect(fullscreen[0].visible).toBe(true); + + expect(mockIsOSX).toHaveBeenCalled(); + expect(mockIsFullScreenable).toHaveBeenCalled(); + + // @ts-expect-error click is here TypeScript... + fullscreen[0].click(null, window); + + expect(mockSetFullScreen).toHaveBeenCalledWith(!isFullScreen); + expect(mockSetSimpleFullScreen).not.toHaveBeenCalled(); + }, + ); + + test.each([true, false])( + 'has a fullscreen menu item that toggles simplefullscreen as a fallback on mac', + (isFullScreen) => { + mockIsOSX.mockReturnValue(true); + mockIsFullScreenable.mockReturnValue(false); + mockIsSimpleFullScreen.mockReturnValue(isFullScreen); + + const menu = generateMenu( + { + nativefierVersion: '1.0.0', + zoom: 1.0, + disableDevTools: false, + }, + window, + ); + + const editMenu = menu.filter((item) => item.label === '&View'); + + const fullscreen = ( + editMenu[0].submenu as MenuItemConstructorOptions[] + ).filter((item) => item.label === 'Toggle Full Screen'); + + expect(fullscreen).toHaveLength(1); + expect(fullscreen[0].enabled).toBe(true); + expect(fullscreen[0].visible).toBe(true); + + expect(mockIsOSX).toHaveBeenCalled(); + expect(mockIsFullScreenable).toHaveBeenCalled(); + + // @ts-expect-error click is here TypeScript... + fullscreen[0].click(null, window); + + expect(mockSetSimpleFullScreen).toHaveBeenCalledWith(!isFullScreen); + expect(mockSetFullScreen).not.toHaveBeenCalled(); + }, + ); +}); diff --git a/app/src/components/menu.ts b/app/src/components/menu.ts new file mode 100644 index 0000000..cd935b7 --- /dev/null +++ b/app/src/components/menu.ts @@ -0,0 +1,424 @@ +import * as fs from 'fs'; +import path from 'path'; + +import { + BrowserWindow, + clipboard, + Menu, + MenuItem, + MenuItemConstructorOptions, +} from 'electron'; + +import { cleanupPlainText, isOSX, openExternal } from '../helpers/helpers'; +import * as log from '../helpers/loggingHelper'; +import { + clearAppData, + getCurrentURL, + goBack, + goForward, + goToURL, + zoomIn, + zoomOut, + zoomReset, +} from '../helpers/windowHelpers'; +import { OutputOptions } from '../../../shared/src/options/model'; + +type BookmarksLink = { + type: 'link'; + title: string; + url: string; + shortcut?: string; +}; + +type BookmarksSeparator = { + type: 'separator'; +}; + +type BookmarkConfig = BookmarksLink | BookmarksSeparator; + +type BookmarksMenuConfig = { + menuLabel: string; + bookmarks: BookmarkConfig[]; +}; + +export function createMenu( + options: OutputOptions, + mainWindow: BrowserWindow, +): void { + log.debug('createMenu', { options }); + const menuTemplate = generateMenu(options, mainWindow); + + injectBookmarks(menuTemplate); + + const menu = Menu.buildFromTemplate(menuTemplate); + Menu.setApplicationMenu(menu); +} + +export function generateMenu( + options: { + disableDevTools: boolean; + nativefierVersion: string; + zoom?: number; + }, + mainWindow: BrowserWindow, +): MenuItemConstructorOptions[] { + const { nativefierVersion, zoom, disableDevTools } = options; + const zoomResetLabel = + !zoom || zoom === 1.0 + ? 'Reset Zoom' + : `Reset Zoom (to ${(zoom * 100).toFixed(1)}%, set at build time)`; + + const editMenu: MenuItemConstructorOptions = { + label: '&Edit', + submenu: [ + { + label: 'Undo', + accelerator: 'CmdOrCtrl+Z', + role: 'undo', + }, + { + label: 'Redo', + accelerator: 'Shift+CmdOrCtrl+Z', + role: 'redo', + }, + { + type: 'separator', + }, + { + label: 'Cut', + accelerator: 'CmdOrCtrl+X', + role: 'cut', + }, + { + label: 'Copy', + accelerator: 'CmdOrCtrl+C', + role: 'copy', + }, + { + label: 'Copy as Plain Text', + accelerator: 'CmdOrCtrl+Shift+C', + click: (): void => { + // We use clipboard.readText to strip down formatting + const text = clipboard.readText('selection'); + clipboard.writeText(cleanupPlainText(text), 'clipboard'); + }, + }, + { + label: 'Copy Current URL', + accelerator: 'CmdOrCtrl+L', + click: (): void => clipboard.writeText(getCurrentURL()), + }, + { + label: 'Paste', + accelerator: 'CmdOrCtrl+V', + role: 'paste', + }, + { + label: 'Paste and Match Style', + // https://github.com/nativefier/nativefier/issues/404 + // Apple's HIG lists this shortcut for paste and match style + // https://support.apple.com/en-us/HT209651 + accelerator: isOSX() ? 'Option+Shift+Cmd+V' : 'Ctrl+Shift+V', + role: 'pasteAndMatchStyle', + }, + { + label: 'Select All', + accelerator: 'CmdOrCtrl+A', + role: 'selectAll', + }, + { + label: 'Clear App Data', + click: ( + item: MenuItem, + focusedWindow: BrowserWindow | undefined, + ): void => { + log.debug('Clear App Data.click', { + item, + focusedWindow, + mainWindow, + }); + if (!focusedWindow) { + focusedWindow = mainWindow; + } + clearAppData(focusedWindow).catch((err) => + log.error('clearAppData ERROR', err), + ); + }, + }, + ], + }; + + const viewMenu: MenuItemConstructorOptions = { + label: '&View', + submenu: [ + { + label: 'Back', + accelerator: isOSX() ? 'Cmd+Left' : 'Alt+Left', + click: goBack, + }, + { + label: 'BackAdditionalShortcut', + visible: false, + acceleratorWorksWhenHidden: true, + accelerator: 'CmdOrCtrl+[', // What old versions of Nativefier used, kept for backwards compat + click: goBack, + }, + { + label: 'Forward', + accelerator: isOSX() ? 'Cmd+Right' : 'Alt+Right', + click: goForward, + }, + { + label: 'ForwardAdditionalShortcut', + visible: false, + acceleratorWorksWhenHidden: true, + accelerator: 'CmdOrCtrl+]', // What old versions of Nativefier used, kept for backwards compat + click: goForward, + }, + { + label: 'Reload', + role: 'reload', + }, + { + type: 'separator', + }, + { + label: 'Toggle Full Screen', + accelerator: isOSX() ? 'Ctrl+Cmd+F' : 'F11', + enabled: mainWindow.isFullScreenable() || isOSX(), + visible: mainWindow.isFullScreenable() || isOSX(), + click: ( + item: MenuItem, + focusedWindow: BrowserWindow | undefined, + ): void => { + log.debug('Toggle Full Screen.click()', { + item, + focusedWindow, + isFullScreen: focusedWindow?.isFullScreen(), + isFullScreenable: focusedWindow?.isFullScreenable(), + }); + if (!focusedWindow) { + focusedWindow = mainWindow; + } + if (focusedWindow.isFullScreenable()) { + focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); + } else if (isOSX()) { + focusedWindow.setSimpleFullScreen( + !focusedWindow.isSimpleFullScreen(), + ); + } + }, + }, + { + label: 'Zoom In', + accelerator: 'CmdOrCtrl+=', + click: zoomIn, + }, + { + label: 'ZoomInAdditionalShortcut', + visible: false, + acceleratorWorksWhenHidden: true, + accelerator: 'CmdOrCtrl+numadd', + click: zoomIn, + }, + { + label: 'Zoom Out', + accelerator: 'CmdOrCtrl+-', + click: zoomOut, + }, + { + label: 'ZoomOutAdditionalShortcut', + visible: false, + acceleratorWorksWhenHidden: true, + accelerator: 'CmdOrCtrl+numsub', + click: zoomOut, + }, + { + label: zoomResetLabel, + accelerator: 'CmdOrCtrl+0', + click: (): void => zoomReset(options), + }, + { + label: 'ZoomResetAdditionalShortcut', + visible: false, + acceleratorWorksWhenHidden: true, + accelerator: 'CmdOrCtrl+num0', + click: (): void => zoomReset(options), + }, + ], + }; + + if (!disableDevTools) { + (viewMenu.submenu as MenuItemConstructorOptions[]).push( + { + type: 'separator', + }, + { + label: 'Toggle Developer Tools', + accelerator: isOSX() ? 'Alt+Cmd+I' : 'Ctrl+Shift+I', + click: (item: MenuItem, focusedWindow: BrowserWindow | undefined) => { + log.debug('Toggle Developer Tools.click()', { item, focusedWindow }); + if (!focusedWindow) { + focusedWindow = mainWindow; + } + focusedWindow.webContents.toggleDevTools(); + }, + }, + ); + } + + const windowMenu: MenuItemConstructorOptions = { + label: '&Window', + role: 'window', + submenu: [ + { + label: 'Minimize', + accelerator: 'CmdOrCtrl+M', + role: 'minimize', + }, + { + label: 'Close', + accelerator: 'CmdOrCtrl+W', + role: 'close', + }, + ], + }; + + const helpMenu: MenuItemConstructorOptions = { + label: '&Help', + role: 'help', + submenu: [ + { + label: `Built with Nativefier v${nativefierVersion}`, + click: (): void => { + openExternal('https://github.com/nativefier/nativefier').catch( + (err: unknown): void => + log.error( + 'Built with Nativefier v${nativefierVersion}.click ERROR', + err, + ), + ); + }, + }, + { + label: 'Report an Issue', + click: (): void => { + openExternal('https://github.com/nativefier/nativefier/issues').catch( + (err: unknown): void => + log.error('Report an Issue.click ERROR', err), + ); + }, + }, + ], + }; + + let menuTemplate: MenuItemConstructorOptions[]; + + if (isOSX()) { + const electronMenu: MenuItemConstructorOptions = { + label: 'E&lectron', + submenu: [ + { + label: 'Services', + role: 'services', + submenu: [], + }, + { + type: 'separator', + }, + { + label: 'Hide App', + accelerator: 'Cmd+H', + role: 'hide', + }, + { + label: 'Hide Others', + accelerator: 'Cmd+Shift+H', + role: 'hideOthers', + }, + { + label: 'Show All', + role: 'unhide', + }, + { + type: 'separator', + }, + { + label: 'Quit', + accelerator: 'Cmd+Q', + role: 'quit', + }, + ], + }; + (windowMenu.submenu as MenuItemConstructorOptions[]).push( + { + type: 'separator', + }, + { + label: 'Bring All to Front', + role: 'front', + }, + ); + menuTemplate = [electronMenu, editMenu, viewMenu, windowMenu, helpMenu]; + } else { + menuTemplate = [editMenu, viewMenu, windowMenu, helpMenu]; + } + + return menuTemplate; +} + +function injectBookmarks(menuTemplate: MenuItemConstructorOptions[]): void { + const bookmarkConfigPath = path.join(__dirname, '..', 'bookmarks.json'); + + if (!fs.existsSync(bookmarkConfigPath)) { + return; + } + + try { + const bookmarksMenuConfig = JSON.parse( + fs.readFileSync(bookmarkConfigPath, 'utf-8'), + ) as BookmarksMenuConfig; + const submenu: MenuItemConstructorOptions[] = + bookmarksMenuConfig.bookmarks.map((bookmark) => { + switch (bookmark.type) { + case 'link': + if (!('title' in bookmark && 'url' in bookmark)) { + throw new Error( + 'All links in the bookmarks menu must have a title and url.', + ); + } + try { + new URL(bookmark.url); + } catch { + throw new Error('Bookmark URL "' + bookmark.url + '"is invalid.'); + } + return { + label: bookmark.title, + click: (): void => { + goToURL(bookmark.url)?.catch((err: unknown): void => + log.error(`${bookmark.title}.click ERROR`, err), + ); + }, + accelerator: + 'shortcut' in bookmark ? bookmark.shortcut : undefined, + }; + case 'separator': + return { + type: 'separator', + }; + default: + throw new Error( + 'A bookmarks menu entry has an invalid type; type must be one of "link", "separator".', + ); + } + }); + const bookmarksMenu: MenuItemConstructorOptions = { + label: bookmarksMenuConfig.menuLabel, + submenu, + }; + // Insert custom bookmarks menu between menus "View" and "Window" + menuTemplate.splice(menuTemplate.length - 2, 0, bookmarksMenu); + } catch (err: unknown) { + log.error('Failed to load & parse bookmarks configuration JSON file.', err); + } +} diff --git a/app/src/components/trayIcon.ts b/app/src/components/trayIcon.ts new file mode 100644 index 0000000..c3f46cd --- /dev/null +++ b/app/src/components/trayIcon.ts @@ -0,0 +1,88 @@ +import { app, Tray, Menu, ipcMain, nativeImage, BrowserWindow } from 'electron'; + +import { getAppIcon, getCounterValue, isOSX } from '../helpers/helpers'; +import * as log from '../helpers/loggingHelper'; +import { OutputOptions } from '../../../shared/src/options/model'; + +export function createTrayIcon( + nativefierOptions: OutputOptions, + mainWindow: BrowserWindow, +): Tray | undefined { + const options = { ...nativefierOptions }; + + if (options.tray && options.tray !== 'false') { + const iconPath = getAppIcon(); + if (!iconPath) { + throw new Error('Icon path not found found to use with tray option.'); + } + const nimage = nativeImage.createFromPath(iconPath); + const appIcon = new Tray(nativeImage.createEmpty()); + + if (isOSX()) { + //sets the icon to the height of the tray. + appIcon.setImage( + nimage.resize({ height: appIcon.getBounds().height - 2 }), + ); + } else { + appIcon.setImage(nimage); + } + + const onClick = (): void => { + log.debug('onClick'); + if (mainWindow.isVisible()) { + mainWindow.hide(); + } else { + mainWindow.show(); + } + }; + + const contextMenu = Menu.buildFromTemplate([ + { + label: options.name, + click: onClick, + }, + { + label: 'Quit', + click: (): void => app.exit(0), + }, + ]); + + appIcon.on('click', onClick); + + if (options.counter) { + mainWindow.on('page-title-updated', (event, title) => { + log.debug('mainWindow.page-title-updated', { event, title }); + const counterValue = getCounterValue(title); + if (counterValue) { + appIcon.setToolTip( + `(${counterValue}) ${options.name ?? 'Nativefier'}`, + ); + } else { + appIcon.setToolTip(options.name ?? ''); + } + }); + } else { + ipcMain.on('notification', () => { + log.debug('ipcMain.notification'); + if (mainWindow.isFocused()) { + return; + } + if (options.name) { + appIcon.setToolTip(`• ${options.name}`); + } + }); + + mainWindow.on('focus', () => { + log.debug('mainWindow.focus'); + appIcon.setToolTip(options.name ?? ''); + }); + } + + appIcon.setToolTip(options.name ?? ''); + appIcon.setContextMenu(contextMenu); + + return appIcon; + } + + return undefined; +} diff --git a/app/src/helpers/helpers.test.ts b/app/src/helpers/helpers.test.ts new file mode 100644 index 0000000..4f8c65e --- /dev/null +++ b/app/src/helpers/helpers.test.ts @@ -0,0 +1,348 @@ +import { shell } from 'electron'; +jest.mock('./windowHelpers'); + +import { + cleanupPlainText, + getCounterValue, + linkIsInternal, + openExternal, + removeUserAgentSpecifics, +} from './helpers'; +import { showNavigationBlockedMessage } from './windowHelpers'; + +const internalUrl = 'https://medium.com/'; +const internalUrlWww = 'https://www.medium.com/'; +const internalUrlSubPathRegex = /https:\/\/www.medium.com\/.*/; +const sameBaseDomainUrl = 'https://app.medium.com/'; +const internalUrlCoUk = 'https://medium.co.uk/'; +const differentBaseDomainUrlCoUk = 'https://other.domain.co.uk/'; +const sameBaseDomainUrlCoUk = 'https://app.medium.co.uk/'; +const sameBaseDomainUrlTidalListen = 'https://listen.tidal.com/'; +const sameBaseDomainUrlTidalLogin = 'https://login.tidal.com/'; +const sameBaseDomainUrlTidalRegex = /https:\/\/(login|listen).tidal.com\/.*/; +const internalUrlSubPath = 'topic/technology'; +const externalUrl = 'https://www.wikipedia.org/wiki/Electron'; +const wildcardRegex = /.*/; + +test('the original url should be internal without --strict-internal-urls', () => { + expect( + linkIsInternal(internalUrl, internalUrl, undefined, undefined), + ).toEqual(true); +}); + +test('the original url should be internal with --strict-internal-urls off', () => { + expect(linkIsInternal(internalUrl, internalUrl, undefined, false)).toEqual( + true, + ); +}); + +test('the original url should be internal with --strict-internal-urls on', () => { + expect(linkIsInternal(internalUrl, internalUrl, undefined, true)).toEqual( + true, + ); +}); + +test('sub-paths of the original url should be internal with --strict-internal-urls off', () => { + expect( + linkIsInternal( + internalUrl, + internalUrl + internalUrlSubPath, + undefined, + false, + ), + ).toEqual(true); +}); + +test('sub-paths of the original url should not be internal with --strict-internal-urls on', () => { + expect( + linkIsInternal( + internalUrl, + internalUrl + internalUrlSubPath, + undefined, + true, + ), + ).toEqual(false); +}); + +test('sub-paths of the original url should be internal with using a regex and --strict-internal-urls on', () => { + expect( + linkIsInternal( + internalUrl, + internalUrl + internalUrlSubPath, + internalUrlSubPathRegex, + true, + ), + ).toEqual(false); +}); + +test("'about:blank' should always be internal", () => { + expect(linkIsInternal(internalUrl, 'about:blank', undefined, true)).toEqual( + true, + ); +}); + +test('urls from different sites should not be internal', () => { + expect(linkIsInternal(internalUrl, externalUrl, undefined, false)).toEqual( + false, + ); +}); + +test('all urls should be internal with wildcard regex', () => { + expect(linkIsInternal(internalUrl, externalUrl, wildcardRegex, true)).toEqual( + true, + ); +}); + +test('a "www." of a domain should be considered internal', () => { + expect(linkIsInternal(internalUrl, internalUrlWww, undefined, false)).toEqual( + true, + ); +}); + +test('urls on the same "base domain" should be considered internal', () => { + expect( + linkIsInternal(internalUrl, sameBaseDomainUrl, undefined, false), + ).toEqual(true); +}); + +test('urls on the same "base domain" should NOT be considered internal using --strict-internal-urls', () => { + expect( + linkIsInternal(internalUrl, sameBaseDomainUrl, undefined, true), + ).toEqual(false); +}); + +test('urls on the same "base domain" should be considered internal, even with a www', () => { + expect( + linkIsInternal(internalUrlWww, sameBaseDomainUrl, undefined, false), + ).toEqual(true); +}); + +test('urls on the same "base domain" should be considered internal, even with different sub domains', () => { + expect( + linkIsInternal( + sameBaseDomainUrlTidalListen, + sameBaseDomainUrlTidalLogin, + undefined, + false, + ), + ).toEqual(true); +}); + +test('urls should support sub domain matching with a regex', () => { + expect( + linkIsInternal( + sameBaseDomainUrlTidalListen, + sameBaseDomainUrlTidalLogin, + sameBaseDomainUrlTidalRegex, + false, + ), + ).toEqual(true); +}); + +test('urls on the same "base domain" should NOT be considered internal with different sub domains when using --strict-internal-urls', () => { + expect( + linkIsInternal( + sameBaseDomainUrlTidalListen, + sameBaseDomainUrlTidalLogin, + undefined, + true, + ), + ).toEqual(false); +}); + +test('urls on the same "base domain" should be considered internal, long SLD', () => { + expect( + linkIsInternal(internalUrlCoUk, sameBaseDomainUrlCoUk, undefined, false), + ).toEqual(true); +}); + +test('urls on the a different "base domain" are considered NOT internal, long SLD', () => { + expect( + linkIsInternal( + internalUrlCoUk, + differentBaseDomainUrlCoUk, + undefined, + false, + ), + ).toEqual(false); +}); + +const testLoginPages = [ + 'https://amazon.co.uk/signin', + 'https://amazon.com/signin', + 'https://amazon.de/signin', + 'https://amazon.com/ap/signin', + 'https://facebook.co.uk/login', + 'https://facebook.com/login', + 'https://facebook.de/login', + 'https://github.co.uk/login', + 'https://github.com/login', + 'https://github.de/login', + // GitHub 2FA flow with FIDO token + 'https://github.com/session', + 'https://github.com/sessions/two-factor/webauth', + 'https://accounts.google.co.uk', + 'https://accounts.google.com', + 'https://mail.google.com/accounts/SetOSID', + 'https://mail.google.co.uk/accounts/SetOSID', + 'https://accounts.google.de', + 'https://linkedin.co.uk/uas/login', + 'https://linkedin.com/uas/login', + 'https://linkedin.de/uas/login', + 'https://login.live.co.uk', + 'https://login.live.com', + 'https://login.live.de', + 'https://login.microsoftonline.com/common/oauth2/authorize', + 'https://login.microsoftonline.co.uk/common/oauth2/authorize', + 'https://login.microsoftonline.de/common/oauth2/authorize', + 'https://okta.co.uk', + 'https://okta.com', + 'https://subdomain.okta.com', + 'https://okta.de', + 'https://twitter.co.uk/oauth/authenticate', + 'https://twitter.com/oauth/authenticate', + 'https://twitter.de/oauth/authenticate', + 'https://appleid.apple.com/auth/authorize', + 'https://id.atlassian.com', + 'https://auth.atlassian.com', + 'https://vmware.workspaceair.com', + 'https://vmware.auth.securid.com', +]; + +test.each(testLoginPages)( + '%s login page should be internal', + (loginUrl: string) => { + expect(linkIsInternal(internalUrl, loginUrl, undefined, false)).toEqual( + true, + ); + }, +); + +// Ensure that we don't over-match service pages +const testNonLoginPages = [ + 'https://www.amazon.com/Node-Cookbook-techniques-server-side-development-ebook', + 'https://github.com/nativefier/nativefier', + 'https://github.com/org/nativefier', + 'https://microsoft.com', + 'https://office.microsoftonline.com', + 'https://twitter.com/marcoroth_/status/1325938620906287104', + 'https://appleid.apple.com/account', + 'https://mail.google.com/', + 'https://atlassian.com', +]; + +test.each(testNonLoginPages)( + '%s page should not be internal', + (url: string) => { + expect(linkIsInternal(internalUrl, url, undefined, false)).toEqual(false); + }, +); + +const smallCounterTitle = 'Inbox (11) - nobody@example.com - Gmail'; +const largeCounterTitle = 'Inbox (8,756) - nobody@example.com - Gmail'; +const hourCounterTitle = 'Today (1:23) - nobody@example.com - TimeTracker'; +const noCounterTitle = 'Inbox - nobody@example.com - Gmail'; + +test('getCounterValue should return undefined for titles without counter numbers', () => { + expect(getCounterValue(noCounterTitle)).toEqual(undefined); +}); + +test('getCounterValue should return a string for small counter numbers in the title', () => { + expect(getCounterValue(smallCounterTitle)).toEqual('11'); +}); + +test('getCounterValue should return a string for large counter numbers in the title', () => { + expect(getCounterValue(largeCounterTitle)).toEqual('8,756'); +}); + +test('getCounterValue should return a string for hour counter numbers in the title', () => { + expect(getCounterValue(hourCounterTitle)).toEqual('1:23'); +}); + +describe('removeUserAgentSpecifics', () => { + const userAgentFallback = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) app-nativefier-804458/1.0.0 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36'; + test('removes Electron and App specific info', () => { + expect( + removeUserAgentSpecifics( + userAgentFallback, + 'app-nativefier-804458', + '1.0.0', + ), + ).not.toBe( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36', + ); + }); + + test('should not have multiple spaces in a row', () => { + expect( + removeUserAgentSpecifics( + userAgentFallback, + 'app-nativefier-804458', + '1.0.0', + ), + ).toEqual(expect.not.stringMatching(/\s{2,}/)); + }); +}); + +describe('cleanupPlainText', () => { + test('removes extra spaces from text', () => { + expect(cleanupPlainText(' this is a test ')).toBe('this is a test'); + }); +}); + +describe('openExternal', () => { + const mockShellOpenExternal: jest.SpyInstance = jest.spyOn( + shell, + 'openExternal', + ); + const mockShowNavigationBlockedMessage: jest.SpyInstance = + showNavigationBlockedMessage as jest.Mock; + + beforeEach(() => { + mockShellOpenExternal.mockReset(); + mockShowNavigationBlockedMessage + .mockReset() + .mockReturnValue(Promise.resolve(undefined)); + }); + + afterAll(() => { + mockShellOpenExternal.mockRestore(); + mockShowNavigationBlockedMessage.mockRestore(); + }); + + test('https urls scheme should *not* be blocked', async () => { + await openExternal('https://whatever.foo'); + + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockShellOpenExternal).toHaveBeenCalled(); + }); + + test('urls with whitelisted scheme should *not* be blocked', async () => { + await openExternal('ircs://irc.libera.chat/whatever'); + + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockShellOpenExternal).toHaveBeenCalled(); + }); + + test('urls with non-allowlisted scheme *should* be blocked', async () => { + await openExternal('barf://whatever.foo'); + + expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1); + expect(mockShellOpenExternal).not.toHaveBeenCalled(); + }); + + test('potentially-malicious urls *should* be blocked', async () => { + await openExternal('https://hello.com/wor%00ld'); + + expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1); + expect(mockShellOpenExternal).not.toHaveBeenCalled(); + }); + + test('malformed urls *should* be blocked', async () => { + await openExternal('zombocom'); + + expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1); + expect(mockShellOpenExternal).not.toHaveBeenCalled(); + }); +}); diff --git a/app/src/helpers/helpers.ts b/app/src/helpers/helpers.ts new file mode 100644 index 0000000..335768c --- /dev/null +++ b/app/src/helpers/helpers.ts @@ -0,0 +1,315 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { BrowserWindow, OpenExternalOptions, shell } from 'electron'; + +import * as log from '../helpers/loggingHelper'; +import { showNavigationBlockedMessage } from './windowHelpers'; + +export const INJECT_DIR = path.join(__dirname, '..', 'inject'); + +/** + * Firefox's list of protocols for which opening an external handler is allowed without confirmation. + * Taken from Firefox's. Location might vary in codebase, search for one of them, e.g. + * https://searchfox.org/mozilla-central/search?q=%22xmpp%22&path=&case=false®exp=false + */ +const URL_PROTOCOLS_NOCONFIRMATION_FIREFOX = [ + 'bitcoin:', + 'ftp:', + 'ftps:', + 'geo:', + 'im:', + 'irc:', + 'ircs:', + 'magnet:', + 'mailto:', + 'matrix:', + 'mms:', + 'news:', + 'nntp:', + 'openpgp4fpr:', + 'sftp:', + 'sip:', + 'sms:', + 'smsto:', + 'ssh:', + 'tel:', + 'urn:', + 'webcal:', + 'wtai:', + 'xmpp:', +]; +/** + * Our extension to Firefox's list. If extending this list too much, we should + * really add a confirmation modal (for now we just block), like browsers do. + * But for now, since nobody shouts at us for bluntly blocking anything else, + * let's keep rolling with it. + */ +const URL_PROTOCOLS_NOCONFIRMATION_EXTRA = ['zoommtg:']; +/** + * List of protocols for which opening an external handler is allowed without confirmation. + * Note: "without confirmation" is currently a lie. It was implemented this way + * as a way to know from user feedback what protocols would cause users to shout, + * but there wasn't much shouting happening, so we currently don't have a confirmation + * mechanism, we just bluntly block. That might need to change at some point. + */ +const URL_PROTOCOLS_NOCONFIRMATION = [ + 'http:', + 'https:', + ...URL_PROTOCOLS_NOCONFIRMATION_FIREFOX, + ...URL_PROTOCOLS_NOCONFIRMATION_EXTRA, +]; +const SHELL_SAFETY_FEEDBACK_STR = + 'If you believe this URL should open, you might be right, and our validation might be excessive.' + + 'Please share this error & URL at https://github.com/nativefier/nativefier/issues/1459'; + +export function isUrlShellSafe( + urlToGo: string, +): { blocked: false } | { blocked: true; reason: string } { + let url: URL; + try { + url = new URL(urlToGo.toLowerCase()); + } catch (err: unknown) { + return { + blocked: true, + reason: `URL appears malformed. ${SHELL_SAFETY_FEEDBACK_STR}`, + }; + } + + if (!URL_PROTOCOLS_NOCONFIRMATION.includes(url.protocol)) { + return { + blocked: true, + reason: `URL protocol is disallowed. ${SHELL_SAFETY_FEEDBACK_STR}`, + }; + } + + // https://cwe.mitre.org/data/definitions/177.html + if ( + urlToGo.includes('%00') || + urlToGo.includes('%0a') || + urlToGo.includes('%2e') || + urlToGo.includes('%2f') || + urlToGo.includes('%5c') + ) { + return { + blocked: true, + reason: `URL might be malicious. ${SHELL_SAFETY_FEEDBACK_STR}`, + }; + } + + return { blocked: false }; +} + +/** + * Helper to print debug messages from the main process in the browser window + */ +export function debugLog(browserWindow: BrowserWindow, message: string): void { + // Need a delay, as it takes time for the preloaded js to be loaded by the window + setTimeout(() => { + browserWindow.webContents.send('debug', message); + }, 3000); + log.debug(message); +} + +/** + * Helper to determine domain-ish equality for many cases, the trivial ones + * and the trickier ones, e.g. `blog.foo.com` and `shop.foo.com`, + * in a way that is "good enough", and doesn't need a list of SLDs. + * See chat at https://github.com/nativefier/nativefier/pull/1171#pullrequestreview-649132523 + */ +function domainify(url: string): string { + // So here's what we're doing here: + // Get the hostname from the url + const hostname = new URL(url).hostname; + // Drop the first section if the domain + const domain = hostname.split('.').slice(1).join('.'); + // Check the length, if it's too short, the hostname was probably the domain + // Or if the domain doesn't have a . in it we went too far + if (domain.length < 6 || domain.split('.').length === 0) { + return hostname; + } + // This SHOULD be the domain, but nothing is 100% guaranteed + return domain; +} + +export function getAppIcon(): string | undefined { + // Prefer ICO under Windows, see + // https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions + // https://www.electronjs.org/docs/api/native-image#supported-formats + if (isWindows()) { + const ico = path.join(__dirname, '..', 'icon.ico'); + if (fs.existsSync(ico)) { + return ico; + } + } + const png = path.join(__dirname, '..', 'icon.png'); + if (fs.existsSync(png)) { + return png; + } +} + +export function getCounterValue(title: string): string | undefined { + const itemCountRegex = /[([{]([\d.,:]*)\+?[}\])]/; + const match = itemCountRegex.exec(title); + return match ? match[1] : undefined; +} + +export function getCSSToInject(): string { + let cssToInject = ''; + const cssFiles = fs + .readdirSync(INJECT_DIR, { withFileTypes: true }) + .filter( + (injectFile) => injectFile.isFile() && injectFile.name.endsWith('.css'), + ) + .map((cssFileStat) => + path.resolve(path.join(INJECT_DIR, cssFileStat.name)), + ); + for (const cssFile of cssFiles) { + log.debug('Injecting CSS file', cssFile); + const cssFileData = fs.readFileSync(cssFile); + cssToInject += `/* ${cssFile} */\n\n ${cssFileData.toString()}\n\n`; + } + return cssToInject; +} + +export function isOSX(): boolean { + return os.platform() === 'darwin'; +} + +export function isLinux(): boolean { + return os.platform() === 'linux'; +} + +export function isWindows(): boolean { + return os.platform() === 'win32'; +} + +function isInternalLoginPage(url: string): boolean { + // Making changes? Remember to update the tests in helpers.test.ts and in API.md + const internalLoginPagesArray = [ + 'amazon\\.[a-zA-Z\\.]*/[a-zA-Z\\/]*signin', // Amazon + `facebook\\.[a-zA-Z\\.]*\\/login`, // Facebook + 'github\\.[a-zA-Z\\.]*\\/(?:login|session)', // GitHub + 'accounts\\.google\\.[a-zA-Z\\.]*', // Google + 'mail\\.google\\.[a-zA-Z\\.]*\\/accounts/SetOSID', // Google + 'linkedin\\.[a-zA-Z\\.]*/uas/login', // LinkedIn + 'login\\.live\\.[a-zA-Z\\.]*', // Microsoft + 'login\\.microsoftonline\\.[a-zA-Z\\.]*', // Microsoft + 'okta\\.[a-zA-Z\\.]*', // Okta + 'twitter\\.[a-zA-Z\\.]*/oauth/authenticate', // Twitter + 'appleid\\.apple\\.com/auth/authorize', // Apple + '(?:id|auth)\\.atlassian\\.[a-zA-Z]+', // Atlassian + '.*\\.workspaceair\\.com', // VMWare Workspace One SSO + '.*\\.securid\\.com', // SecurID for VMWare Workspace One SSO + ]; + // Making changes? Remember to update the tests in helpers.test.ts and in API.md + const regex = RegExp(internalLoginPagesArray.join('|')); + return regex.test(url); +} + +export function linkIsInternal( + currentUrl: string, + newUrl: string, + internalUrlRegex: string | RegExp | undefined, + isStrictInternalUrlsEnabled: boolean | undefined, +): boolean { + log.debug('linkIsInternal', { currentUrl, newUrl, internalUrlRegex }); + if (newUrl.split('#')[0] === 'about:blank') { + return true; + } + + if (isInternalLoginPage(newUrl)) { + return true; + } + + if (internalUrlRegex) { + const regex = RegExp(internalUrlRegex); + if (regex.test(newUrl)) { + return true; + } + } + + if (isStrictInternalUrlsEnabled) { + return currentUrl == newUrl; + } + + try { + // Consider as "same domain-ish", without TLD/SLD list: + // 1. app.foo.com and foo.com + // 2. www.foo.com and foo.com + // 3. www.foo.com and app.foo.com + + // Only use the tld and the main domain for domain-ish test + // Enables domain-ish equality for blog.foo.com and shop.foo.com + return domainify(currentUrl) === domainify(newUrl); + } catch (err: unknown) { + log.error( + 'Failed to parse domains as determining if link is internal. From:', + currentUrl, + 'To:', + newUrl, + err, + ); + return false; + } +} + +export function nativeTabsSupported(): boolean { + return isOSX(); +} + +/** + * Open the given external protocol URL in the desktop's default manner + * (e.g. `mailto:` URLs in the user's default mail agent), with extra validation. + */ +export function openExternal( + url: string, + options?: OpenExternalOptions, +): Promise { + const urlShellSafety = isUrlShellSafe(url); + log.debug('openExternal', { url, options, urlShellSafety }); + if (urlShellSafety.blocked) { + return new Promise((resolve) => { + showNavigationBlockedMessage( + `Navigation blocked to ${url}\n\n${urlShellSafety.reason}`, + ) + .then(() => resolve()) + .catch((err: unknown) => { + throw err; + }); + }); + } + + return shell.openExternal(url, options); +} + +// Copy-pastaed as unable to get imports to work in preload. +// If modifying, update also app/src/preload.ts +export function isWayland(): boolean { + return ( + isLinux() && + (Boolean(process.env.WAYLAND_DISPLAY) || + process.env.XDG_SESSION_TYPE === 'wayland') + ); +} + +export function removeUserAgentSpecifics( + userAgentFallback: string, + appName: string, + appVersion: string, +): string { + // Electron userAgentFallback is the user agent used if none is specified when creating a window. + // For our purposes, it's useful because its format is similar enough to a real Chrome's user agent to not need + // to infer the userAgent. userAgentFallback normally looks like this: + // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) app-nativefier-804458/1.0.0 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36 + // We just need to strip out the appName/1.0.0 and Electron/electronVersion + return userAgentFallback + .replace(`Electron/${process.versions.electron} `, '') + .replace(`${appName}/${appVersion} `, ''); +} + +/** Removes extra spaces from a text */ +export function cleanupPlainText(text: string): string { + return text.trim().replace(/\s+/g, ' '); +} diff --git a/app/src/helpers/inferFlash.ts b/app/src/helpers/inferFlash.ts new file mode 100644 index 0000000..eeb1282 --- /dev/null +++ b/app/src/helpers/inferFlash.ts @@ -0,0 +1,90 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { isOSX, isWindows, isLinux } from './helpers'; +import * as log from './loggingHelper'; + +type fsError = Error & { code: string }; + +/** + * Find a file or directory + */ +function findSync( + pattern: RegExp, + basePath: string, + limitSearchToDirectories = false, +): string[] { + const matches: string[] = []; + + (function findSyncRecurse(base): void { + let children: string[]; + try { + children = fs.readdirSync(base); + } catch (err: unknown) { + if ((err as fsError).code === 'ENOENT') { + return; + } + throw err; + } + + for (const child of children) { + const childPath = path.join(base, child); + const childIsDirectory = fs.lstatSync(childPath).isDirectory(); + const patternMatches = pattern.test(childPath); + + if (!patternMatches) { + if (!childIsDirectory) { + return; + } + findSyncRecurse(childPath); + return; + } + + if (!limitSearchToDirectories) { + matches.push(childPath); + return; + } + + if (childIsDirectory) { + matches.push(childPath); + } + } + })(basePath); + return matches; +} + +function findFlashOnLinux(): string { + return findSync(/libpepflashplayer\.so/, '/opt/google/chrome')[0]; +} + +function findFlashOnWindows(): string { + return findSync( + /pepflashplayer\.dll/, + 'C:\\Program Files (x86)\\Google\\Chrome', + )[0]; +} + +function findFlashOnMac(): string { + return findSync( + /PepperFlashPlayer.plugin/, + '/Applications/Google Chrome.app/', + true, + )[0]; +} + +export function inferFlashPath(): string | undefined { + if (isOSX()) { + return findFlashOnMac(); + } + + if (isWindows()) { + return findFlashOnWindows(); + } + + if (isLinux()) { + return findFlashOnLinux(); + } + + log.warn('Unable to determine OS to infer flash player'); + return undefined; +} diff --git a/app/src/helpers/loggingHelper.ts b/app/src/helpers/loggingHelper.ts new file mode 100644 index 0000000..1735d96 --- /dev/null +++ b/app/src/helpers/loggingHelper.ts @@ -0,0 +1,82 @@ +// This helper allows logs to either be printed to the console as they would normally or if +// the USE_LOG_FILE environment variable is set (such as through our playwright tests), then +// the logs can be diverted from the command line to a log file, so that they can be displayed +// later (such as at the end of a playwright test run to help diagnose potential failures). +// Use this instead of loglevel whenever logging messages inside the app. + +import * as fs from 'fs'; +import * as path from 'path'; + +import loglevel from 'loglevel'; + +import { safeGetEnv } from './playwrightHelpers'; + +const USE_LOG_FILE = safeGetEnv('USE_LOG_FILE') === '1'; +const LOG_FILE_DIR = safeGetEnv('LOG_FILE_DIR') ?? process.cwd(); +const LOG_FILENAME = path.join(LOG_FILE_DIR, `${new Date().getTime()}.log`); + +const logLevelNames = ['TRACE', 'DEBUG', 'INFO ', 'WARN ', 'ERROR']; + +function _logger( + logFunc: (...args: unknown[]) => void, + level: loglevel.LogLevelNumbers, + ...args: unknown[] +): void { + if (USE_LOG_FILE && loglevel.getLevel() >= level) { + for (const arg of args) { + try { + const lines = + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + JSON.stringify(arg, null, 2)?.split('\n') ?? `${arg}`.split('\n'); + for (const line of lines) { + fs.appendFileSync( + LOG_FILENAME, + `${new Date().getTime()} ${logLevelNames[level]} ${line}\n`, + ); + } + } catch { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + fs.appendFileSync(LOG_FILENAME, `${logLevelNames[level]} ${arg}\n`); + } + } + } else { + logFunc(...args); + } +} + +export function debug(...args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + _logger(loglevel.debug, loglevel.levels.DEBUG, ...args); +} + +export function error(...args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + _logger(loglevel.error, loglevel.levels.ERROR, ...args); +} + +export function info(...args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + _logger(loglevel.info, loglevel.levels.INFO, ...args); +} + +export function log(...args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + _logger(loglevel.info, loglevel.levels.INFO, ...args); +} + +export function setLevel( + level: loglevel.LogLevelDesc, + persist?: boolean, +): void { + loglevel.setLevel(level, persist); +} + +export function trace(...args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + _logger(loglevel.trace, loglevel.levels.TRACE, ...args); +} + +export function warn(...args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + _logger(loglevel.warn, loglevel.levels.WARN, ...args); +} diff --git a/app/src/helpers/playwrightHelpers.ts b/app/src/helpers/playwrightHelpers.ts new file mode 100644 index 0000000..6b58961 --- /dev/null +++ b/app/src/helpers/playwrightHelpers.ts @@ -0,0 +1,6 @@ +export const IS_PLAYWRIGHT = safeGetEnv('PLAYWRIGHT_TEST') === '1'; +export const PLAYWRIGHT_CONFIG = safeGetEnv('PLAYWRIGHT_CONFIG'); + +export function safeGetEnv(key: string): string | undefined { + return key in process.env ? process.env[key] : undefined; +} diff --git a/app/src/helpers/windowEvents.test.ts b/app/src/helpers/windowEvents.test.ts new file mode 100644 index 0000000..292060d --- /dev/null +++ b/app/src/helpers/windowEvents.test.ts @@ -0,0 +1,353 @@ +jest.mock('./helpers'); +jest.mock('./windowEvents'); +jest.mock('./windowHelpers'); + +import { dialog, BrowserWindow, HandlerDetails, WebContents } from 'electron'; +import { WindowOptions } from '../../../shared/src/options/model'; +import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers'; +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const { + onNewWindowHelper, + onWillNavigate, + onWillPreventUnload, +}: { + onNewWindowHelper: ( + options: WindowOptions, + setupWindow: (options: WindowOptions, window: BrowserWindow) => void, + details: Partial, + parent?: BrowserWindow, + ) => ReturnType[0]>; + onWillNavigate: ( + options: { + blockExternalUrls: boolean; + internalUrls?: string | RegExp; + targetUrl: string; + }, + event: unknown, + urlToGo: string, + ) => Promise; + onWillPreventUnload: (event: unknown) => void; +} = jest.requireActual('./windowEvents'); +import { + showNavigationBlockedMessage, + createAboutBlankWindow, + createNewTab, +} from './windowHelpers'; + +describe('onNewWindowHelper', () => { + const originalURL = 'https://medium.com/'; + const internalURL = 'https://medium.com/topics/technology'; + const externalURL = 'https://www.wikipedia.org/wiki/Electron'; + const foregroundDisposition = 'foreground-tab'; + const backgroundDisposition = 'background-tab'; + const baseOptions = { + autoHideMenuBar: true, + blockExternalUrls: false, + insecure: false, + name: 'TEST_APP', + targetUrl: originalURL, + zoom: 1.0, + } as WindowOptions; + const mockShowNavigationBlockedMessage: jest.SpyInstance = + showNavigationBlockedMessage as jest.Mock; + const mockCreateAboutBlank: jest.SpyInstance = + createAboutBlankWindow as jest.Mock; + const mockCreateNewTab: jest.SpyInstance = createNewTab as jest.Mock; + const mockLinkIsInternal: jest.SpyInstance = ( + linkIsInternal as jest.Mock + ).mockImplementation(() => true); + const mockNativeTabsSupported: jest.SpyInstance = + nativeTabsSupported as jest.Mock; + const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock; + const setupWindow = jest.fn(); + + beforeEach(() => { + mockShowNavigationBlockedMessage + .mockReset() + .mockReturnValue(Promise.resolve(undefined)); + mockCreateAboutBlank.mockReset(); + mockCreateNewTab.mockReset(); + mockLinkIsInternal.mockReset().mockReturnValue(true); + mockNativeTabsSupported.mockReset().mockReturnValue(false); + mockOpenExternal.mockReset(); + setupWindow.mockReset(); + }); + + afterAll(() => { + mockShowNavigationBlockedMessage.mockRestore(); + mockCreateAboutBlank.mockRestore(); + mockCreateNewTab.mockRestore(); + mockLinkIsInternal.mockRestore(); + mockNativeTabsSupported.mockRestore(); + mockOpenExternal.mockRestore(); + }); + + test('internal urls should not be handled', () => { + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: internalURL, + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('allow'); + }); + + test('external urls should be opened externally', () => { + mockLinkIsInternal.mockReturnValue(false); + + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: externalURL, + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).toHaveBeenCalledTimes(1); + expect(result.action).toEqual('deny'); + }); + + test('external urls should be ignored if blockExternalUrls is true', () => { + mockLinkIsInternal.mockReturnValue(false); + const options = { + ...baseOptions, + blockExternalUrls: true, + }; + const result = onNewWindowHelper(options, setupWindow, { + url: externalURL, + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('deny'); + }); + + test('tab disposition should be ignored if tabs are not enabled', () => { + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: internalURL, + disposition: foregroundDisposition, + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('allow'); + }); + + test('tab disposition should be ignored if url is external', () => { + mockLinkIsInternal.mockReturnValue(false); + + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: externalURL, + disposition: foregroundDisposition, + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).toHaveBeenCalledTimes(1); + expect(result.action).toEqual('deny'); + }); + + test('foreground tabs with internal urls should be opened in the foreground', () => { + mockNativeTabsSupported.mockReturnValue(true); + + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: internalURL, + disposition: foregroundDisposition, + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).toHaveBeenCalledTimes(1); + expect(mockCreateNewTab).toHaveBeenCalledWith( + baseOptions, + setupWindow, + internalURL, + true, + ); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('deny'); + }); + + test('background tabs with internal urls should be opened in background tabs', () => { + mockNativeTabsSupported.mockReturnValue(true); + + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: internalURL, + disposition: backgroundDisposition, + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).toHaveBeenCalledTimes(1); + expect(mockCreateNewTab).toHaveBeenCalledWith( + baseOptions, + setupWindow, + internalURL, + false, + ); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('deny'); + }); + + test('about:blank urls should be handled', () => { + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: 'about:blank', + }); + + expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('deny'); + }); + + test('about:blank#blocked urls should be handled', () => { + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: 'about:blank#blocked', + }); + + expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('deny'); + }); + + test('about:blank#other urls should not be handled', () => { + const result = onNewWindowHelper(baseOptions, setupWindow, { + url: 'about:blank#other', + }); + + expect(mockCreateAboutBlank).not.toHaveBeenCalled(); + expect(mockCreateNewTab).not.toHaveBeenCalled(); + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(result.action).toEqual('allow'); + }); +}); + +describe('onWillNavigate', () => { + const originalURL = 'https://medium.com/'; + const internalURL = 'https://medium.com/topics/technology'; + const externalURL = 'https://www.wikipedia.org/wiki/Electron'; + + const mockShowNavigationBlockedMessage: jest.SpyInstance = + showNavigationBlockedMessage as jest.Mock; + const mockLinkIsInternal: jest.SpyInstance = linkIsInternal as jest.Mock; + const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock; + const preventDefault = jest.fn(); + + beforeEach(() => { + mockShowNavigationBlockedMessage + .mockReset() + .mockReturnValue(Promise.resolve(undefined)); + mockLinkIsInternal.mockReset().mockReturnValue(false); + mockOpenExternal.mockReset(); + preventDefault.mockReset(); + }); + + afterAll(() => { + mockShowNavigationBlockedMessage.mockRestore(); + mockLinkIsInternal.mockRestore(); + mockOpenExternal.mockRestore(); + }); + + test('internal urls should not be handled', async () => { + mockLinkIsInternal.mockReturnValue(true); + const options = { + blockExternalUrls: false, + targetUrl: originalURL, + }; + const event = { preventDefault }; + await onWillNavigate(options, event, internalURL); + + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); + + test('external urls should be opened externally', async () => { + const options = { + blockExternalUrls: false, + targetUrl: originalURL, + }; + const event = { preventDefault }; + await onWillNavigate(options, event, externalURL); + + expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled(); + expect(mockOpenExternal).toHaveBeenCalledTimes(1); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); + + test('external urls should be blocked if blockExternalUrls is true', async () => { + const options = { + blockExternalUrls: true, + targetUrl: originalURL, + }; + const event = { preventDefault }; + await onWillNavigate(options, event, externalURL); + + expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1); + expect(mockOpenExternal).not.toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledTimes(1); + }); +}); + +describe('onWillPreventUnload', () => { + const mockFromWebContents: jest.SpyInstance = jest + .spyOn(BrowserWindow, 'fromWebContents') + .mockImplementation(() => new BrowserWindow()); + const mockShowDialog: jest.SpyInstance = jest.spyOn( + dialog, + 'showMessageBoxSync', + ); + const preventDefault: jest.SpyInstance = jest.fn(); + + beforeEach(() => { + mockFromWebContents.mockReset(); + mockShowDialog.mockReset().mockReturnValue(undefined); + preventDefault.mockReset(); + }); + + afterAll(() => { + mockFromWebContents.mockRestore(); + mockShowDialog.mockRestore(); + }); + + test('with no sender', () => { + const event = {}; + onWillPreventUnload(event); + + expect(mockFromWebContents).not.toHaveBeenCalled(); + expect(mockShowDialog).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); + + test('shows dialog and calls preventDefault on ok', () => { + mockShowDialog.mockReturnValue(0); + + const event = { preventDefault, sender: {} }; + onWillPreventUnload(event); + + expect(mockFromWebContents).toHaveBeenCalledWith(event.sender); + expect(mockShowDialog).toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalledWith(); + }); + + test('shows dialog and does not call preventDefault on cancel', () => { + mockShowDialog.mockReturnValue(1); + + const event = { preventDefault, sender: {} }; + onWillPreventUnload(event); + + expect(mockFromWebContents).toHaveBeenCalledWith(event.sender); + expect(mockShowDialog).toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + }); +}); diff --git a/app/src/helpers/windowEvents.ts b/app/src/helpers/windowEvents.ts new file mode 100644 index 0000000..fcbcc26 --- /dev/null +++ b/app/src/helpers/windowEvents.ts @@ -0,0 +1,188 @@ +import { + dialog, + BrowserWindow, + Event, + WebContents, + HandlerDetails, +} from 'electron'; + +import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers'; +import * as log from './loggingHelper'; +import { + createAboutBlankWindow, + createNewTab, + injectCSS, + sendParamsOnDidFinishLoad, + setProxyRules, + showNavigationBlockedMessage, +} from './windowHelpers'; +import { WindowOptions } from '../../../shared/src/options/model'; + +type NewWindowHandlerResult = ReturnType< + Parameters[0] +>; + +export function onNewWindow( + options: WindowOptions, + setupWindow: (options: WindowOptions, window: BrowserWindow) => void, + details: HandlerDetails, + parent?: BrowserWindow, +): NewWindowHandlerResult { + log.debug('onNewWindow', { + details, + }); + return onNewWindowHelper( + options, + setupWindow, + details, + nativeTabsSupported() ? undefined : parent, + ); +} + +export function onNewWindowHelper( + options: WindowOptions, + setupWindow: (options: WindowOptions, window: BrowserWindow) => void, + details: HandlerDetails, + parent?: BrowserWindow, +): NewWindowHandlerResult { + log.debug('onNewWindowHelper', { + options, + details, + }); + try { + if ( + !linkIsInternal( + options.targetUrl, + details.url, + options.internalUrls, + options.strictInternalUrls, + ) + ) { + if (options.blockExternalUrls) { + showNavigationBlockedMessage( + `Navigation to external URL blocked by options: ${details.url}`, + ) + .then(() => { + // blockExternalURL(details.url).then(resolve).catch((err: unknown) => { + // log.error('blockExternalURL', err); + // }); + }) + .catch((err: unknown) => { + throw err; + }); + return { action: 'deny' }; + } else { + openExternal(details.url).catch((err: unknown) => { + log.error('openExternal', err); + }); + return { action: 'deny' }; + } + } + // Normally the following would be: + // if (urlToGo.startsWith('about:blank'))... + // But due to a bug we resolved in https://github.com/nativefier/nativefier/issues/1197 + // Some sites use about:blank#something to use as placeholder windows to fill + // with content via JavaScript. So we'll stay specific for now... + else if (['about:blank', 'about:blank#blocked'].includes(details.url)) { + createAboutBlankWindow( + options, + setupWindow, + nativeTabsSupported() ? undefined : parent, + ); + return { action: 'deny' }; + } else if (nativeTabsSupported()) { + createNewTab( + options, + setupWindow, + details.url, + details.disposition === 'foreground-tab', + ); + return { action: 'deny' }; + } + return { action: 'allow' }; + } catch (err: unknown) { + return { action: 'deny' }; + } +} + +export function onWillNavigate( + options: WindowOptions, + event: Event, + urlToGo: string, +): Promise { + log.debug('onWillNavigate', urlToGo); + if ( + !linkIsInternal( + options.targetUrl, + urlToGo, + options.internalUrls, + options.strictInternalUrls, + ) + ) { + event.preventDefault(); + if (options.blockExternalUrls) { + return new Promise((resolve) => { + showNavigationBlockedMessage( + `Navigation to external URL blocked by options: ${urlToGo}`, + ) + .then(() => resolve()) + .catch((err: unknown) => { + throw err; + }); + }); + } else { + return openExternal(urlToGo); + } + } + return Promise.resolve(undefined); +} + +export function onWillPreventUnload( + event: Event & { sender?: WebContents }, +): void { + log.debug('onWillPreventUnload', event); + + const webContents = event.sender; + if (!webContents) { + return; + } + + const browserWindow = + BrowserWindow.fromWebContents(webContents) ?? + BrowserWindow.getFocusedWindow(); + if (browserWindow) { + const choice = dialog.showMessageBoxSync(browserWindow, { + type: 'question', + buttons: ['Proceed', 'Stay'], + message: + 'You may have unsaved changes, are you sure you want to proceed?', + title: 'Changes you made may not be saved.', + defaultId: 0, + cancelId: 1, + }); + if (choice === 0) { + event.preventDefault(); + } + } +} + +export function setupNativefierWindow( + options: WindowOptions, + window: BrowserWindow, +): void { + if (options.proxyRules) { + setProxyRules(window, options.proxyRules); + } + + injectCSS(window); + + window.webContents.on('will-navigate', (event: Event, url: string) => { + onWillNavigate(options, event, url).catch((err) => { + log.error('window.webContents.on.will-navigate ERROR', err); + event.preventDefault(); + }); + }); + window.webContents.on('will-prevent-unload', onWillPreventUnload); + + sendParamsOnDidFinishLoad(options, window); +} diff --git a/app/src/helpers/windowHelpers.test.ts b/app/src/helpers/windowHelpers.test.ts new file mode 100644 index 0000000..0b0dbec --- /dev/null +++ b/app/src/helpers/windowHelpers.test.ts @@ -0,0 +1,292 @@ +import { dialog, BrowserWindow } from 'electron'; +jest.mock('loglevel'); +import { error } from 'loglevel'; +import { WindowOptions } from '../../../shared/src/options/model'; + +jest.mock('./helpers'); +import { getCSSToInject } from './helpers'; +jest.mock('./windowEvents'); +import { clearAppData, createNewTab, injectCSS } from './windowHelpers'; + +describe('clearAppData', () => { + let window: BrowserWindow; + let mockClearCache: jest.SpyInstance; + let mockClearStorageData: jest.SpyInstance; + const mockShowDialog: jest.SpyInstance = jest.spyOn(dialog, 'showMessageBox'); + + beforeEach(() => { + window = new BrowserWindow(); + mockClearCache = jest.spyOn(window.webContents.session, 'clearCache'); + mockClearStorageData = jest.spyOn( + window.webContents.session, + 'clearStorageData', + ); + mockShowDialog.mockReset().mockResolvedValue(undefined); + }); + + afterAll(() => { + mockClearCache.mockRestore(); + mockClearStorageData.mockRestore(); + mockShowDialog.mockRestore(); + }); + + test('will not clear app data if dialog canceled', async () => { + mockShowDialog.mockResolvedValue(1); + + await clearAppData(window); + + expect(mockShowDialog).toHaveBeenCalledTimes(1); + expect(mockClearCache).not.toHaveBeenCalled(); + expect(mockClearStorageData).not.toHaveBeenCalled(); + }); + + test('will clear app data if ok is clicked', async () => { + mockShowDialog.mockResolvedValue(0); + + await clearAppData(window); + + expect(mockShowDialog).toHaveBeenCalledTimes(1); + expect(mockClearCache).not.toHaveBeenCalledTimes(1); + expect(mockClearStorageData).not.toHaveBeenCalledTimes(1); + }); +}); + +describe('createNewTab', () => { + // const window = new BrowserWindow(); + const options: WindowOptions = { + autoHideMenuBar: true, + blockExternalUrls: false, + insecure: false, + name: 'Test App', + targetUrl: 'https://github.com/nativefier/natifefier', + zoom: 1.0, + } as WindowOptions; + const setupWindow = jest.fn(); + const url = 'https://github.com/nativefier/nativefier'; + const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn( + BrowserWindow.prototype, + 'addTabbedWindow', + ); + const mockFocus: jest.SpyInstance = jest.spyOn( + BrowserWindow.prototype, + 'focus', + ); + const mockLoadURL: jest.SpyInstance = jest.spyOn( + BrowserWindow.prototype, + 'loadURL', + ); + + test('creates new foreground tab', () => { + const foreground = true; + + const tab = createNewTab(options, setupWindow, url, foreground); + + expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); + expect(setupWindow).toHaveBeenCalledWith(options, tab); + expect(mockLoadURL).toHaveBeenCalledWith(url); + expect(mockFocus).not.toHaveBeenCalled(); + }); + + test('creates new background tab', () => { + const foreground = false; + + const tab = createNewTab( + options, + setupWindow, + url, + foreground, + // window + ); + + expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab); + expect(setupWindow).toHaveBeenCalledWith(options, tab); + expect(mockLoadURL).toHaveBeenCalledWith(url); + expect(mockFocus).toHaveBeenCalledTimes(1); + }); +}); + +describe('injectCSS', () => { + jest.setTimeout(10000); + + const mockGetCSSToInject: jest.SpyInstance = getCSSToInject as jest.Mock; + const mockLogError: jest.SpyInstance = error as jest.Mock; + + const css = 'body { color: white; }'; + let responseHeaders: Record; + + beforeEach(() => { + mockGetCSSToInject.mockReset().mockReturnValue(''); + mockLogError.mockReset(); + responseHeaders = { 'x-header': ['value'], 'content-type': ['test/other'] }; + }); + + afterAll(() => { + mockGetCSSToInject.mockRestore(); + mockLogError.mockRestore(); + }); + + test('will not inject if getCSSToInject is empty', () => { + const window = new BrowserWindow(); + const mockWebContentsInsertCSS: jest.SpyInstance = jest + .spyOn(window.webContents, 'insertCSS') + .mockResolvedValue(''); + jest + .spyOn(window.webContents, 'getURL') + .mockReturnValue('https://example.com'); + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + expect(mockWebContentsInsertCSS).not.toHaveBeenCalled(); + }); + + test('will inject on did-navigate + onResponseStarted', () => { + mockGetCSSToInject.mockReturnValue(css); + const window = new BrowserWindow(); + const mockWebContentsInsertCSS: jest.SpyInstance = jest + .spyOn(window.webContents, 'insertCSS') + .mockResolvedValue(''); + jest + .spyOn(window.webContents, 'getURL') + .mockReturnValue('https://example.com'); + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + + window.webContents.emit('did-navigate'); + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + window.webContents.session.webRequest.send('onResponseStarted', { + responseHeaders, + webContents: window.webContents, + }); + + expect(mockWebContentsInsertCSS).toHaveBeenCalledWith(css); + }); + + test.each(['application/json', 'font/woff2', 'image/png'])( + 'will not inject for content-type %s', + (contentType: string) => { + mockGetCSSToInject.mockReturnValue(css); + const window = new BrowserWindow(); + const mockWebContentsInsertCSS: jest.SpyInstance = jest + .spyOn(window.webContents, 'insertCSS') + .mockResolvedValue(''); + jest + .spyOn(window.webContents, 'getURL') + .mockReturnValue('https://example.com'); + + responseHeaders['content-type'] = [contentType]; + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + + expect(window.webContents.emit('did-navigate')).toBe(true); + mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + window.webContents.session.webRequest.send('onResponseStarted', { + responseHeaders, + webContents: window.webContents, + url: `test-${contentType}`, + }); + // insertCSS will still run once for the did-navigate + expect(mockWebContentsInsertCSS).not.toHaveBeenCalled(); + }, + ); + + test.each(['text/html'])( + 'will inject for content-type %s', + (contentType: string) => { + mockGetCSSToInject.mockReturnValue(css); + const window = new BrowserWindow(); + const mockWebContentsInsertCSS: jest.SpyInstance = jest + .spyOn(window.webContents, 'insertCSS') + .mockResolvedValue(''); + jest + .spyOn(window.webContents, 'getURL') + .mockReturnValue('https://example.com'); + + responseHeaders['content-type'] = [contentType]; + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + + window.webContents.emit('did-navigate'); + mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + window.webContents.session.webRequest.send('onResponseStarted', { + responseHeaders, + webContents: window.webContents, + url: `test-${contentType}`, + }); + + expect(mockWebContentsInsertCSS).toHaveBeenCalledTimes(1); + }, + ); + + test.each(['image', 'script', 'stylesheet', 'xhr'])( + 'will not inject for resource type %s', + (resourceType: string) => { + mockGetCSSToInject.mockReturnValue(css); + const window = new BrowserWindow(); + const mockWebContentsInsertCSS: jest.SpyInstance = jest + .spyOn(window.webContents, 'insertCSS') + .mockResolvedValue(''); + jest + .spyOn(window.webContents, 'getURL') + .mockReturnValue('https://example.com'); + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + + window.webContents.emit('did-navigate'); + mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + window.webContents.session.webRequest.send('onResponseStarted', { + responseHeaders, + webContents: window.webContents, + resourceType, + url: `test-${resourceType}`, + }); + // insertCSS will still run once for the did-navigate + expect(mockWebContentsInsertCSS).not.toHaveBeenCalled(); + }, + ); + + test.each(['html', 'other'])( + 'will inject for resource type %s', + (resourceType: string) => { + mockGetCSSToInject.mockReturnValue(css); + const window = new BrowserWindow(); + const mockWebContentsInsertCSS: jest.SpyInstance = jest + .spyOn(window.webContents, 'insertCSS') + .mockResolvedValue(''); + jest + .spyOn(window.webContents, 'getURL') + .mockReturnValue('https://example.com'); + + injectCSS(window); + + expect(mockGetCSSToInject).toHaveBeenCalled(); + + window.webContents.emit('did-navigate'); + mockWebContentsInsertCSS.mockReset().mockResolvedValue(undefined); + // @ts-expect-error this function doesn't exist in the actual electron version, but will in our mock + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + window.webContents.session.webRequest.send('onResponseStarted', { + responseHeaders, + webContents: window.webContents, + resourceType, + url: `test-${resourceType}`, + }); + expect(mockWebContentsInsertCSS).toHaveBeenCalledTimes(1); + }, + ); +}); diff --git a/app/src/helpers/windowHelpers.ts b/app/src/helpers/windowHelpers.ts new file mode 100644 index 0000000..7a11c84 --- /dev/null +++ b/app/src/helpers/windowHelpers.ts @@ -0,0 +1,365 @@ +import path from 'path'; + +import { + dialog, + BrowserWindow, + BrowserWindowConstructorOptions, + Event, + MessageBoxReturnValue, + WebPreferences, + OnResponseStartedListenerDetails, +} from 'electron'; + +import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers'; +import * as log from './loggingHelper'; +import { TrayValue, WindowOptions } from '../../../shared/src/options/model'; +import { randomUUID } from 'crypto'; + +const ZOOM_INTERVAL = 0.1; + +export function adjustWindowZoom(adjustment: number): void { + withFocusedWindow((focusedWindow: BrowserWindow) => { + focusedWindow.webContents.zoomFactor = + focusedWindow.webContents.zoomFactor + adjustment; + }); +} + +export function showNavigationBlockedMessage( + message: string, +): Promise { + return new Promise((resolve, reject) => { + withFocusedWindow((focusedWindow) => { + dialog + .showMessageBox(focusedWindow, { + message, + type: 'error', + title: 'Navigation blocked', + }) + .then((result) => resolve(result)) + .catch((err) => { + reject(err); + }); + }); + }); +} + +export async function clearAppData(window: BrowserWindow): Promise { + const response = await dialog.showMessageBox(window, { + type: 'warning', + buttons: ['Yes', 'Cancel'], + defaultId: 1, + title: 'Clear cache confirmation', + message: + 'This will clear all data (cookies, local storage etc) from this app. Are you sure you wish to proceed?', + }); + + if (response.response !== 0) { + return; + } + await clearCache(window); +} + +export async function clearCache(window: BrowserWindow): Promise { + const { session } = window.webContents; + await session.clearStorageData(); + await session.clearCache(); +} + +export function createAboutBlankWindow( + options: WindowOptions, + setupWindow: (options: WindowOptions, window: BrowserWindow) => void, + parent?: BrowserWindow, +): BrowserWindow { + const window = createNewWindow( + { ...options, show: false }, + setupWindow, + 'about:blank', + nativeTabsSupported() ? undefined : parent, + ); + window.webContents.once('did-stop-loading', () => { + if (window.webContents.getURL() === 'about:blank') { + window.close(); + } else { + window.show(); + } + }); + return window; +} + +export function createNewTab( + options: WindowOptions, + setupWindow: (options: WindowOptions, window: BrowserWindow) => void, + url: string, + foreground: boolean, +): BrowserWindow | undefined { + const focusedWindow = BrowserWindow.getFocusedWindow(); + log.debug('createNewTab', { + url, + foreground, + focusedWindow, + }); + return withFocusedWindow((focusedWindow) => { + const newTab = createNewWindow(options, setupWindow, url); + log.debug('createNewTab.withFocusedWindow', { focusedWindow, newTab }); + focusedWindow.addTabbedWindow(newTab); + if (!foreground) { + focusedWindow.focus(); + } + return newTab; + }); +} + +export function createNewWindow( + options: WindowOptions, + setupWindow: (options: WindowOptions, window: BrowserWindow) => void, + url: string, + parent?: BrowserWindow, +): BrowserWindow { + log.debug('createNewWindow', { + url, + parent, + }); + const window = new BrowserWindow({ + parent: nativeTabsSupported() ? undefined : parent, + ...getDefaultWindowOptions(options), + }); + setupWindow(options, window); + window.loadURL(url).catch((err) => log.error('window.loadURL ERROR', err)); + return window; +} + +export function getCurrentURL(): string { + return withFocusedWindow((focusedWindow) => + focusedWindow.webContents.getURL(), + ) as unknown as string; +} + +export function getDefaultWindowOptions( + options: WindowOptions, +): BrowserWindowConstructorOptions { + const browserwindowOptions: BrowserWindowConstructorOptions = { + ...options.browserwindowOptions, + }; + // We're going to remove this and merge it separately into DEFAULT_WINDOW_OPTIONS.webPreferences + // Otherwise the browserwindowOptions.webPreferences object will completely replace the + // webPreferences specified in the DEFAULT_WINDOW_OPTIONS with itself + delete browserwindowOptions.webPreferences; + + const webPreferences: WebPreferences = { + ...(options.browserwindowOptions?.webPreferences ?? {}), + }; + + const defaultOptions: BrowserWindowConstructorOptions = { + autoHideMenuBar: options.autoHideMenuBar, + fullscreenable: true, + tabbingIdentifier: nativeTabsSupported() + ? options.tabbingIdentifier ?? randomUUID() + : undefined, + title: options.name, + webPreferences: { + javascript: true, + nodeIntegration: false, // `true` is *insecure*, and cause trouble with messenger.com + preload: path.join(__dirname, 'preload.js'), + plugins: true, + sandbox: false, // https://www.electronjs.org/blog/electron-20-0#default-changed-renderers-without-nodeintegration-true-are-sandboxed-by-default + webSecurity: !options.insecure, + zoomFactor: options.zoom, + // `contextIsolation` was switched to true in Electron 12, which: + // 1. Breaks access to global variables in `--inject`-ed scripts: + // https://github.com/nativefier/nativefier/issues/1269 + // 2. Might break notifications under Windows, although this was refuted: + // https://github.com/nativefier/nativefier/issues/1292 + // So, it was flipped to false in https://github.com/nativefier/nativefier/pull/1308 + // + // If attempting to set it back to `true` (for security), + // do test exhaustively these two areas, and more. + contextIsolation: false, + ...webPreferences, + }, + ...browserwindowOptions, + }; + + log.debug('getDefaultWindowOptions', { + options, + webPreferences, + defaultOptions, + }); + + return defaultOptions; +} + +export function goBack(): void { + log.debug('onGoBack'); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.goBack(); + }); +} + +export function goForward(): void { + log.debug('onGoForward'); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.goForward(); + }); +} + +export function goToURL(url: string): Promise | undefined { + return withFocusedWindow((focusedWindow) => focusedWindow.loadURL(url)); +} + +export function hideWindow( + window: BrowserWindow, + event: Event, + fastQuit: boolean, + tray: TrayValue, +): void { + if (isOSX() && !fastQuit) { + // this is called when exiting from clicking the cross button on the window + event.preventDefault(); + window.hide(); + } else if (!fastQuit && tray !== 'false') { + event.preventDefault(); + window.hide(); + } + // will close the window on other platforms +} + +export function injectCSS(browserWindow: BrowserWindow): void { + const cssToInject = getCSSToInject(); + + if (!cssToInject) { + return; + } + + browserWindow.webContents.on('did-navigate', () => { + log.debug( + 'browserWindow.webContents.did-navigate', + browserWindow.webContents.getURL(), + ); + + browserWindow.webContents + .insertCSS(cssToInject) + .catch((err: unknown) => + log.error('browserWindow.webContents.insertCSS', err), + ); + + // We must inject css early enough; so onResponseStarted is a good place. + browserWindow.webContents.session.webRequest.onResponseStarted( + { urls: [] }, // Pass an empty filter list; null will not match _any_ urls + (details: OnResponseStartedListenerDetails): void => { + log.debug('onResponseStarted', { + resourceType: details.resourceType, + url: details.url, + }); + injectCSSIntoResponse(details, cssToInject).catch((err: unknown) => { + log.error('injectCSSIntoResponse ERROR', err); + }); + }, + ); + }); +} + +function injectCSSIntoResponse( + details: OnResponseStartedListenerDetails, + cssToInject: string, +): Promise { + const contentType = + details.responseHeaders && 'content-type' in details.responseHeaders + ? details.responseHeaders['content-type'][0] + : undefined; + + log.debug('injectCSSIntoResponse', { details, cssToInject, contentType }); + + // We go with a denylist rather than a whitelist (e.g. only text/html) + // to avoid "whoops I didn't think this should have been CSS-injected" cases + const nonInjectableContentTypes = [ + /application\/.*/, + /font\/.*/, + /image\/.*/, + ]; + const nonInjectableResourceTypes = ['image', 'script', 'stylesheet', 'xhr']; + + if ( + (contentType && + nonInjectableContentTypes.filter((x) => { + const matches = x.exec(contentType); + return matches && matches?.length > 0; + })?.length > 0) || + nonInjectableResourceTypes.includes(details.resourceType) || + !details.webContents + ) { + log.debug( + `Skipping CSS injection for:\n${details.url}\nwith resourceType ${ + details.resourceType + } and content-type ${contentType as string}`, + ); + return Promise.resolve(undefined); + } + + log.debug( + `Injecting CSS for:\n${details.url}\nwith resourceType ${ + details.resourceType + } and content-type ${contentType as string}`, + ); + return details.webContents.insertCSS(cssToInject); +} + +export function sendParamsOnDidFinishLoad( + options: WindowOptions, + window: BrowserWindow, +): void { + window.webContents.on('did-finish-load', () => { + log.debug( + 'sendParamsOnDidFinishLoad.window.webContents.did-finish-load', + window.webContents.getURL(), + ); + // In children windows too: Restore pinch-to-zoom, disabled by default in recent Electron. + // See https://github.com/nativefier/nativefier/issues/379#issuecomment-598612128 + // and https://github.com/electron/electron/pull/12679 + window.webContents + .setVisualZoomLevelLimits(1, 3) + .catch((err) => log.error('webContents.setVisualZoomLevelLimits', err)); + + window.webContents.send('params', JSON.stringify(options)); + }); +} + +export function setProxyRules( + window: BrowserWindow, + proxyRules?: string, +): void { + window.webContents.session + .setProxy({ + proxyRules, + pacScript: '', + proxyBypassRules: '', + }) + .catch((err) => log.error('session.setProxy ERROR', err)); +} + +export function withFocusedWindow( + block: (window: BrowserWindow) => T, +): T | undefined { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + return block(focusedWindow); + } + + return undefined; +} + +export function zoomOut(): void { + log.debug('zoomOut'); + adjustWindowZoom(-ZOOM_INTERVAL); +} + +export function zoomReset(options: { zoom?: number }): void { + log.debug('zoomReset'); + withFocusedWindow((focusedWindow) => { + focusedWindow.webContents.zoomFactor = options.zoom ?? 1.0; + }); +} + +export function zoomIn(): void { + log.debug('zoomIn'); + adjustWindowZoom(ZOOM_INTERVAL); +} diff --git a/app/src/main.ts b/app/src/main.ts new file mode 100644 index 0000000..3bd4c10 --- /dev/null +++ b/app/src/main.ts @@ -0,0 +1,458 @@ +import 'source-map-support/register'; + +import fs from 'fs'; +import * as path from 'path'; + +import electron, { + app, + dialog, + globalShortcut, + systemPreferences, + BrowserWindow, + Event, +} from 'electron'; +import electronDownload from 'electron-dl'; + +import { createLoginWindow } from './components/loginWindow'; +import { + createMainWindow, + saveAppArgs, + APP_ARGS_FILE_PATH, +} from './components/mainWindow'; +import { createTrayIcon } from './components/trayIcon'; +import { + isOSX, + isWayland, + isWindows, + removeUserAgentSpecifics, +} from './helpers/helpers'; +import { inferFlashPath } from './helpers/inferFlash'; +import * as log from './helpers/loggingHelper'; +import { + IS_PLAYWRIGHT, + PLAYWRIGHT_CONFIG, + safeGetEnv, +} from './helpers/playwrightHelpers'; +import { OutputOptions } from '../../shared/src/options/model'; + +// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744 +if (require('electron-squirrel-startup')) { + app.exit(); +} + +if (process.argv.indexOf('--verbose') > -1 || safeGetEnv('VERBOSE') === '1') { + log.setLevel('DEBUG'); + process.traceDeprecation = true; + process.traceProcessWarnings = true; + process.argv.slice(1); +} + +let mainWindow: BrowserWindow; + +const appArgs = + IS_PLAYWRIGHT && PLAYWRIGHT_CONFIG + ? (JSON.parse(PLAYWRIGHT_CONFIG) as OutputOptions) + : (JSON.parse( + fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'), + ) as OutputOptions); + +log.debug('appArgs', appArgs); +// Do this relatively early so that we can start storing appData with the app +if (appArgs.portable) { + log.debug( + 'App was built as portable; setting appData and userData to the app folder: ', + path.resolve(path.join(__dirname, '..', 'appData')), + ); + app.setPath('appData', path.join(__dirname, '..', 'appData')); + app.setPath('userData', path.join(__dirname, '..', 'appData')); +} + +if (!appArgs.userAgentHonest) { + if (appArgs.userAgent) { + app.userAgentFallback = appArgs.userAgent; + } else { + app.userAgentFallback = removeUserAgentSpecifics( + app.userAgentFallback, + app.getName(), + app.getVersion(), + ); + } +} + +// this step is required to allow app names to be displayed correctly in notifications on windows +// https://www.electronjs.org/docs/latest/api/app#appsetappusermodelidid-windows +// https://www.electronjs.org/docs/latest/tutorial/notifications#windows +if (isWindows()) { + app.setAppUserModelId(app.getName()); +} + +const urlArgv = process.argv.filter((a) => a.startsWith('http')); + +// Take in a URL on the command line as an override +if (urlArgv.length > 0) { + const maybeUrl = urlArgv[0]; + try { + new URL(maybeUrl); + appArgs.targetUrl = maybeUrl; + log.info('Loading override URL passed as argument:', maybeUrl); + } catch (err: unknown) { + log.error( + 'Not loading override URL passed as argument, because failed to parse:', + maybeUrl, + err, + ); + } +} + +// Nativefier is a browser, and an old browser is an insecure / badly-performant one. +// Given our builder/app design, we currently don't have an easy way to offer +// upgrades from the app themselves (like browsers do). +// As a workaround, we ask for a manual upgrade & re-build if the build is old. +// The period in days is chosen to be not too small to be exceedingly annoying, +// but not too large to be exceedingly insecure. +const OLD_BUILD_WARNING_THRESHOLD_DAYS = 90; +const OLD_BUILD_WARNING_THRESHOLD_MS = + OLD_BUILD_WARNING_THRESHOLD_DAYS * 24 * 60 * 60 * 1000; + +const fileDownloadOptions = { ...appArgs.fileDownloadOptions }; +electronDownload(fileDownloadOptions); + +if (appArgs.processEnvs) { + let processEnvs: Record = + appArgs.processEnvs as unknown as Record; + // This is compatibility if just a string was passed. + if (typeof appArgs.processEnvs === 'string') { + try { + processEnvs = JSON.parse(appArgs.processEnvs) as Record; + } catch { + // This wasn't JSON. Fall back to the old code + processEnvs = {}; + process.env.processEnvs = appArgs.processEnvs; + } + } + Object.keys(processEnvs) + .filter((key) => key !== undefined) + .forEach((key) => { + process.env[key] = processEnvs[key]; + }); +} + +if (typeof appArgs.flashPluginDir === 'string') { + app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir); +} else if (appArgs.flashPluginDir) { + const flashPath = inferFlashPath(); + app.commandLine.appendSwitch('ppapi-flash-path', flashPath); +} + +if (appArgs.ignoreCertificate) { + app.commandLine.appendSwitch('ignore-certificate-errors'); +} + +if (appArgs.disableGpu) { + app.disableHardwareAcceleration(); +} + +if (appArgs.ignoreGpuBlacklist) { + app.commandLine.appendSwitch('ignore-gpu-blacklist'); +} + +if (appArgs.enableEs3Apis) { + app.commandLine.appendSwitch('enable-es3-apis'); +} + +if (appArgs.diskCacheSize) { + app.commandLine.appendSwitch( + 'disk-cache-size', + appArgs.diskCacheSize.toString(), + ); +} + +if (appArgs.basicAuthUsername) { + app.commandLine.appendSwitch( + 'basic-auth-username', + appArgs.basicAuthUsername, + ); +} + +if (appArgs.basicAuthPassword) { + app.commandLine.appendSwitch( + 'basic-auth-password', + appArgs.basicAuthPassword, + ); +} + +if (isWayland()) { + app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer'); +} + +if (appArgs.lang) { + const langParts = appArgs.lang.split(','); + // Convert locales to languages, because for some reason locales don't work. Stupid Chromium + const langPartsParsed = Array.from( + // Convert to set to dedupe in case something like "en-GB,en-US" was passed + new Set(langParts.map((l) => l.split('-')[0])), + ); + const langFlag = langPartsParsed.join(','); + log.debug('Setting --lang flag to', langFlag); + app.commandLine.appendSwitch('--lang', langFlag); +} + +let currentBadgeCount = 0; +const setDockBadge = isOSX() + ? (count?: number | string, bounce = false): void => { + if (count !== undefined) { + app.dock.setBadge(count.toString()); + if (bounce && typeof count === 'number' && count > currentBadgeCount) + app.dock.bounce(); + currentBadgeCount = typeof count === 'number' ? count : 0; + } + } + : (): void => undefined; + +app.on('window-all-closed', () => { + log.debug('app.window-all-closed'); + if (!isOSX() || appArgs.fastQuit || IS_PLAYWRIGHT) { + app.quit(); + } +}); + +app.on('before-quit', () => { + log.debug('app.before-quit'); + // not fired when the close button on the window is clicked + if (isOSX()) { + // need to force a quit as a workaround here to simulate the osx app hiding behaviour + // Somehow sokution at https://github.com/atom/electron/issues/444#issuecomment-76492576 does not work, + // e.prevent default appears to persist + + // might cause issues in the future as before-quit and will-quit events are not called + app.exit(0); + } +}); + +app.on('will-quit', (event) => { + log.debug('app.will-quit', event); +}); + +app.on('quit', (event, exitCode) => { + log.debug('app.quit', { event, exitCode }); +}); + +app.on('will-finish-launching', () => { + log.debug('app.will-finish-launching'); +}); + +app.on('open-url', (event, url) => { + log.debug('app.open-url', { event, url }); + + event.preventDefault(); + if (mainWindow) { + mainWindow.webContents.send('open-url', url); + } +}); + +if (appArgs.widevine) { + // @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime + app.on('widevine-ready', (version: string, lastVersion: string) => { + log.debug('app.widevine-ready', { version, lastVersion }); + onReady().catch((err) => log.error('onReady ERROR', err)); + }); + + app.on( + // @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime + 'widevine-update-pending', + (currentVersion: string, pendingVersion: string) => { + log.debug('app.widevine-update-pending', { + currentVersion, + pendingVersion, + }); + }, + ); + + // @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime + app.on('widevine-error', (error: Error) => { + log.error('app.widevine-error', error); + }); +} else { + app.on('ready', () => { + log.debug('ready'); + onReady().catch((err) => log.error('onReady ERROR', err)); + }); +} + +app.on('activate', (event: electron.Event, hasVisibleWindows: boolean) => { + log.debug('app.activate', { event, hasVisibleWindows }); + if (isOSX() && !IS_PLAYWRIGHT) { + // this is called when the dock is clicked + if (!hasVisibleWindows) { + mainWindow.show(); + } + } +}); + +// quit if singleInstance mode and there's already another instance running +const shouldQuit = appArgs.singleInstance && !app.requestSingleInstanceLock(); +if (shouldQuit) { + app.quit(); +} else { + app.on('second-instance', () => { + log.debug('app.second-instance'); + if (mainWindow) { + if (!mainWindow.isVisible()) { + // try + mainWindow.show(); + } + if (mainWindow.isMinimized()) { + // minimized + mainWindow.restore(); + } + mainWindow.focus(); + } + }); +} + +app.on('new-window-for-tab', (event: Event) => { + log.debug('app.new-window-for-tab', { event }); + if (mainWindow) { + mainWindow.emit('new-window-for-tab', event); + } +}); + +app.on( + 'login', + ( + event, + webContents, + request, + authInfo, + callback: (username?: string, password?: string) => void, + ) => { + log.debug('app.login', { event, request }); + // for http authentication + event.preventDefault(); + + if (appArgs.basicAuthUsername && appArgs.basicAuthPassword) { + callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword); + } else { + createLoginWindow( + callback, + // mainWindow + ).catch((err) => log.error('createLoginWindow ERROR', err)); + } + }, +); + +async function onReady(): Promise { + // Warning: `mainWindow` below is the *global* unique `mainWindow`, created at init time + mainWindow = await createMainWindow(appArgs, setDockBadge); + + createTrayIcon(appArgs, mainWindow); + + // Register global shortcuts + if (appArgs.globalShortcuts) { + appArgs.globalShortcuts.forEach((shortcut) => { + globalShortcut.register(shortcut.key, () => { + shortcut.inputEvents.forEach((inputEvent) => { + // @ts-expect-error without including electron in our models, these will never match + mainWindow.webContents.sendInputEvent(inputEvent); + }); + }); + }); + + if (isOSX() && appArgs.accessibilityPrompt) { + const mediaKeys = [ + 'MediaPlayPause', + 'MediaNextTrack', + 'MediaPreviousTrack', + 'MediaStop', + ]; + const globalShortcutsKeys = appArgs.globalShortcuts.map((g) => g.key); + const mediaKeyWasSet = globalShortcutsKeys.find((g) => + mediaKeys.includes(g), + ); + if ( + mediaKeyWasSet && + !systemPreferences.isTrustedAccessibilityClient(false) + ) { + // Since we're trying to set global keyboard shortcuts for media keys, we need to prompt + // the user for permission on Mac. + // For reference: + // https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback + const accessibilityPromptResult = dialog.showMessageBoxSync( + mainWindow, + { + type: 'question', + message: 'Accessibility Permissions Needed', + buttons: ['Yes', 'No', 'No and never ask again'], + defaultId: 0, + detail: + `${appArgs.name} would like to use one or more of your keyboard's media keys (start, stop, next track, or previous track) to control it.\n\n` + + `Would you like Mac OS to ask for your permission to do so?\n\n` + + `If so, you will need to restart ${appArgs.name} after granting permissions for these keyboard shortcuts to begin working.`, + }, + ); + switch (accessibilityPromptResult) { + // User clicked Yes, prompt for accessibility + case 0: + systemPreferences.isTrustedAccessibilityClient(true); + break; + // User cliecked Never Ask Me Again, save that info + case 2: + appArgs.accessibilityPrompt = false; + saveAppArgs(appArgs); + break; + // User clicked No + default: + break; + } + } + } + } + if ( + !appArgs.disableOldBuildWarning && + new Date().getTime() - appArgs.buildDate > OLD_BUILD_WARNING_THRESHOLD_MS + ) { + const oldBuildWarningText = + appArgs.oldBuildWarningText || + 'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is insecure to keep using an old version of it. Please upgrade Nativefier and rebuild this app.'; + dialog + .showMessageBox(mainWindow, { + type: 'warning', + message: 'Old build detected', + detail: oldBuildWarningText, + }) + .catch((err) => log.error('dialog.showMessageBox ERROR', err)); + } + + if (appArgs.targetUrl) { + await mainWindow.loadURL(appArgs.targetUrl); + } +} + +app.on( + 'accessibility-support-changed', + (event: Event, accessibilitySupportEnabled: boolean) => { + log.debug('app.accessibility-support-changed', { + event, + accessibilitySupportEnabled, + }); + }, +); + +app.on( + 'activity-was-continued', + (event: Event, type: string, userInfo: unknown) => { + log.debug('app.activity-was-continued', { event, type, userInfo }); + }, +); + +app.on('browser-window-blur', () => { + log.debug('app.browser-window-blur'); +}); + +app.on('browser-window-created', () => { + log.debug('app.browser-window-created'); +}); + +app.on('browser-window-focus', () => { + log.debug('app.browser-window-focus'); +}); diff --git a/app/src/mocks/electron.ts b/app/src/mocks/electron.ts new file mode 100644 index 0000000..7f6aa17 --- /dev/null +++ b/app/src/mocks/electron.ts @@ -0,0 +1,160 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { EventEmitter } from 'events'; + +/* + These mocks are PURPOSEFULLY minimal. A few reasons as to why: + 1. I'm l̶a̶z̶y̶ a busy person :) + 2. The less we have in here, the less we'll need to fix if an electron API changes + 3. Only mocking what we need as we need it helps reveal areas under test where electron + is being accessed in previously unaccounted for ways + 4. These mocks will get fleshed out as more unit tests are added, so if you need + something here as you are adding unit tests, then feel free to add exactly what you + need (and no more than that please). + + As well, please resist the urge to turn this into a reimplimentation of electron. + When adding functions/classes, keep your implementation to only the minimal amount of code + it takes for TypeScript to recognize what you are doing. For anything more complex (including + implementation code and return values) please do that within your tests via jest with + mockImplementation or mockReturnValue. +*/ + +class MockBrowserWindow extends EventEmitter { + webContents: MockWebContents; + + constructor(options?: unknown) { + // @ts-expect-error options is really EventEmitterOptions, but events.d.ts doesn't expose it... + super(options); + this.webContents = new MockWebContents(); + } + + addTabbedWindow(tab: MockBrowserWindow): void { + return; + } + + focus(): void { + return; + } + + static fromWebContents(webContents: MockWebContents): MockBrowserWindow { + return new MockBrowserWindow(); + } + + static getFocusedWindow(window: MockBrowserWindow): MockBrowserWindow { + return window ?? new MockBrowserWindow(); + } + + isSimpleFullScreen(): boolean { + throw new Error('Not implemented'); + } + + isFullScreen(): boolean { + throw new Error('Not implemented'); + } + + isFullScreenable(): boolean { + throw new Error('Not implemented'); + } + + loadURL(url: string, options?: unknown): Promise { + return Promise.resolve(undefined); + } + + setFullScreen(flag: boolean): void { + return; + } + + setSimpleFullScreen(flag: boolean): void { + return; + } +} + +class MockDialog { + static showMessageBox( + browserWindow: MockBrowserWindow, + options: unknown, + ): Promise { + throw new Error('Not implemented'); + } + + static showMessageBoxSync( + browserWindow: MockBrowserWindow, + options: unknown, + ): number { + throw new Error('Not implemented'); + } +} + +class MockSession extends EventEmitter { + webRequest: MockWebRequest; + + constructor() { + super(); + this.webRequest = new MockWebRequest(); + } + + clearCache(): Promise { + return Promise.resolve(); + } + + clearStorageData(): Promise { + return Promise.resolve(); + } +} + +class MockWebContents extends EventEmitter { + session: MockSession; + + constructor() { + super(); + this.session = new MockSession(); + } + + getURL(): string { + throw new Error('Not implemented'); + } + + insertCSS(css: string, options?: unknown): Promise { + throw new Error('Not implemented'); + } +} + +class MockWebRequest { + emitter: InternalEmitter; + + constructor() { + this.emitter = new InternalEmitter(); + } + + onResponseStarted( + filter: unknown, + listener: ((details: unknown) => void) | null, + ): void { + if (listener) { + this.emitter.addListener('onResponseStarted', (details: unknown) => + listener(details), + ); + } + } + + send(event: string, ...args: unknown[]): void { + this.emitter.emit(event, ...args); + } +} + +class InternalEmitter extends EventEmitter {} + +const mockShell = { + openExternal(url: string, options?: unknown): Promise { + return new Promise((resolve) => resolve()); + }, +}; + +export { + MockDialog as dialog, + MockBrowserWindow as BrowserWindow, + MockSession as Session, + MockWebContents as WebContents, + MockWebRequest as WebRequest, + mockShell as shell, +}; diff --git a/app/src/preload.ts b/app/src/preload.ts new file mode 100644 index 0000000..1044b44 --- /dev/null +++ b/app/src/preload.ts @@ -0,0 +1,352 @@ +/** + * Preload file that will be executed in the renderer process. + * Note: This needs to be attached **prior to imports**, as imports + * would delay the attachment till after the event has been raised. + */ +document.addEventListener('DOMContentLoaded', () => { + injectScripts(); // eslint-disable-line @typescript-eslint/no-use-before-define +}); + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { ipcRenderer } from 'electron'; +import { OutputOptions } from '../../shared/src/options/model'; + +// Do *NOT* add 3rd-party imports here in preload (except for webpack `externals` like electron). +// They will work during development, but break in the prod build :-/ . +// Electron doc isn't explicit about that, so maybe *we*'re doing something wrong. +// At any rate, that's what we have now. If you want an import here, go ahead, but +// verify that apps built with a non-devbuild nativefier (installed from tarball) work. +// Recipe to monkey around this, assuming you git-cloned nativefier in /opt/nativefier/ : +// cd /opt/nativefier/ && rm -f nativefier-43.1.0.tgz && npm run build && npm pack && mkdir -p ~/n4310/ && cd ~/n4310/ \ +// && rm -rf ./* && npm i /opt/nativefier/nativefier-43.1.0.tgz && ./node_modules/.bin/nativefier 'google.com' +// See https://github.com/nativefier/nativefier/issues/1175 +// and https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions / preload + +const log = console; // since we can't have `loglevel` here in preload + +export const INJECT_DIR = path.join(__dirname, '..', 'inject'); + +/** + * Patches window.Notification to: + * - set a callback on a new Notification + * - set a callback for clicks on notifications + * @param createCallback + * @param clickCallback + */ +function setNotificationCallback( + createCallback: { + (title: string, opt: NotificationOptions): void; + (...args: unknown[]): void; + }, + clickCallback: { (): void; (this: Notification, ev: Event): unknown }, +): void { + const OldNotify = window.Notification; + const newNotify = function ( + title: string, + opt: NotificationOptions, + ): Notification { + createCallback(title, opt); + const instance = new OldNotify(title, opt); + instance.addEventListener('click', clickCallback); + return instance; + }; + newNotify.requestPermission = OldNotify.requestPermission.bind(OldNotify); + Object.defineProperty(newNotify, 'permission', { + get: () => OldNotify.permission, + }); + + // @ts-expect-error TypeScript says its not compatible, but it works? + window.Notification = newNotify; +} + +async function getDisplayMedia( + sourceId: number | string, +): Promise { + type OriginalVideoPropertyType = boolean | MediaTrackConstraints | undefined; + if (!window?.navigator?.mediaDevices) { + throw Error('window.navigator.mediaDevices is not present'); + } + // Electron supports an outdated specification for mediaDevices, + // see https://www.electronjs.org/docs/latest/api/desktop-capturer/ + const stream = await window.navigator.mediaDevices.getUserMedia({ + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: sourceId, + }, + } as unknown as OriginalVideoPropertyType, + }); + + return stream; +} + +function setupScreenSharePickerStyles(id: string): void { + const screenShareStyles = document.createElement('style'); + screenShareStyles.id = id; + screenShareStyles.innerHTML = ` + .desktop-capturer-selection { + --overlay-color: hsla(0, 0%, 11.8%, 0.75); + --highlight-color: highlight; + --text-content-color: #fff; + --selection-button-color: hsl(180, 1.3%, 14.7%); + } + .desktop-capturer-selection { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: var(--overlay-color); + color: var(--text-content-color); + z-index: 10000000; + display: flex; + align-items: center; + justify-content: center; + } + .desktop-capturer-selection__close { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + padding: 1rem; + color: inherit; + position: absolute; + left: 1rem; + top: 1rem; + cursor: pointer; + } + .desktop-capturer-selection__scroller { + width: 100%; + max-height: 100vh; + overflow-y: auto; + } + .desktop-capturer-selection__list { + max-width: calc(100% - 100px); + margin: 50px; + padding: 0; + display: flex; + flex-wrap: wrap; + list-style: none; + overflow: hidden; + justify-content: center; + } + .desktop-capturer-selection__item { + display: flex; + margin: 4px; + } + .desktop-capturer-selection__btn { + display: flex; + flex-direction: column; + align-items: stretch; + width: 145px; + margin: 0; + border: 0; + border-radius: 3px; + padding: 4px; + background: var(--selection-button-color); + text-align: left; + transition: background-color .15s, box-shadow .15s; + } + .desktop-capturer-selection__btn:hover, + .desktop-capturer-selection__btn:focus { + background: var(--highlight-color); + } + .desktop-capturer-selection__thumbnail { + width: 100%; + height: 81px; + object-fit: cover; + } + .desktop-capturer-selection__name { + margin: 6px 0 6px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + @media (prefers-color-scheme: light) { + .desktop-capturer-selection { + --overlay-color: hsla(0, 0%, 90.2%, 0.75); + --text-content-color: hsl(0, 0%, 12.9%); + --selection-button-color: hsl(180, 1.3%, 85.3%); + } + }`; + document.head.appendChild(screenShareStyles); +} + +function setupScreenSharePickerElement( + id: string, + sources: Electron.DesktopCapturerSource[], +): void { + const selectionElem = document.createElement('div'); + selectionElem.classList.add('desktop-capturer-selection'); + selectionElem.id = id; + selectionElem.innerHTML = ` + +
+
    + ${sources + .map( + ({ id, name, thumbnail }) => ` +
  • + +
  • + `, + ) + .join('')} +
+
+ `; + document.body.appendChild(selectionElem); +} + +function setupScreenSharePicker( + resolve: (value: MediaStream | PromiseLike) => void, + reject: (reason?: unknown) => void, + sources: Electron.DesktopCapturerSource[], +): void { + const baseElementsId = 'native-screen-share-picker'; + const pickerStylesElementId = baseElementsId + '-styles'; + + setupScreenSharePickerElement(baseElementsId, sources); + setupScreenSharePickerStyles(pickerStylesElementId); + + const clearElements = (): void => { + document.getElementById(pickerStylesElementId)?.remove(); + document.getElementById(baseElementsId)?.remove(); + }; + + document + .getElementById(`${baseElementsId}-close`) + ?.addEventListener('click', () => { + clearElements(); + reject('Screen share was cancelled by the user.'); + }); + + document + .querySelectorAll('.desktop-capturer-selection__btn') + .forEach((button) => { + button.addEventListener('click', () => { + const id = button.getAttribute('data-id'); + if (!id) { + log.error("Couldn't find `data-id` of element"); + clearElements(); + return; + } + const source = sources.find((source) => source.id === id); + if (!source) { + log.error(`Source with id "${id}" does not exist`); + clearElements(); + return; + } + + getDisplayMedia(source.id) + .then((stream) => { + resolve(stream); + }) + .catch((err) => { + log.error('Error selecting desktop capture source:', err); + reject(err); + }) + .finally(() => { + clearElements(); + }); + }); + }); +} + +function setDisplayMediaPromise(): void { + // Since no implementation for `getDisplayMedia` exists in Electron we write our own. + if (!window?.navigator?.mediaDevices) { + return; + } + window.navigator.mediaDevices.getDisplayMedia = (): Promise => { + return new Promise((resolve, reject) => { + const sources = ipcRenderer.invoke( + 'desktop-capturer-get-sources', + ) as Promise; + sources + .then(async (sources) => { + if (isWayland()) { + // No documentation is provided wether the first element is always PipeWire-picked or not + // i.e. maybe it's not deterministic, we are only taking a guess here. + const stream = await getDisplayMedia(sources[0].id); + resolve(stream); + } else { + setupScreenSharePicker(resolve, reject, sources); + } + }) + .catch((err) => { + reject(err); + }); + }); + }; +} + +function injectScripts(): void { + const needToInject = fs.existsSync(INJECT_DIR); + if (!needToInject) { + return; + } + // Dynamically require scripts + try { + const jsFiles = fs + .readdirSync(INJECT_DIR, { withFileTypes: true }) + .filter( + (injectFile) => injectFile.isFile() && injectFile.name.endsWith('.js'), + ) + .map((jsFileStat) => path.join('..', 'inject', jsFileStat.name)); + for (const jsFile of jsFiles) { + log.debug('Injecting JS file', jsFile); + require(jsFile); + } + } catch (err: unknown) { + log.error('Error encoutered injecting JS files', err); + } +} + +function notifyNotificationCreate( + title: string, + opt: NotificationOptions, +): void { + ipcRenderer.send('notification', title, opt); +} +function notifyNotificationClick(): void { + ipcRenderer.send('notification-click'); +} + +// @ts-expect-error TypeScript thinks these are incompatible but they aren't +setNotificationCallback(notifyNotificationCreate, notifyNotificationClick); +setDisplayMediaPromise(); + +ipcRenderer.on('params', (event, message: string) => { + log.debug('ipcRenderer.params', { event, message }); + const appArgs: unknown = JSON.parse(message) as OutputOptions; + log.info('nativefier.json', appArgs); +}); + +ipcRenderer.on('debug', (event, message: string) => { + log.debug('ipcRenderer.debug', { event, message }); +}); + +// Copy-pastaed as unable to get imports to work in preload. +// If modifying, update also app/src/helpers/helpers.ts +function isWayland(): boolean { + return ( + isLinux() && + (Boolean(process.env.WAYLAND_DISPLAY) || + process.env.XDG_SESSION_TYPE === 'wayland') + ); +} + +function isLinux(): boolean { + return os.platform() === 'linux'; +} diff --git a/app/src/static/.eslintrc.yml b/app/src/static/.eslintrc.yml new file mode 100644 index 0000000..5c9fc4c --- /dev/null +++ b/app/src/static/.eslintrc.yml @@ -0,0 +1,2 @@ +env: + browser: true diff --git a/app/src/static/login.css b/app/src/static/login.css new file mode 100644 index 0000000..1e867fa --- /dev/null +++ b/app/src/static/login.css @@ -0,0 +1,57 @@ +label, input { + display: block; +} + +* { + font-family: Verdana, sans-serif; +} + +html, body { + height: 100%; + margin: 0; + -webkit-app-region: drag; +} +.login-form { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.login-form > div { + margin-bottom: 30px; +} + +.login-form > div > label { + margin-bottom: 25px; + letter-spacing: 5px; + font-weight: 100; + text-transform: uppercase; + font-size: 18px; + text-align: center; + color: rgba(0, 0, 0, 0.7); +} + +.login-form > div > input { + height: 30px; + width: 225px; + font-size: 16px; + -webkit-app-region: no-drag; +} + +.login-form > div > button { + border: 0; + font-size: 15px; + font-weight: 100; + text-transform: uppercase; + height: 35px; + width: 225px; + background-color: #2196F3; + color: white; + -webkit-app-region: no-drag; +} + +button:hover { + background-color: #0D47A1; +} diff --git a/app/src/static/login.html b/app/src/static/login.html new file mode 100644 index 0000000..6508c54 --- /dev/null +++ b/app/src/static/login.html @@ -0,0 +1,24 @@ + + + + + Login + + + + + + + diff --git a/app/src/static/login.js b/app/src/static/login.js new file mode 100644 index 0000000..65617dc --- /dev/null +++ b/app/src/static/login.js @@ -0,0 +1,10 @@ +const { ipcRenderer } = require('electron'); + +document.getElementById('login-form').addEventListener('submit', (event) => { + event.preventDefault(); + const usernameInput = document.getElementById('username-input'); + const username = usernameInput.nodeValue || usernameInput.value; + const passwordInput = document.getElementById('password-input'); + const password = passwordInput.nodeValue || passwordInput.value; + ipcRenderer.send('login-message', [username, password]); +}); diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..9861102 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "outDir": "./dist", + // Here in app/tsconfig.json, we want to set the `target` and `lib` keys to + // the "best" values for the version of Node **coming with the chosen Electron**. + // Careful: we're *not* talking about Nativefier's (CLI) required Node version, + // we're talking about the version of the Node runtime **bundled with Electron**. + // + // Like in our main tsconfig.json, we want to be as conservative as possible, + // to support (as much as reasonable) users using old versions of Electron. + // Then, at some point, an app dependency (declared in app/package.json) + // will require a more recent Node, then it's okay to bump our app compilerOptions + // to what's supported by the more recent Node. + // + // TS doesn't offer any easy "preset" for this, so the best we have is to + // believe people who know which {syntax, library} parts of current EcmaScript + // are supported for the version of Node coming with the Electron being used, + // and use what they recommend. For the current Node version, I followed + // https://stackoverflow.com/questions/51716406/typescript-tsconfig-settings-for-node-js-10 + // and 'dom' to tell tsc it's okay to use the URL object (which is in Node >= 7) + "target": "es2018", + "lib": [ + "es2018", + "dom" + ] + }, + "include": [ + "./src/**/*" + ], + "references": [ + { + "path": "../shared" + } + ] +} diff --git a/app/webpack.config.js b/app/webpack.config.js new file mode 100644 index 0000000..582ef9b --- /dev/null +++ b/app/webpack.config.js @@ -0,0 +1,36 @@ +const path = require('path'); + +// Q: Why do you use webpack? +// A: https://github.com/nativefier/nativefier/commit/cde5c1e13bdc2739604cab04bac64eae0d719ed1 +module.exports = { + target: 'node', + entry: './src/main.ts', + devtool: 'source-map', // https://webpack.js.org/configuration/devtool/ + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', // https://webpack.js.org/guides/typescript/ + exclude: /node_modules/, + }, + ], + }, + // Don't mock __dirname; https://webpack.js.org/configuration/node/#root + node: { + __dirname: false, + }, + // Prevent bundling of certain imported packages and instead retrieve these + // external deps at runtime. This is what we want for electron, placed in the + // app by electron-packager. https://webpack.js.org/configuration/externals/ + externals: { + electron: 'commonjs electron', + }, + resolve: { + extensions: [ '.ts', '.js' ], + }, + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'lib'), + }, + mode: 'none' +}; \ No newline at end of file diff --git a/base-eslintrc.js b/base-eslintrc.js new file mode 100644 index 0000000..ce0ee2f --- /dev/null +++ b/base-eslintrc.js @@ -0,0 +1,39 @@ +// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'prettier'], + extends: [ + 'eslint:recommended', + 'prettier', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], + rules: { + 'no-console': 'error', + 'prettier/prettier': [ + 'error', + { + endOfLine: 'auto', + }, + ], + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-confusing-non-null-assertion': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-invalid-void-type': 'error', + '@typescript-eslint/prefer-ts-expect-error': 'error', + '@typescript-eslint/type-annotation-spacing': 'error', + '@typescript-eslint/typedef': 'error', + '@typescript-eslint/unified-signatures': 'error', + }, + // https://eslint.org/docs/user-guide/configuring/ignoring-code#ignorepatterns-in-config-files + ignorePatterns: [ + 'node_modules/**', + 'app/node_modules/**', + 'app/lib/**', + 'lib/**', + 'built-tests/**', + 'coverage/**', + ], +}; diff --git a/icon-scripts/convertToIcns b/icon-scripts/convertToIcns new file mode 100755 index 0000000..e313e24 --- /dev/null +++ b/icon-scripts/convertToIcns @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +### USAGE + +# ./convertToIcns +# Example +# ./convertToIcns ~/sample.png ~/Desktop/converted.icns + +# exit the shell script on error immediately +set -e + +# Exec Paths +HAVE_IMAGEMAGICK= +HAVE_ICONUTIL= +HAVE_SIPS= +HAVE_GRAPHICSMAGICK= + +type convert &>/dev/null && HAVE_IMAGEMAGICK=true +type iconutil &>/dev/null && HAVE_ICONUTIL=true +type sips &>/dev/null && HAVE_SIPS=true +type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && HAVE_GRAPHICSMAGICK=true + +[[ -z "$HAVE_ICONUTIL" ]] && { echo >&2 "Cannot find required iconutil executable"; exit 1; } +[[ -z "$HAVE_IMAGEMAGICK" && -z "$HAVE_SIPS" && -z "$HAVE_GRAPHICSMAGICK" ]] && { echo >&2 "Cannot find required image converter, please install sips, imagemagick or graphicsmagick"; exit 1; } + +# Parameters +SOURCE="$1" +DEST="$2" + +# Check source and destination arguments +if [ -z "${SOURCE}" ]; then + echo "No source image specified" + exit 1 +fi + +if [ -z "${DEST}" ]; then + echo "No destination specified" + exit 1 +fi + +TEMP_DIR="$(mktemp -d -t nativefier-icns-XXXXXX)" +ICONSET="${TEMP_DIR}/converted.iconset" + +function cleanUp() { + rm -rf "${TEMP_DIR}" +} + +trap cleanUp EXIT + +"${BASH_SOURCE%/*}/convertToIconset" "${SOURCE}" "${ICONSET}" + +# Create an icns file lefrom the iconset +iconutil -c icns "${ICONSET}" -o "${DEST}" + +trap - EXIT +cleanUp diff --git a/icon-scripts/convertToIco b/icon-scripts/convertToIco new file mode 100755 index 0000000..737a2e1 --- /dev/null +++ b/icon-scripts/convertToIco @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# USAGE + +# ./convertToIco .ico +# Example +# ./convertToPng ~/sample.png ~/converted.ico + +set -e + +CONVERT= + +type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && CONVERT="gm convert" +type convert &>/dev/null && CONVERT="convert" + +[[ -z "$CONVERT" ]] && { echo >&2 "Cannot find required ImageMagick Convert or GraphicsMagick executable"; exit 1; } + +SOURCE=$1 +DEST=$2 + +if [ -z "${SOURCE}" ]; then + echo "No source image specified" + exit 1 +fi + +if [ -z "${DEST}" ]; then + echo "No destination specified" + exit 1 +fi + +NAME=$(basename "${SOURCE}") +EXT="${NAME##*.}" + +if [ "${EXT}" == "ico" ]; then + cp "${SOURCE}" "${DEST}" + exit 0 +fi + +$CONVERT "${SOURCE}" -resize 256x256 "${DEST}" diff --git a/icon-scripts/convertToIconset b/icon-scripts/convertToIconset new file mode 100755 index 0000000..82fb32d --- /dev/null +++ b/icon-scripts/convertToIconset @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +### USAGE + +# ./convertToIconset +# Example +# ./convertToIconset ~/sample.png ~/Desktop/converted.iconset + +# exit the shell script on error immediately +set -e + +make_iconset_imagemagick() { + local file iconset + file="${1}" + iconset="${2}" + + mkdir "$iconset" + + for size in {16,32,64,128,256,512}; do + $CONVERT "${file}" -define png:big-depth=16 -define png:color-type=6 -sample "${size}x${size}" "${iconset}/icon_${size}x${size}.png" + $CONVERT "${file}" -define png:big-depth=16 -define png:color-type=6 -sample "$((size * 2))x$((size * 2))" "${iconset}/icon_${size}x${size}@2x.png" + done +} + +make_iconset_sips() { + local file iconset + file="${1}" + iconset="${2}" + + mkdir "$iconset" + + for size in {16,32,64,128,256,512}; do + sips --setProperty format png --resampleHeightWidth "${size}" "${size}" "${file}" --out "${iconset}/icon_${size}x${size}.png" &> /dev/null + sips --setProperty format png --resampleHeightWidth "$((size * 2))" "$((size * 2))" "${file}" --out "${iconset}/icon_${size}x${size}@2x.png" &> /dev/null + done +} + +# Parameters +SOURCE="$1" +DEST="$2" + +# Check source and destination arguments +if [ -z "${SOURCE}" ]; then + echo >&2 "No source image specified"; exit 1 +fi + +if [ -z "${DEST}" ]; then + echo >&2 "No destination specified"; exit 1 +fi + +HAVE_IMAGEMAGICK= +HAVE_SIPS= +HAVE_GRAPHICSMAGICK= +CONVERT= + +type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && HAVE_GRAPHICSMAGICK=true && CONVERT="gm convert" +type convert &>/dev/null && HAVE_IMAGEMAGICK=true && CONVERT="convert" +type sips &>/dev/null && HAVE_SIPS=true + +if [[ -n "$HAVE_IMAGEMAGICK" || -n "$HAVE_GRAPHICSMAGICK" ]]; then + PNG_PATH="$(mktemp -d -t nativefier-iconset-XXXXXX)/icon.png" + "${BASH_SOURCE%/*}/convertToPng" "${SOURCE}" "${PNG_PATH}" + make_iconset_imagemagick "${PNG_PATH}" "${DEST}" +elif [[ -n "$HAVE_SIPS" ]]; then + make_iconset_sips "${SOURCE}" "${DEST}" +else + echo >&2 "Cannot find convert or sips executables"; exit 1; +fi diff --git a/icon-scripts/convertToPng b/icon-scripts/convertToPng new file mode 100755 index 0000000..cd6ce8c --- /dev/null +++ b/icon-scripts/convertToPng @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# USAGE + +# ./convertToPng .png +# Example +# ./convertToPng ~/sample.ico ~/Desktop/converted.png + +set -e + +HAVE_IMAGEMAGICK= +HAVE_GRAPHICSMAGICK= + +type convert &>/dev/null && type identify &>/dev/null && HAVE_IMAGEMAGICK=true +type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && HAVE_GRAPHICSMAGICK=true + +if [[ -z "$HAVE_IMAGEMAGICK" && -z "$HAVE_GRAPHICSMAGICK" ]]; then + type convert >/dev/null 2>&1 || echo >&2 "Cannot find required ImageMagick 'convert' executable" + type identify >/dev/null 2>&1 || echo >&2 "Cannot find required ImageMagick 'identify' executable" + type gm &>/dev/null && gm version | grep GraphicsMagick &>/dev/null && echo >&2 "Cannot find GraphicsMagick" + echo >&2 "ImageMagic or GraphicsMagic is required, please ensure they are in your PATH" + exit 1 +fi + +CONVERT="convert" +IDENTIFY="identify" +if [[ -z "$HAVE_IMAGEMAGICK" ]]; then + # we must have GraphicsMagick then + CONVERT="gm convert" + IDENTIFY="gm identify" +fi + +# Parameters +SOURCE="$1" +DEST="$2" + +# Check source and destination arguments +if [ -z "${SOURCE}" ]; then + echo "No source image specified" + exit 1 +fi + +if [ -z "${DEST}" ]; then + echo "No destination specified" + exit 1 +fi + +# File Infrastructure +NAME=$(basename "${SOURCE}") +BASE="${NAME%.*}" +TEMP_DIR="convert_temp" + +function cleanUp() { + rm -rf "${TEMP_DIR}" +} + +trap cleanUp EXIT + +mkdir -p "${TEMP_DIR}" + +# check if .ico is a sequence +# pipe into cat so no exit code is given for grep if no matches are found +IS_ICO_SET="$($IDENTIFY "${SOURCE}" | grep -e "\w\.ico\[0" | cat )" + +$CONVERT "${SOURCE}" "${TEMP_DIR}/${BASE}.png" +if [ "${IS_ICO_SET}" ]; then + # extract the largest(?) image from the set + cp "${TEMP_DIR}/${BASE}-0.png" "${DEST}" +else + cp "${TEMP_DIR}/${BASE}.png" "${DEST}" +fi + +rm -rf "${TEMP_DIR}" + +trap - EXIT +cleanUp diff --git a/icon-scripts/convertToTrayIcon b/icon-scripts/convertToTrayIcon new file mode 100755 index 0000000..a9657df --- /dev/null +++ b/icon-scripts/convertToTrayIcon @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# USAGE + +# ./convertToTrayIcon .png +# Example +# ./convertToTrayIcon ~/sample.icns ~/converted.png + +set -e + +SOURCE=$1 +DEST=$2 + +if [ -z "${SOURCE}" ]; then + echo "No source image specified" + exit 1 +fi + +if [ -z "${DEST}" ]; then + echo "No destination specified" + exit 1 +fi + +NAME=$(basename "${SOURCE}") +EXT="${NAME##*.}" + +if [ "${EXT}" == "png" ]; then + cp "${SOURCE}" "${DEST}" + exit 0 +fi + +sips --setProperty format png --resampleHeightWidth "256" "256" "${SOURCE}" --out "${DEST}" diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json new file mode 100644 index 0000000..058c93d --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,8144 @@ +{ + "name": "nativefier", + "version": "51.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nativefier", + "version": "51.0.1", + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.4", + "axios": "^1.4.0", + "electron-packager": "^17.1.1", + "fs-extra": "^11.1.1", + "gitcloud": "^0.2.4", + "hasbin": "^1.2.3", + "loglevel": "^1.8.1", + "ncp": "^2.0.0", + "page-icon": "^0.4.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.21", + "tmp": "^0.2.1", + "yargs": "^17.7.2" + }, + "bin": { + "nativefier": "lib/cli.js" + }, + "devDependencies": { + "@types/debug": "^4.1.8", + "@types/fs-extra": "^11.0.1", + "@types/hasbin": "^1.2.0", + "@types/jest": "^29.5.4", + "@types/ncp": "^2.0.5", + "@types/node": "^20.5.6", + "@types/page-icon": "^0.3.4", + "@types/tmp": "^0.2.3", + "@types/yargs": "^17.0.24", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", + "electron": "^25.7.0", + "eslint": "^8.46.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.6.2", + "playwright": "^1.36.2", + "prettier": "^3.0.1", + "rimraf": "^5.0.1", + "ts-loader": "^9.4.4", + "typescript": "^5.1.6", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + }, + "engines": { + "node": ">= 16.16.0", + "npm": ">= 8.11.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", + "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.10", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", + "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.11", + "@babel/parser": "^7.22.11", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", + "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.10", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", + "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", + "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", + "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz", + "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz", + "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.11", + "@babel/types": "^7.22.11", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", + "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz", + "integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==", + "dependencies": { + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", + "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz", + "integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/universal": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz", + "integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", + "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.4.tgz", + "integrity": "sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.4.tgz", + "integrity": "sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.6.4", + "@jest/reporters": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.6.3", + "jest-config": "^29.6.4", + "jest-haste-map": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-resolve-dependencies": "^29.6.4", + "jest-runner": "^29.6.4", + "jest-runtime": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "jest-watcher": "^29.6.4", + "micromatch": "^4.0.4", + "pretty-format": "^29.6.3", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.4.tgz", + "integrity": "sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.4.tgz", + "integrity": "sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA==", + "dev": true, + "dependencies": { + "expect": "^29.6.4", + "jest-snapshot": "^29.6.4" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", + "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.4.tgz", + "integrity": "sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.6.3", + "jest-mock": "^29.6.3", + "jest-util": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.4.tgz", + "integrity": "sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.4", + "@jest/expect": "^29.6.4", + "@jest/types": "^29.6.3", + "jest-mock": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.4.tgz", + "integrity": "sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.4.tgz", + "integrity": "sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.4.tgz", + "integrity": "sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.6.4", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.4", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", + "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.4", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz", + "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hasbin": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/hasbin/-/hasbin-1.2.0.tgz", + "integrity": "sha512-QhPPTycu+tr/RnGA4mvv+4P1Vebmq9TGEbDvBS9WjPT1pW7dheWeXXWcxb9zJ+YC38LbO8mwVW/DP+FwBroFKw==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", + "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/jsonfile": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", + "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "node_modules/@types/ncp": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/ncp/-/ncp-2.0.5.tgz", + "integrity": "sha512-ocK0p8JuFmX7UkMabFPjY0F7apPvQyLWt5qtdvuvQEBz9i4m2dbzV+6L1zNaUp042RfnL6pHnxDE53OH6XQ9VQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.5.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz", + "integrity": "sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==" + }, + "node_modules/@types/page-icon": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@types/page-icon/-/page-icon-0.3.4.tgz", + "integrity": "sha512-erCUyuzZItLdJ1svtko+1LRIRb9Zvn0yGh2cyteiYSVLiVIgocc9v0vF7truw9VrH4zTpdGwPkrpb/U7MdHIow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.1.tgz", + "integrity": "sha512-3F5PtBzUW0dYlq77Lcqo13fv+58KDwUib3BddilE8ajPJT+faGgxmI9Sw+I8ZS22BYwoir9ZhNXcLi+S+I2bkw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.4.1", + "@typescript-eslint/type-utils": "6.4.1", + "@typescript-eslint/utils": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.1.tgz", + "integrity": "sha512-610G6KHymg9V7EqOaNBMtD1GgpAmGROsmfHJPXNLCU9bfIuLrkdOygltK784F6Crboyd5tBFayPB7Sf0McrQwg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.4.1", + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/typescript-estree": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.4.1.tgz", + "integrity": "sha512-p/OavqOQfm4/Hdrr7kvacOSFjwQ2rrDVJRPxt/o0TOWdFnjJptnjnZ+sYDR7fi4OimvIuKp+2LCkc+rt9fIW+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.4.1.tgz", + "integrity": "sha512-7ON8M8NXh73SGZ5XvIqWHjgX2f+vvaOarNliGhjrJnv1vdjG0LVIz+ToYfPirOoBi56jxAKLfsLm40+RvxVVXA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.4.1", + "@typescript-eslint/utils": "6.4.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.1.tgz", + "integrity": "sha512-zAAopbNuYu++ijY1GV2ylCsQsi3B8QvfPHVqhGdDcbx/NK5lkqMnCGU53amAjccSpk+LfeONxwzUhDzArSfZJg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.1.tgz", + "integrity": "sha512-xF6Y7SatVE/OyV93h1xGgfOkHr2iXuo8ip0gbfzaKeGGuKiAnzS+HtVhSPx8Www243bwlW8IF7X0/B62SzFftg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/visitor-keys": "6.4.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.4.1.tgz", + "integrity": "sha512-F/6r2RieNeorU0zhqZNv89s9bDZSovv3bZQpUNOmmQK1L80/cV4KEu95YUJWi75u5PhboFoKUJBnZ4FQcoqhDw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.4.1", + "@typescript-eslint/types": "6.4.1", + "@typescript-eslint/typescript-estree": "6.4.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.1.tgz", + "integrity": "sha512-y/TyRJsbZPkJIZQXrHfdnxVnxyKegnpEvnRGNam7s3TRR2ykGefEWOhaef00/UUN3IZxizS7BTO3svd3lCOJRQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.4.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", + "integrity": "sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.6.4", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "optional": true + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001523", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001523.tgz", + "integrity": "sha512-I5q5cisATTPZ1mc588Z//pj/Ox80ERYDfR71YnvY7raS/NOk8xXlZcB0sF7JdqaV//kOaa6aus7lRfpdnt1eBA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==" + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "optional": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "optional": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.7.0.tgz", + "integrity": "sha512-P82EzYZ8k9J21x5syhXV7EkezDmEXwycReXnagfzS0kwepnrlWzq1aDIUWdNvzTdHobky4m/nYcL98qd73mEVA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-packager": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz", + "integrity": "sha512-XofXdikjYI7MVBcnXeoOvRR+yFFFHOLs3J7PF5KYQweigtgLshcH4W660PsvHr4lYZ03JBpLyEcUB8DzHZ+BNw==", + "dependencies": { + "@electron/asar": "^3.2.1", + "@electron/get": "^2.0.0", + "@electron/notarize": "^1.2.3", + "@electron/osx-sign": "^1.0.5", + "@electron/universal": "^1.3.2", + "cross-spawn-windows-exe": "^1.2.0", + "debug": "^4.0.1", + "extract-zip": "^2.0.0", + "filenamify": "^4.1.0", + "fs-extra": "^11.1.0", + "galactus": "^1.0.0", + "get-package-info": "^1.0.0", + "junk": "^3.1.0", + "parse-author": "^2.0.0", + "plist": "^3.0.0", + "rcedit": "^3.0.1", + "resolve": "^1.1.6", + "semver": "^7.1.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "electron-packager": "bin/electron-packager.js" + }, + "engines": { + "node": ">= 14.17.5" + }, + "funding": { + "url": "https://github.com/electron/electron-packager?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.502", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.502.tgz", + "integrity": "sha512-xqeGw3Gr6o3uyHy/yKjdnDQHY2RQvXcGC2cfHjccK1IGkH6cX1WQBN8EeC/YpwPhGkBaikDTecJ8+ssxSVRQlw==", + "dev": true + }, + "node_modules/electron/node_modules/@types/node": { + "version": "18.17.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", + "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "optional": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", + "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "^8.47.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", + "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/flora-colossus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", + "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", + "dependencies": { + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/flora-colossus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/galactus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", + "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", + "dependencies": { + "debug": "^4.3.4", + "flora-colossus": "^2.0.0", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/galactus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "optional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", + "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", + "dependencies": { + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/get-package-info/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-package-info/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gitcloud": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/gitcloud/-/gitcloud-0.2.4.tgz", + "integrity": "sha512-xtO+GTv6c/XqnGqajRpv1aoTk3wFQPdQKjHP8cRZFxsCIQvYCTvgwqFGNv0+ux9HVREW10H5EIHN1Kw0r3PKMQ==", + "dependencies": { + "axios": "^0.27.2", + "cheerio": "^1.0.0-rc.12" + }, + "engines": { + "node": ">= 12.0.0", + "npm": ">= 6.0.0" + } + }, + "node_modules/gitcloud/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "optional": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "optional": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasbin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/hasbin/-/hasbin-1.2.3.tgz", + "integrity": "sha512-CCd8e/w2w28G8DyZvKgiHnQJ/5XXDz6qiUHnthvtag/6T5acUeN5lqq+HMoBqcmgWueWDhiCplrw0Kb1zDACRg==", + "dependencies": { + "async": "~1.5" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", + "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.0.tgz", + "integrity": "sha512-uKmsITSsF4rUWQHzqaRUuyAir3fZfW3f202Ee34lz/gZCi970CPZwyQXLGNgWJvvZbvFyzeyGq0+4fcG/mBKZg==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.4.tgz", + "integrity": "sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.6.4", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.6.4" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.6.3.tgz", + "integrity": "sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.6.3", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.4.tgz", + "integrity": "sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.4", + "@jest/expect": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-runtime": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", + "p-limit": "^3.1.0", + "pretty-format": "^29.6.3", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.4.tgz", + "integrity": "sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==", + "dev": true, + "dependencies": { + "@jest/core": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.4.tgz", + "integrity": "sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.6.4", + "@jest/types": "^29.6.3", + "babel-jest": "^29.6.4", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.6.4", + "jest-environment-node": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-runner": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.6.3", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", + "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.6.3.tgz", + "integrity": "sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.3.tgz", + "integrity": "sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.6.3", + "pretty-format": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.4.tgz", + "integrity": "sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.4", + "@jest/fake-timers": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.6.3", + "jest-util": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", + "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz", + "integrity": "sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", + "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.6.4", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", + "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.6.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.3.tgz", + "integrity": "sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.4.tgz", + "integrity": "sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.4.tgz", + "integrity": "sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.6.4" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.4.tgz", + "integrity": "sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw==", + "dev": true, + "dependencies": { + "@jest/console": "^29.6.4", + "@jest/environment": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.6.3", + "jest-environment-node": "^29.6.4", + "jest-haste-map": "^29.6.4", + "jest-leak-detector": "^29.6.3", + "jest-message-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-runtime": "^29.6.4", + "jest-util": "^29.6.3", + "jest-watcher": "^29.6.4", + "jest-worker": "^29.6.4", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.4.tgz", + "integrity": "sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.4", + "@jest/fake-timers": "^29.6.4", + "@jest/globals": "^29.6.4", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-mock": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.4.tgz", + "integrity": "sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.6.4", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", + "natural-compare": "^1.4.0", + "pretty-format": "^29.6.3", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", + "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.3.tgz", + "integrity": "sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.4.tgz", + "integrity": "sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.6.3", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", + "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.6.3", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/page-icon": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/page-icon/-/page-icon-0.4.0.tgz", + "integrity": "sha512-dPY5pK+AT8Z1YFXTIfN4vmth8JngNny4qKm1EXBuOUyfiVdO5X3/Rq+tirHUN5A7b7eKQ+pZMsA74RPdHU17Sw==", + "dependencies": { + "axios": "^0.21.1", + "cheerio": "^1.0.0-rc.5", + "file-type": "^16.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 6.0.0" + } + }, + "node_modules/page-icon/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.37.1.tgz", + "integrity": "sha512-bgUXRrQKhT48zHdxDYQTpf//0xDfDd5hLeEhjuSw8rXEGoT9YeElpfvs/izonTNY21IQZ7d3s22jLxYaAnubbQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.37.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright-core": { + "version": "1.37.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz", + "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", + "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rcedit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.1.0.tgz", + "integrity": "sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", + "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", + "dev": true, + "dependencies": { + "glob": "^10.2.5" + }, + "bin": { + "rimraf": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "optional": true + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "optional": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.19.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", + "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", + "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4b085ae --- /dev/null +++ b/package.json @@ -0,0 +1,145 @@ +{ + "name": "nativefier", + "version": "52.0.0", + "description": "Wrap web apps natively", + "license": "MIT", + "author": "Goh Jia Hao", + "engines_README": "Bumping the minimum required Node version? You must bump: 1. package.json -> engines.node, 2. package.json -> devDependencies.@types/node , 3. tsconfig.json -> {target, lib} , 4. .github/workflows/ci.yml -> node-version", + "engines_READMEforEnginesNode": "Here in engines.node, we require a version as old as possible, for Nativefier to be easily installable using the stock Node.js shipped by conservative Linux distros. It's a balancing act between this, and our own dependencies requiring more a recent Node; as much as possible, try to keep supporting Debian stable; https://packages.debian.org/search?suite=stable&keywords=nodejs", + "engines": { + "node": ">= 16.16.0", + "npm": ">= 8.11.0" + }, + "keywords": [ + "desktop", + "electron", + "app", + "native", + "wrapper" + ], + "main": "lib/main.js", + "typings": "lib/main.d.ts", + "bin": { + "nativefier": "lib/cli.js" + }, + "homepage": "https://github.com/nativefier/nativefier", + "repository": { + "type": "git", + "url": "git+https://github.com/nativefier/nativefier.git" + }, + "bugs": { + "url": "https://github.com/nativefier/nativefier/issues" + }, + "scripts": { + "build-app": "cd app && webpack", + "build-app-static": "ncp app/src/static/ app/lib/static/ && ncp app/dist/preload.js app/lib/preload.js && ncp app/dist/preload.js.map app/lib/preload.js.map", + "build": "npm run clean && tsc --build shared src app && npm run build-app && npm run build-app-static", + "build:watch": "npm run clean && tsc --build shared src app --watch", + "changelog": "./.github/generate-changelog", + "clean": "rimraf coverage/ lib/ app/lib/ app/dist/ shared/lib", + "clean:full": "npm run clean && rimraf app/node_modules/ node_modules/", + "lint:fix": "cd src && eslint . --ext .ts --fix && cd ../shared && eslint src --ext .ts --fix && cd ../app && eslint src --ext .ts --fix", + "lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts' 'shared/src/**/*.ts'", + "lint": "eslint shared app src --ext .ts", + "list-outdated-deps": "npm out -l; cd app && npm out -l; true", + "prepare": "cd app && npm ci && cd .. && npm run build", + "relock:cli": "rm -rf ./node_modules/ ./npm-shrinkwrap.json && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out -l", + "relock:app": "rm -rf ./app/node_modules/ ./app/npm-shrinkwrap.json && cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out -l", + "relock": "npm run relock:cli; npm run relock:app", + "test:integration": "jest --testRegex=integration-test", + "test:manual": "npm run build && bash .github/manual-test", + "test:playwright": "jest --detectOpenHandles --testRegex=playwright-test", + "test:noplaywright": "jest --testPathIgnorePatterns=playwright", + "test:unit": "jest", + "test:watch": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false", + "test:watch:unit": "echo 'Remember to run npm run build:watch for the test watcher to work!' && jest --watchAll --collectCoverage=false --testPathIgnorePatterns=integration --testPathIgnorePatterns=playwright", + "test:withlog": "LOGLEVEL=trace npm run test", + "test": "jest", + "watch": "npx concurrently \"npm:*:watch\"" + }, + "dependencies": { + "@electron/asar": "^3.2.4", + "axios": "^1.4.0", + "electron-packager": "^17.1.1", + "fs-extra": "^11.1.1", + "gitcloud": "^0.2.4", + "hasbin": "^1.2.3", + "loglevel": "^1.8.1", + "ncp": "^2.0.0", + "page-icon": "^0.4.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.21", + "tmp": "^0.2.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/debug": "^4.1.8", + "@types/fs-extra": "^11.0.1", + "@types/hasbin": "^1.2.0", + "@types/jest": "^29.5.4", + "@types/ncp": "^2.0.5", + "@types/node": "^20.5.6", + "@types/page-icon": "^0.3.4", + "@types/tmp": "^0.2.3", + "@types/yargs": "^17.0.24", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", + "electron": "^25.7.0", + "eslint": "^8.46.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.6.2", + "playwright": "^1.36.2", + "prettier": "^3.0.1", + "rimraf": "^5.0.1", + "ts-loader": "^9.4.4", + "typescript": "^5.1.6", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + }, + "jest_COMMENTS": { + "testPathIgnorePatterns": "See https://jestjs.io/docs/configuration#testpathignorepatterns-arraystring . We set it to 1. ignore coverage for deps, and 2. be sure we test the compiled JS, which is in `lib`, not `src` or `dist`", + "watchPathIgnorePatterns": "See https://jestjs.io/docs/configuration#watchpathignorepatterns-arraystring . We set it for `jest --watch` (a.k.a. `npm run test:watch`) to trigger only after `tsc --watch` (a.k.a. `npm run build:watch`) completes its incremental compilation. Else, jest will pick up immediately on changes in `src` when TSC is barely running, hence testing not-recompiled-yet code and being super confusing, as 1. your changes won't be taken during this first run, and 2. the *next* run (e.g. after a second 'Save' in your editor) will actually have the new code :D" + }, + "jest": { + "collectCoverage": true, + "collectCoverageFrom": [ + "./app/dist/**/*.js", + "./lib/**/*.js", + "./shared/lib/**/*.js" + ], + "coveragePathIgnorePatterns": [ + "[.-]test.js$" + ], + "moduleNameMapper": { + "^electron$": "/app/dist/mocks/electron.js" + }, + "setupFiles": [ + "./lib/jestSetupFiles" + ], + "testEnvironment": "node", + "testPathIgnorePatterns": [ + "/app/node_modules.*", + "/app/src.*", + "/app/lib.*", + "/src.*", + ".+\\.d\\.ts", + ".+\\.js\\.map" + ], + "testRegex": "test\\.js", + "testTimeout": 15000, + "watchPathIgnorePatterns": [ + "/app/lib.*", + "/app/src.*", + "/app/tsconfig.json", + "/shared/tsconfig.json", + "/src.*", + "/tsconfig-base.json" + ] + }, + "prettier": { + "arrowParens": "always", + "singleQuote": true, + "trailingComma": "all" + } +} diff --git a/shared/.eslintrc.js b/shared/.eslintrc.js new file mode 100644 index 0000000..6f881af --- /dev/null +++ b/shared/.eslintrc.js @@ -0,0 +1,14 @@ +const base = require('../base-eslintrc'); + +// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md +module.exports = { + parser: base.parser, + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + plugins: base.plugins, + extends: base.extends, + rules: base.rules, + ignorePatterns: ['lib/**'], +}; diff --git a/shared/src/options/model.ts b/shared/src/options/model.ts new file mode 100644 index 0000000..9424fe5 --- /dev/null +++ b/shared/src/options/model.ts @@ -0,0 +1,238 @@ +import { CreateOptions } from '@electron/asar'; +import { randomUUID } from 'crypto'; +import * as electronPackager from 'electron-packager'; + +export type TitleBarValue = + | 'default' + | 'hidden' + | 'hiddenInset' + | 'customButtonsOnHover'; +export type TrayValue = 'true' | 'false' | 'start-in-tray'; + +export interface ElectronPackagerOptions extends electronPackager.Options { + arch: string; + portable: boolean; + platform?: string; + targetUrl: string; + upgrade: boolean; + upgradeFrom?: string; +} + +export interface AppOptions { + packager: ElectronPackagerOptions; + nativefier: { + accessibilityPrompt: boolean; + alwaysOnTop: boolean; + backgroundColor?: string; + basicAuthPassword?: string; + basicAuthUsername?: string; + blockExternalUrls: boolean; + bookmarksMenu?: string; + bounce: boolean; + browserwindowOptions?: BrowserWindowOptions; + clearCache: boolean; + counter: boolean; + crashReporter?: string; + disableContextMenu: boolean; + disableDevTools: boolean; + disableGpu: boolean; + disableOldBuildWarning: boolean; + diskCacheSize?: number; + electronVersionUsed?: string; + enableEs3Apis: boolean; + fastQuit: boolean; + fileDownloadOptions?: Record; + flashPluginDir?: string; + fullScreen: boolean; + globalShortcuts?: GlobalShortcut[]; + hideWindowFrame: boolean; + ignoreCertificate: boolean; + ignoreGpuBlacklist: boolean; + inject?: string[]; + insecure: boolean; + internalUrls?: string; + lang?: string; + maximize: boolean; + nativefierVersion: string; + processEnvs?: string; + proxyRules?: string; + quiet?: boolean; + showMenuBar: boolean; + singleInstance: boolean; + strictInternalUrls: boolean; + titleBarStyle?: TitleBarValue; + tray: TrayValue; + userAgent?: string; + userAgentHonest: boolean; + verbose: boolean; + versionString?: string; + width?: number; + widevine: boolean; + height?: number; + minWidth?: number; + minHeight?: number; + maxWidth?: number; + maxHeight?: number; + x?: number; + y?: number; + zoom: number; + }; +} + +export type BrowserWindowOptions = Record & { + webPreferences?: Record; +}; + +export type GlobalShortcut = { + key: string; + inputEvents: { + type: + | 'mouseDown' + | 'mouseUp' + | 'mouseEnter' + | 'mouseLeave' + | 'contextMenu' + | 'mouseWheel' + | 'mouseMove' + | 'keyDown' + | 'keyUp' + | 'char'; + keyCode: string; + }[]; +}; + +export type NativefierOptions = Partial< + AppOptions['packager'] & AppOptions['nativefier'] +>; + +export type OutputOptions = NativefierOptions & { + blockExternalUrls: boolean; + browserwindowOptions?: BrowserWindowOptions; + buildDate: number; + companyName?: string; + disableDevTools: boolean; + fileDownloadOptions?: Record; + internalUrls: string | RegExp | undefined; + isUpgrade: boolean; + name: string; + nativefierVersion: string; + oldBuildWarningText: string; + strictInternalUrls: boolean; + tabbingIdentifier?: string; + targetUrl: string; + userAgent?: string; + zoom?: number; +}; + +export type PackageJSON = { + name: string; +}; + +export type RawOptions = { + accessibilityPrompt?: boolean; + alwaysOnTop?: boolean; + appCopyright?: string; + appVersion?: string; + arch?: string; + asar?: boolean | CreateOptions; + backgroundColor?: string; + basicAuthPassword?: string; + basicAuthUsername?: string; + blockExternalUrls?: boolean; + bookmarksMenu?: string; + bounce?: boolean; + browserwindowOptions?: BrowserWindowOptions; + buildVersion?: string; + clearCache?: boolean; + conceal?: boolean; + counter?: boolean; + crashReporter?: string; + darwinDarkModeSupport?: boolean; + disableContextMenu?: boolean; + disableDevTools?: boolean; + disableGpu?: boolean; + disableOldBuildWarning?: boolean; + disableOldBuildWarningYesiknowitisinsecure?: boolean; + diskCacheSize?: number; + electronVersion?: string; + electronVersionUsed?: string; + enableEs3Apis?: boolean; + fastQuit?: boolean; + fileDownloadOptions?: Record; + flashPath?: string; + flashPluginDir?: string; + fullScreen?: boolean; + globalShortcuts?: string | GlobalShortcut[]; + height?: number; + hideWindowFrame?: boolean; + icon?: string; + ignoreCertificate?: boolean; + ignoreGpuBlacklist?: boolean; + inject?: string[]; + insecure?: boolean; + internalUrls?: string; + lang?: string; + maxHeight?: number; + maximize?: boolean; + maxWidth?: number; + minHeight?: number; + minWidth?: number; + name?: string; + nativefierVersion?: string; + out?: string; + overwrite?: boolean; + platform?: string; + portable?: boolean; + processEnvs?: string; + proxyRules?: string; + quiet?: boolean; + showMenuBar?: boolean; + singleInstance?: boolean; + strictInternalUrls?: boolean; + targetUrl?: string; + titleBarStyle?: TitleBarValue; + tray?: TrayValue; + upgrade?: string | boolean; + upgradeFrom?: string; + userAgent?: string; + userAgentHonest?: boolean; + verbose?: boolean; + versionString?: string; + widevine?: boolean; + width?: number; + win32metadata?: electronPackager.Win32MetadataOptions; + x?: number; + y?: number; + zoom?: number; +}; + +export type WindowOptions = { + autoHideMenuBar: boolean; + blockExternalUrls: boolean; + browserwindowOptions?: BrowserWindowOptions; + insecure: boolean; + internalUrls?: string | RegExp; + strictInternalUrls?: boolean; + name: string; + proxyRules?: string; + show?: boolean; + tabbingIdentifier?: string; + targetUrl: string; + userAgent?: string; + zoom: number; +}; + +export function outputOptionsToWindowOptions( + options: OutputOptions, + generateTabbingIdentifierIfMissing: boolean, +): WindowOptions { + return { + ...options, + autoHideMenuBar: !options.showMenuBar, + insecure: options.insecure ?? false, + tabbingIdentifier: generateTabbingIdentifierIfMissing + ? options.tabbingIdentifier ?? randomUUID() + : options.tabbingIdentifier, + zoom: options.zoom ?? 1.0, + }; +} diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 0000000..060ba00 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "composite": true, + "outDir": "./lib", + // Here we want to set target and lib to the *worst* of app/tsconfig.json and src/tsconfig.json + // (plus "dom"), because shared code will run both in CLI Node and app Node. + // See comments in app/tsconfig.json and src/tsconfig.json + "target": "es2018", + "lib": [ + "es2018", + "dom" + ] + }, + "include": [ + "./src/**/*" + ], +} diff --git a/src/.eslintrc.js b/src/.eslintrc.js new file mode 100644 index 0000000..3b13199 --- /dev/null +++ b/src/.eslintrc.js @@ -0,0 +1,13 @@ +const base = require('../base-eslintrc'); + +// # https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md +module.exports = { + parser: base.parser, + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + plugins: base.plugins, + extends: base.extends, + rules: base.rules, +}; diff --git a/src/build/buildIcon.ts b/src/build/buildIcon.ts new file mode 100644 index 0000000..8761f81 --- /dev/null +++ b/src/build/buildIcon.ts @@ -0,0 +1,97 @@ +import * as path from 'path'; + +import * as log from 'loglevel'; + +import { isOSX } from '../helpers/helpers'; +import { + convertToPng, + convertToIco, + convertToIcns, + convertToTrayIcon, +} from '../helpers/iconShellHelpers'; +import { AppOptions } from '../../shared/src/options/model'; + +function iconIsIco(iconPath: string): boolean { + return path.extname(iconPath) === '.ico'; +} + +function iconIsPng(iconPath: string): boolean { + return path.extname(iconPath) === '.png'; +} + +function iconIsIcns(iconPath: string): boolean { + return path.extname(iconPath) === '.icns'; +} + +/** + * Will convert a `.png` icon to the appropriate arch format (if necessary), + * and return adjusted options + */ +export function convertIconIfNecessary(options: AppOptions): void { + if (!options.packager.icon) { + log.debug('Option "icon" not set, skipping icon conversion.'); + return; + } + + if (options.packager.platform === 'win32') { + if (iconIsIco(options.packager.icon)) { + log.debug( + 'Building for Windows and icon is already a .ico, no conversion needed', + ); + return; + } + + try { + const iconPath = convertToIco(options.packager.icon); + options.packager.icon = iconPath; + return; + } catch (err: unknown) { + log.warn('Failed to convert icon to .ico, skipping.', err); + return; + } + } + + if (options.packager.platform === 'linux') { + if (iconIsPng(options.packager.icon)) { + log.debug( + 'Building for Linux and icon is already a .png, no conversion needed', + ); + return; + } + + try { + const iconPath = convertToPng(options.packager.icon); + options.packager.icon = iconPath; + return; + } catch (err: unknown) { + log.warn('Failed to convert icon to .png, skipping.', err); + return; + } + } + + if (iconIsIcns(options.packager.icon)) { + log.debug( + 'Building for macOS and icon is already a .icns, no conversion needed', + ); + } + + if (!isOSX()) { + log.warn( + 'Skipping icon conversion to .icns, conversion is only supported on macOS', + ); + return; + } + + try { + if (!iconIsIcns(options.packager.icon)) { + const iconPath = convertToIcns(options.packager.icon); + options.packager.icon = iconPath; + } + if (options.nativefier.tray !== 'false') { + convertToTrayIcon(options.packager.icon); + } + } catch (err: unknown) { + log.warn('Failed to convert icon to .icns, skipping.', err); + options.packager.icon = undefined; + } +} diff --git a/src/build/buildNativefierApp.ts b/src/build/buildNativefierApp.ts new file mode 100644 index 0000000..97786a4 --- /dev/null +++ b/src/build/buildNativefierApp.ts @@ -0,0 +1,267 @@ +import * as path from 'path'; + +import * as electronGet from '@electron/get'; +import electronPackager from 'electron-packager'; +import * as fs from 'fs-extra'; +import * as log from 'loglevel'; + +import { convertIconIfNecessary } from './buildIcon'; +import { + getTempDir, + hasWine, + isWindows, + isWindowsAdmin, +} from '../helpers/helpers'; +import { useOldAppOptions, findUpgradeApp } from '../helpers/upgrade/upgrade'; +import { AppOptions, RawOptions } from '../../shared/src/options/model'; +import { getOptions } from '../options/optionsMain'; +import { prepareElectronApp } from './prepareElectronApp'; + +const OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD = [ + 'icon', + 'appCopyright', + 'appVersion', + 'buildVersion', + 'versionString', + 'win32metadata', +]; + +/** + * For Windows & Linux, we have to copy over the icon to the resources/app + * folder, which the BrowserWindow is hard-coded to read the icon from + */ +async function copyIconsIfNecessary( + options: AppOptions, + appPath: string, +): Promise { + log.debug('Copying icons if necessary'); + if (!options.packager.icon) { + log.debug('No icon specified in options; aborting'); + return; + } + + if ( + options.packager.platform === 'darwin' || + options.packager.platform === 'mas' + ) { + if (options.nativefier.tray !== 'false') { + //tray icon needs to be .png + log.debug('Copying icon for tray application'); + const trayIconFileName = `tray-icon.png`; + const destIconPath = path.join(appPath, 'icon.png'); + await fs.copy( + `${path.dirname(options.packager.icon)}/${trayIconFileName}`, + destIconPath, + ); + } else { + log.debug('No copying necessary on macOS; aborting'); + } + return; + } + + // windows & linux: put the icon file into the app + const destFileName = `icon${path.extname(options.packager.icon)}`; + const destIconPath = path.join(appPath, destFileName); + + log.debug(`Copying icon ${options.packager.icon} to`, destIconPath); + await fs.copy(options.packager.icon, destIconPath); +} + +/** + * Checks the app path array to determine if packaging completed successfully + */ +function getAppPath(appPath: string | string[]): string | undefined { + if (!Array.isArray(appPath)) { + return appPath; + } + + if (appPath.length === 0) { + return undefined; // directory already exists and `--overwrite` not set + } + + if (appPath.length > 1) { + log.warn( + 'Warning: This should not be happening, packaged app path contains more than one element:', + appPath, + ); + } + + return appPath[0]; +} + +function isUpgrade(rawOptions: RawOptions): boolean { + if ( + rawOptions.upgrade !== undefined && + typeof rawOptions.upgrade === 'string' && + rawOptions.upgrade !== '' + ) { + rawOptions.upgradeFrom = rawOptions.upgrade; + rawOptions.upgrade = true; + return true; + } + return false; +} + +function trimUnprocessableOptions(options: AppOptions): void { + if (options.packager.platform === 'win32' && !isWindows() && !hasWine()) { + const optionsPresent = Object.entries(options) + .filter( + ([key, value]) => + OPTIONS_REQUIRING_WINDOWS_FOR_WINDOWS_BUILD.includes(key) && !!value, + ) + .map(([key]) => key); + if (optionsPresent.length === 0) { + return; + } + log.warn( + `*Not* setting [${optionsPresent.join(', ')}], as couldn't find Wine.`, + 'Wine is required when packaging a Windows app under on non-Windows platforms.', + 'Also, note that Windows apps built under non-Windows platforms without Wine *will lack* certain', + 'features, like a correct icon and process name. Do yourself a favor and install Wine, please.', + ); + for (const keyToUnset of optionsPresent) { + (options as unknown as Record)[keyToUnset] = undefined; + } + } +} + +function getOSRunHelp(platform?: string): string { + if (platform === 'win32') { + return `the contained .exe file.`; + } else if (platform === 'linux') { + return `the contained executable file (prefixing with ./ if necessary)\nMenu/desktop shortcuts are up to you, because Nativefier cannot know where you're going to move the app. Search for "linux .desktop file" for help, or see https://wiki.archlinux.org/index.php/Desktop_entries`; + } else if (platform === 'darwin') { + return `the app bundle.`; + } + return ''; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function buildNativefierApp( + rawOptions: RawOptions, +): Promise { + // early-suppress potential logging before full options handling + if (rawOptions.quiet) { + log.setLevel('silent'); + } + + log.warn( + '\n\n Hi! Nativefier is minimally maintained these days, and needs more hands.\n' + + ' If you have the time & motivation, help with bugfixes and maintenance is VERY welcome.\n' + + ' Please go to https://github.com/nativefier/nativefier and help how you can. Thanks.\n\n', + ); + + log.info('\nProcessing options...'); + + let finalOutDirectory = rawOptions.out ?? process.cwd(); + + if (isUpgrade(rawOptions)) { + log.debug('Attempting to upgrade from', rawOptions.upgradeFrom); + const oldApp = findUpgradeApp(rawOptions.upgradeFrom as string); + if (!oldApp) { + throw new Error( + `Could not find an old Nativfier app in "${ + rawOptions.upgradeFrom as string + }"`, + ); + } + rawOptions = useOldAppOptions(rawOptions, oldApp); + if (rawOptions.out === undefined && rawOptions.overwrite) { + finalOutDirectory = oldApp.appRoot; + rawOptions.out = getTempDir('appUpgrade', 0o755); + } + } + log.debug('rawOptions', rawOptions); + + const options = await getOptions(rawOptions); + log.debug('options', options); + + if (options.packager.platform === 'darwin' && isWindows()) { + // electron-packager has to extract the desired electron package for the target platform. + // For a target platform of Mac, this zip file contains symlinks. And on Windows, extracting + // files that are symlinks need Admin permissions. So we'll check if the user is an admin, and + // fail early if not. + // For reference + // https://github.com/electron/electron-packager/issues/933 + // https://github.com/electron/electron-packager/issues/1194 + // https://github.com/electron/electron/issues/11094 + if (!isWindowsAdmin()) { + throw new Error( + 'Building an app with a target platform of Mac on a Windows machine requires admin priveleges to perform. Please rerun this command in an admin command prompt.', + ); + } + } + + log.info('\nPreparing Electron app...'); + const tmpPath = getTempDir('app', 0o755); + await prepareElectronApp(options.packager.dir, tmpPath, options); + + log.info('\nConverting icons...'); + options.packager.dir = tmpPath; + convertIconIfNecessary(options); + await copyIconsIfNecessary(options, tmpPath); + + options.packager.quiet = !rawOptions.verbose; + + log.info( + "\nPackaging... This will take a few seconds, maybe minutes if the requested Electron isn't cached yet...", + ); + trimUnprocessableOptions(options); + electronGet.initializeProxy(); // https://github.com/electron/get#proxies + const appPathArray = await electronPackager(options.packager); + + log.info('\nFinalizing build...'); + let appPath = getAppPath(appPathArray); + + if (!appPath) { + throw new Error('App Path could not be determined.'); + } + + if ( + options.packager.upgrade && + options.packager.upgradeFrom && + options.packager.overwrite + ) { + if (options.packager.platform === 'darwin') { + try { + // This is needed due to a funky thing that happens when copying Squirrel.framework + // over where it gets into a circular file reference somehow. + await fs.remove( + path.join( + finalOutDirectory, + `${options.packager.name ?? ''}.app`, + 'Contents', + 'Frameworks', + ), + ); + } catch (err: unknown) { + log.warn( + 'Encountered an error when attempting to pre-delete old frameworks:', + err, + ); + } + await fs.copy( + path.join(appPath, `${options.packager.name ?? ''}.app`), + path.join(finalOutDirectory, `${options.packager.name ?? ''}.app`), + { + overwrite: options.packager.overwrite, + preserveTimestamps: true, + }, + ); + } else { + await fs.copy(appPath, finalOutDirectory, { + overwrite: options.packager.overwrite, + preserveTimestamps: true, + }); + } + await fs.remove(appPath); + appPath = finalOutDirectory; + } + + const osRunHelp = getOSRunHelp(options.packager.platform); + log.info( + `App built to ${appPath}, move to wherever it makes sense for you and run ${osRunHelp}`, + ); + + return appPath; +} diff --git a/src/build/prepareElectronApp.test.ts b/src/build/prepareElectronApp.test.ts new file mode 100644 index 0000000..2dca8e6 --- /dev/null +++ b/src/build/prepareElectronApp.test.ts @@ -0,0 +1,11 @@ +import { normalizeAppName } from './prepareElectronApp'; + +describe('normalizeAppName', () => { + test('it is stable', () => { + // Non-determinism / unstability would cause using a different appName + // at each app regen, thus a different appData folder, which would cause + // losing user state, including login state through cookies. + const normalizedTrello = normalizeAppName('Trello', 'https://trello.com'); + expect(normalizedTrello).toBe('trello-nativefier-679e8e'); + }); +}); diff --git a/src/build/prepareElectronApp.ts b/src/build/prepareElectronApp.ts new file mode 100644 index 0000000..637d514 --- /dev/null +++ b/src/build/prepareElectronApp.ts @@ -0,0 +1,219 @@ +import * as crypto from 'crypto'; +import * as fs from 'fs-extra'; +import * as path from 'path'; + +import * as log from 'loglevel'; + +import { generateRandomSuffix } from '../helpers/helpers'; +import { + AppOptions, + OutputOptions, + PackageJSON, +} from '../../shared/src/options/model'; +import { parseJson } from '../utils/parseUtils'; +import { DEFAULT_APP_NAME } from '../constants'; + +/** + * Only picks certain app args to pass to nativefier.json + */ +function pickElectronAppArgs(options: AppOptions): OutputOptions { + return { + accessibilityPrompt: options.nativefier.accessibilityPrompt, + alwaysOnTop: options.nativefier.alwaysOnTop, + appBundleId: options.packager.appBundleId, + appCategoryType: options.packager.appCategoryType, + appCopyright: options.packager.appCopyright, + appVersion: options.packager.appVersion, + arch: options.packager.arch, + asar: options.packager.asar, + backgroundColor: options.nativefier.backgroundColor, + basicAuthPassword: options.nativefier.basicAuthPassword, + basicAuthUsername: options.nativefier.basicAuthUsername, + blockExternalUrls: options.nativefier.blockExternalUrls, + bounce: options.nativefier.bounce, + browserwindowOptions: options.nativefier.browserwindowOptions, + buildDate: new Date().getTime(), + buildVersion: options.packager.buildVersion, + clearCache: options.nativefier.clearCache, + counter: options.nativefier.counter, + crashReporter: options.nativefier.crashReporter, + darwinDarkModeSupport: options.packager.darwinDarkModeSupport, + derefSymlinks: options.packager.derefSymlinks, + disableContextMenu: options.nativefier.disableContextMenu, + disableDevTools: options.nativefier.disableDevTools, + disableGpu: options.nativefier.disableGpu, + disableOldBuildWarning: options.nativefier.disableOldBuildWarning, + diskCacheSize: options.nativefier.diskCacheSize, + download: options.packager.download, + electronVersionUsed: options.packager.electronVersion, + enableEs3Apis: options.nativefier.enableEs3Apis, + executableName: options.packager.executableName, + fastQuit: options.nativefier.fastQuit, + fileDownloadOptions: options.nativefier.fileDownloadOptions, + flashPluginDir: options.nativefier.flashPluginDir, + fullScreen: options.nativefier.fullScreen, + globalShortcuts: options.nativefier.globalShortcuts, + height: options.nativefier.height, + helperBundleId: options.packager.helperBundleId, + hideWindowFrame: options.nativefier.hideWindowFrame, + ignoreCertificate: options.nativefier.ignoreCertificate, + ignoreGpuBlacklist: options.nativefier.ignoreGpuBlacklist, + insecure: options.nativefier.insecure, + internalUrls: options.nativefier.internalUrls, + isUpgrade: options.packager.upgrade, + junk: options.packager.junk, + lang: options.nativefier.lang, + maximize: options.nativefier.maximize, + maxHeight: options.nativefier.maxHeight, + maxWidth: options.nativefier.maxWidth, + minHeight: options.nativefier.minHeight, + minWidth: options.nativefier.minWidth, + name: options.packager.name ?? DEFAULT_APP_NAME, + nativefierVersion: options.nativefier.nativefierVersion, + osxNotarize: options.packager.osxNotarize, + osxSign: options.packager.osxSign, + portable: options.packager.portable, + processEnvs: options.nativefier.processEnvs, + protocols: options.packager.protocols, + proxyRules: options.nativefier.proxyRules, + prune: options.packager.prune, + quiet: options.packager.quiet, + showMenuBar: options.nativefier.showMenuBar, + singleInstance: options.nativefier.singleInstance, + strictInternalUrls: options.nativefier.strictInternalUrls, + targetUrl: options.packager.targetUrl, + titleBarStyle: options.nativefier.titleBarStyle, + tray: options.nativefier.tray, + usageDescription: options.packager.usageDescription, + userAgent: options.nativefier.userAgent, + userAgentHonest: options.nativefier.userAgentHonest, + versionString: options.nativefier.versionString, + width: options.nativefier.width, + widevine: options.nativefier.widevine, + win32metadata: options.packager.win32metadata, + x: options.nativefier.x, + y: options.nativefier.y, + zoom: options.nativefier.zoom, + // OLD_BUILD_WARNING_TEXT is an undocumented env. var to let *packagers* + // tweak the message shown on warning about an old build, to something + // more tailored to their audience (who might not even know Nativefier). + // See https://github.com/kelyvin/Google-Messages-For-Desktop/issues/34#issuecomment-812731144 + // and https://github.com/nativefier/nativefier/issues/1131#issuecomment-812646988 + oldBuildWarningText: process.env.OLD_BUILD_WARNING_TEXT || '', + }; +} + +async function maybeCopyScripts( + srcs: string[] | undefined, + dest: string, +): Promise { + if (!srcs || srcs.length === 0) { + log.debug('No files to inject, skipping copy.'); + return; + } + + const supportedInjectionExtensions = ['.css', '.js']; + + log.debug(`Copying ${srcs.length} files to inject in app.`); + for (const src of srcs) { + if (!fs.existsSync(src)) { + throw new Error( + `File ${src} not found. Note that Nativefier expects *local* files, not URLs.`, + ); + } + + if (supportedInjectionExtensions.indexOf(path.extname(src)) < 0) { + log.warn('Skipping unsupported injection file', src); + continue; + } + + const postFixHash = generateRandomSuffix(); + const destFileName = `inject-${postFixHash}${path.extname(src)}`; + const destPath = path.join(dest, 'inject', destFileName); + log.debug(`Copying injection file "${src}" to "${destPath}"`); + await fs.copy(src, destPath); + } +} + +/** + * Use a basic 6-character hash to prevent collisions. The hash is deterministic url & name, + * so that an upgrade (same URL) of an app keeps using the same appData folder. + * Warning! Changing this normalizing & hashing will change the way appNames are generated, + * changing appData folder, and users will get logged out of their apps after an upgrade. + */ +export function normalizeAppName(appName: string, url: string): string { + const hash = crypto.createHash('md5'); + hash.update(url); + const postFixHash = hash.digest('hex').substring(0, 6); + const normalized = appName + .toLowerCase() + .replace(/[,:.]/g, '') + .replace(/[\s_]/g, '-'); + return `${normalized}-nativefier-${postFixHash}`; +} + +function changeAppPackageJsonName( + appPath: string, + name: string, + url: string, +): string { + const packageJsonPath = path.join(appPath, '/package.json'); + const packageJson = parseJson( + fs.readFileSync(packageJsonPath).toString(), + ); + if (!packageJson) { + throw new Error(`Could not load package.json from ${packageJsonPath}`); + } + const normalizedAppName = normalizeAppName(name, url); + packageJson.name = normalizedAppName; + log.debug(`Updating ${packageJsonPath} 'name' field to ${normalizedAppName}`); + + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + + return normalizedAppName; +} + +/** + * Creates a temporary directory, copies the './app folder' inside, + * and adds a text file with the app configuration. + */ +export async function prepareElectronApp( + src: string, + dest: string, + options: AppOptions, +): Promise { + log.debug(`Copying electron app from ${src} to ${dest}`); + try { + await fs.copy(src, dest); + } catch (err: unknown) { + throw `Error copying electron app from ${src} to temp dir ${dest}. Error: ${ + (err as Error).message + }`; + } + + const appJsonPath = path.join(dest, '/nativefier.json'); + const pickedOptions = pickElectronAppArgs(options); + log.debug(`Writing app config to ${appJsonPath}`, pickedOptions); + await fs.writeFile(appJsonPath, JSON.stringify(pickedOptions)); + + if (options.nativefier.bookmarksMenu) { + const bookmarksJsonPath = path.join(dest, '/bookmarks.json'); + try { + await fs.copy(options.nativefier.bookmarksMenu, bookmarksJsonPath); + } catch (err: unknown) { + log.error('Error copying bookmarks menu config file.', err); + } + } + + try { + await maybeCopyScripts(options.nativefier.inject, dest); + } catch (err: unknown) { + log.error('Error copying injection files.', err); + } + const normalizedAppName = changeAppPackageJsonName( + dest, + options.packager.name as string, + options.packager.targetUrl, + ); + options.packager.appBundleId = `com.electron.nativefier.${normalizedAppName}`; +} diff --git a/src/cli.test.ts b/src/cli.test.ts new file mode 100644 index 0000000..ef67b8b --- /dev/null +++ b/src/cli.test.ts @@ -0,0 +1,326 @@ +import 'source-map-support/register'; + +import { initArgs, parseArgs } from './cli'; +import { parseJson } from './utils/parseUtils'; + +describe('initArgs + parseArgs', () => { + let mockExit: jest.SpyInstance; + + beforeEach(() => { + mockExit = jest.spyOn(process, 'exit').mockImplementation(); + }); + + afterEach(() => { + mockExit.mockRestore(); + }); + + test('--help forces exit', () => { + // Mock console.log to not pollute the log with the yargs help text + const mockLog = jest.spyOn(console, 'log').mockImplementation(); + initArgs(['https://www.google.com', '--help']); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockLog).toBeCalled(); + mockLog.mockRestore(); + }); + + test('--version forces exit', () => { + // Mock console.log to not pollute the log with the yargs help text + const mockLog = jest.spyOn(console, 'log').mockImplementation(); + initArgs(['https://www.google.com', '--version']); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockLog).toBeCalled(); + mockLog.mockRestore(); + }); + + // Positional options + + test('first positional becomes targetUrl', () => { + const args = parseArgs(initArgs(['https://google.com'])); + expect(args.targetUrl).toBe('https://google.com'); + expect(args.upgrade).toBeUndefined(); + }); + + test('second positional becomes out', () => { + const args = parseArgs(initArgs(['https://google.com', 'tmp'])); + expect(args.out).toBe('tmp'); + expect(args.targetUrl).toBe('https://google.com'); + expect(args.upgrade).toBeUndefined(); + }); + + // App Creation Options + test('upgrade arg', () => { + const args = parseArgs(initArgs(['--upgrade', 'pathToUpgrade'])); + expect(args.upgrade).toBe('pathToUpgrade'); + expect(args.targetUrl).toBeUndefined(); + }); + + test('upgrade arg with out dir', () => { + const args = parseArgs(initArgs(['tmp', '--upgrade', 'pathToUpgrade'])); + expect(args.upgrade).toBe('pathToUpgrade'); + expect(args.out).toBe('tmp'); + expect(args.targetUrl).toBeUndefined(); + }); + + test('upgrade arg with targetUrl', () => { + expect(() => + parseArgs( + initArgs(['https://www.google.com', '--upgrade', 'path/to/upgrade']), + ), + ).toThrow(); + }); + + test('multi-inject', () => { + const args = parseArgs( + initArgs([ + 'https://google.com', + '--inject', + 'test.js', + '--inject', + 'test2.js', + '--inject', + 'test.css', + '--inject', + 'test2.css', + ]), + ); + expect(args.inject).toEqual([ + 'test.js', + 'test2.js', + 'test.css', + 'test2.css', + ]); + }); + + test.each([ + { arg: 'app-copyright', shortArg: '', value: '(c) Nativefier' }, + { arg: 'app-version', shortArg: '', value: '2.0.0' }, + { arg: 'background-color', shortArg: '', value: '#FFAA88' }, + { arg: 'basic-auth-username', shortArg: '', value: 'user' }, + { arg: 'basic-auth-password', shortArg: '', value: 'p@ssw0rd' }, + { arg: 'bookmarks-menu', shortArg: '', value: 'bookmarks.json' }, + { + arg: 'browserwindow-options', + shortArg: '', + value: '{"test": 456}', + isJsonString: true, + }, + { arg: 'build-version', shortArg: '', value: '3.0.0' }, + { + arg: 'crash-reporter', + shortArg: '', + value: 'https://crash-reporter.com', + }, + { arg: 'electron-version', shortArg: 'e', value: '1.0.0' }, + { + arg: 'file-download-options', + shortArg: '', + value: '{"test": 789}', + isJsonString: true, + }, + { arg: 'flash-path', shortArg: '', value: 'pathToFlash' }, + { arg: 'global-shortcuts', shortArg: '', value: 'shortcuts.json' }, + { arg: 'icon', shortArg: 'i', value: 'icon.png' }, + { arg: 'internal-urls', shortArg: '', value: '.*' }, + { arg: 'lang', shortArg: '', value: 'fr' }, + { arg: 'name', shortArg: 'n', value: 'Google' }, + { + arg: 'process-envs', + shortArg: '', + value: '{"test": 123}', + isJsonString: true, + }, + { arg: 'proxy-rules', shortArg: '', value: 'RULE: PROXY' }, + { arg: 'tray', shortArg: '', value: 'true' }, + { arg: 'user-agent', shortArg: 'u', value: 'FIREFOX' }, + { + arg: 'win32metadata', + shortArg: '', + value: '{"ProductName": "Google"}', + isJsonString: true, + }, + ])('test string arg %s', ({ arg, shortArg, value, isJsonString }) => { + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`, value]), + ) as unknown as Record; + if (!isJsonString) { + expect(args[arg]).toBe(value); + } else { + expect(args[arg]).toEqual(parseJson(value)); + } + + if (shortArg) { + const argsShort = parseArgs( + initArgs(['https://google.com', `-${shortArg}`, value]), + ) as unknown as Record; + if (!isJsonString) { + expect(argsShort[arg]).toBe(value); + } else { + expect(argsShort[arg]).toEqual(parseJson(value)); + } + } + }); + + test.each([ + { arg: 'arch', shortArg: 'a', value: 'x64', badValue: '486' }, + { arg: 'platform', shortArg: 'p', value: 'mac', badValue: 'os2' }, + { + arg: 'title-bar-style', + shortArg: '', + value: 'hidden', + badValue: 'cool', + }, + ])('limited choice arg %s', ({ arg, shortArg, value, badValue }) => { + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`, value]), + ) as unknown as Record; + expect(args[arg]).toBe(value); + + // Mock console.error to not pollute the log with the yargs help text + const mockError = jest.spyOn(console, 'error').mockImplementation(); + initArgs(['https://google.com', `--${arg}`, badValue]); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockError).toBeCalled(); + mockExit.mockClear(); + mockError.mockClear(); + + if (shortArg) { + const argsShort = parseArgs( + initArgs(['https://google.com', `-${shortArg}`, value]), + ) as unknown as Record; + expect(argsShort[arg]).toBe(value); + + initArgs(['https://google.com', `-${shortArg}`, badValue]); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockError).toBeCalled(); + } + mockError.mockRestore(); + }); + + test.each([ + { arg: 'always-on-top', shortArg: '' }, + { arg: 'block-external-urls', shortArg: '' }, + { arg: 'bounce', shortArg: '' }, + { arg: 'clear-cache', shortArg: '' }, + { arg: 'conceal', shortArg: 'c' }, + { arg: 'counter', shortArg: '' }, + { arg: 'darwin-dark-mode-support', shortArg: '' }, + { arg: 'disable-context-menu', shortArg: '' }, + { arg: 'disable-dev-tools', shortArg: '' }, + { arg: 'disable-gpu', shortArg: '' }, + { arg: 'disable-old-build-warning-yesiknowitisinsecure', shortArg: '' }, + { arg: 'enable-es3-apis', shortArg: '' }, + { arg: 'fast-quit', shortArg: 'f' }, + { arg: 'flash', shortArg: '' }, + { arg: 'full-screen', shortArg: '' }, + { arg: 'hide-window-frame', shortArg: '' }, + { arg: 'honest', shortArg: '' }, + { arg: 'ignore-certificate', shortArg: '' }, + { arg: 'ignore-gpu-blacklist', shortArg: '' }, + { arg: 'insecure', shortArg: '' }, + { arg: 'maximize', shortArg: '' }, + { arg: 'portable', shortArg: '' }, + { arg: 'show-menu-bar', shortArg: 'm' }, + { arg: 'single-instance', shortArg: '' }, + { arg: 'strict-internal-urls', shortArg: '' }, + { arg: 'verbose', shortArg: '' }, + { arg: 'widevine', shortArg: '' }, + ])('test boolean arg %s', ({ arg, shortArg }) => { + const defaultArgs = parseArgs( + initArgs(['https://google.com']), + ) as unknown as Record; + expect(defaultArgs[arg]).toBe(false); + + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`]), + ) as unknown as Record; + expect(args[arg]).toBe(true); + if (shortArg) { + const argsShort = parseArgs( + initArgs(['https://google.com', `-${shortArg}`]), + ) as unknown as Record; + expect(argsShort[arg]).toBe(true); + } + }); + + test.each([{ arg: 'no-overwrite', shortArg: '' }])( + 'test inversible boolean arg %s', + ({ arg, shortArg }) => { + const inverse = arg.startsWith('no-') ? arg.substr(3) : `no-${arg}`; + + const defaultArgs = parseArgs( + initArgs(['https://google.com']), + ) as unknown as Record; + expect(defaultArgs[arg]).toBe(false); + expect(defaultArgs[inverse]).toBe(true); + + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`]), + ) as unknown as Record; + expect(args[arg]).toBe(true); + expect(args[inverse]).toBe(false); + + if (shortArg) { + const argsShort = parseArgs( + initArgs(['https://google.com', `-${shortArg}`]), + ) as unknown as Record; + expect(argsShort[arg]).toBe(true); + expect(argsShort[inverse]).toBe(true); + } + }, + ); + + test.each([ + { arg: 'disk-cache-size', shortArg: '', value: 100 }, + { arg: 'height', shortArg: '', value: 200 }, + { arg: 'max-height', shortArg: '', value: 300 }, + { arg: 'max-width', shortArg: '', value: 400 }, + { arg: 'min-height', shortArg: '', value: 500 }, + { arg: 'min-width', shortArg: '', value: 600 }, + { arg: 'width', shortArg: '', value: 700 }, + { arg: 'x', shortArg: '', value: 800 }, + { arg: 'y', shortArg: '', value: 900 }, + ])('test numeric arg %s', ({ arg, shortArg, value }) => { + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`, `${value}`]), + ) as unknown as Record; + expect(args[arg]).toBe(value); + + const badArgs = parseArgs( + initArgs(['https://google.com', `--${arg}`, 'abcd']), + ) as unknown as Record; + expect(badArgs[arg]).toBeNaN(); + + if (shortArg) { + const shortArgs = parseArgs( + initArgs(['https://google.com', `-${shortArg}`, `${value}`]), + ) as unknown as Record; + expect(shortArgs[arg]).toBe(value); + + const badShortArgs = parseArgs( + initArgs(['https://google.com', `-${shortArg}`, 'abcd']), + ) as unknown as Record; + expect(badShortArgs[arg]).toBeNaN(); + } + }); + + test.each([ + { arg: 'tray', value: 'true' }, + { arg: 'tray', value: 'false' }, + { arg: 'tray', value: 'start-in-tray' }, + { arg: 'tray', value: '' }, + ])('test tray valyue %s', ({ arg, value }) => { + const args = parseArgs( + initArgs(['https://google.com', `--${arg}`, `${value}`]), + ) as unknown as Record; + if (value !== '') { + expect(args[arg]).toBe(value); + } else { + expect(args[arg]).toBe('true'); + } + }); + + test('test tray value defaults to false', () => { + const args = parseArgs(initArgs(['https://google.com'])); + expect(args.tray).toBe('false'); + }); +}); diff --git a/src/cli.ts b/src/cli.ts new file mode 100755 index 0000000..718d6ee --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,705 @@ +#!/usr/bin/env node +import 'source-map-support/register'; + +import electronPackager = require('electron-packager'); +import * as log from 'loglevel'; +import yargs from 'yargs'; + +import { DEFAULT_ELECTRON_VERSION } from './constants'; +import { + camelCased, + checkInternet, + getProcessEnvs, + isArgFormatInvalid, +} from './helpers/helpers'; +import { supportedArchs, supportedPlatforms } from './infer/inferOs'; +import { buildNativefierApp } from './main'; +import { RawOptions } from '../shared/src/options/model'; +import { parseJson } from './utils/parseUtils'; + +// @types/yargs@17.x started pretending yargs.argv can be a promise: +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8e17f9ca957a06040badb53ae7688fbb74229ccf/types/yargs/index.d.ts#L73 +// Dunno in which case it happens, but it doesn't for us! So, having to await +// (and end up having to flag sync code as async) would be useless and annoying. +// So, copy-pastaing and axing the Promise half of yargs's type definition, +// to have a *non*-promise type. Maybe that's wrong. If it is, this type should +// be dropped, and extra async-ness should be added where needed. +type YargsArgvSync = { + [key in keyof yargs.Arguments as + | key + | yargs.CamelCaseKey]: yargs.Arguments[key]; +}; + +export function initArgs(argv: string[]): yargs.Argv { + const sanitizedArgs = sanitizeArgs(argv); + const args = yargs(sanitizedArgs) + .scriptName('nativefier') + .usage( + '$0 [outputDirectory] [other options]\nor\n$0 --upgrade [other options]', + ) + .example( + '$0 -n ', + 'Make an app from and set the application name to ', + ) + .example( + '$0 --upgrade ', + 'Upgrade (in place) the existing Nativefier app at ', + ) + .example( + '$0 -p -a ', + 'Make an app from for the OS and CPU architecture ', + ) + .example( + 'for more examples and help...', + 'See https://github.com/nativefier/nativefier/blob/master/CATALOG.md', + ) + .positional('targetUrl', { + description: + 'the URL that you wish to to turn into a native app; required if not using --upgrade', + type: 'string', + }) + .positional('outputDirectory', { + defaultDescription: + 'defaults to the current directory, or env. var. NATIVEFIER_APPS_DIR if set', + description: 'the directory to generate the app in', + normalize: true, + type: 'string', + }) + // App Creation Options + .option('a', { + alias: 'arch', + choices: supportedArchs, + defaultDescription: "current Node's arch", + description: 'the CPU architecture to build for', + type: 'string', + }) + .option('c', { + alias: 'conceal', + default: false, + description: 'package the app source code into an asar archive', + type: 'boolean', + }) + .option('e', { + alias: 'electron-version', + defaultDescription: DEFAULT_ELECTRON_VERSION, + description: + "specify the electron version to use (without the 'v'); see https://github.com/electron/electron/releases", + }) + .option('global-shortcuts', { + description: + 'define global keyboard shortcuts via a JSON file; See https://github.com/nativefier/nativefier/blob/master/API.md#global-shortcuts', + normalize: true, + type: 'string', + }) + .option('i', { + alias: 'icon', + description: + 'the icon file to use as the icon for the app (.ico on Windows, .icns/.png on macOS, .png on Linux)', + normalize: true, + type: 'string', + }) + .option('n', { + alias: 'name', + defaultDescription: 'the title of the page passed via targetUrl', + description: 'specify the name of the app', + type: 'string', + }) + .option('no-overwrite', { + default: false, + description: 'do not overwrite output directory if it already exists', + type: 'boolean', + }) + .option('overwrite', { + // This is needed to have the `no-overwrite` flag to work correctly + default: true, + hidden: true, + type: 'boolean', + }) + .option('p', { + alias: 'platform', + choices: supportedPlatforms, + defaultDescription: 'current operating system', + description: 'the operating system platform to build for', + type: 'string', + }) + .option('portable', { + default: false, + description: + 'make the app store its user data in the app folder; WARNING: see https://github.com/nativefier/nativefier/blob/master/API.md#portable for security risks', + type: 'boolean', + }) + .option('upgrade', { + description: + 'upgrade an app built by an older version of Nativefier\nYou must pass the full path to the existing app executable (app will be overwritten with upgraded version by default)', + normalize: true, + type: 'string', + }) + .option('widevine', { + default: false, + description: + "use a Widevine-enabled version of Electron for DRM playback (use at your own risk, it's unofficial, provided by CastLabs)", + type: 'boolean', + }) + .group( + [ + 'arch', + 'conceal', + 'electron-version', + 'global-shortcuts', + 'icon', + 'name', + 'no-overwrite', + 'platform', + 'portable', + 'upgrade', + 'widevine', + ], + decorateYargOptionGroup('App Creation Options'), + ) + // App Window Options + .option('always-on-top', { + default: false, + description: 'enable always on top window', + type: 'boolean', + }) + .option('background-color', { + description: + "set the app background color, for better integration while the app is loading. Example value: '#2e2c29'", + type: 'string', + }) + .option('bookmarks-menu', { + description: + 'create a bookmarks menu (via JSON file); See https://github.com/nativefier/nativefier/blob/master/API.md#bookmarks-menu', + normalize: true, + type: 'string', + }) + .option('browserwindow-options', { + coerce: parseJson, + description: + 'override Electron BrowserWindow options (via JSON string); see https://github.com/nativefier/nativefier/blob/master/API.md#browserwindow-options', + }) + .option('disable-context-menu', { + default: false, + description: 'disable the context menu (right click)', + type: 'boolean', + }) + .option('disable-dev-tools', { + default: false, + description: 'disable developer tools (Ctrl+Shift+I / F12)', + type: 'boolean', + }) + .option('full-screen', { + default: false, + description: 'always start the app full screen', + type: 'boolean', + }) + .option('height', { + defaultDescription: '800', + description: 'set window default height in pixels', + type: 'number', + }) + .option('hide-window-frame', { + default: false, + description: 'disable window frame and controls', + type: 'boolean', + }) + .option('m', { + alias: 'show-menu-bar', + default: false, + description: 'set menu bar visible', + type: 'boolean', + }) + .option('max-height', { + defaultDescription: 'unlimited', + description: 'set window maximum height in pixels', + type: 'number', + }) + .option('max-width', { + defaultDescription: 'unlimited', + description: 'set window maximum width in pixels', + type: 'number', + }) + .option('maximize', { + default: false, + description: 'always start the app maximized', + type: 'boolean', + }) + .option('min-height', { + defaultDescription: '0', + description: 'set window minimum height in pixels', + type: 'number', + }) + .option('min-width', { + defaultDescription: '0', + description: 'set window minimum width in pixels', + type: 'number', + }) + .option('process-envs', { + coerce: getProcessEnvs, + description: + 'a JSON string of key/value pairs to be set as environment variables before any browser windows are opened', + }) + .option('single-instance', { + default: false, + description: 'allow only a single instance of the app', + type: 'boolean', + }) + .option('tray', { + default: 'false', + description: + "allow app to stay in system tray. If 'start-in-tray' is set as argument, don't show main window on first start", + choices: ['true', 'false', 'start-in-tray'], + }) + .option('width', { + defaultDescription: '1280', + description: 'app window default width in pixels', + type: 'number', + }) + .option('x', { + description: 'set window x location in pixels from left', + type: 'number', + }) + .option('y', { + description: 'set window y location in pixels from top', + type: 'number', + }) + .option('zoom', { + default: 1.0, + description: 'set the default zoom factor for the app', + type: 'number', + }) + .group( + [ + 'always-on-top', + 'background-color', + 'bookmarks-menu', + 'browserwindow-options', + 'disable-context-menu', + 'disable-dev-tools', + 'full-screen', + 'height', + 'hide-window-frame', + 'm', + 'max-width', + 'max-height', + 'maximize', + 'min-height', + 'min-width', + 'process-envs', + 'single-instance', + 'tray', + 'width', + 'x', + 'y', + 'zoom', + ], + decorateYargOptionGroup('App Window Options'), + ) + // Internal Browser Options + .option('file-download-options', { + coerce: parseJson, + description: + 'a JSON string defining file download options; see https://github.com/sindresorhus/electron-dl', + }) + .option('inject', { + description: + 'path to a CSS/JS file to be injected; pass multiple times to inject multiple files', + string: true, + type: 'array', + }) + .option('lang', { + defaultDescription: 'os language at runtime of the app', + description: + 'set the language or locale to render the web site as (e.g., "fr", "en-US", "es", etc.)', + type: 'string', + }) + .option('u', { + alias: 'user-agent', + description: + "set the app's user agent string; may also use 'edge', 'firefox', or 'safari' to have one auto-generated", + type: 'string', + }) + .option('user-agent-honest', { + alias: 'honest', + default: false, + description: + 'prevent the normal changing of the user agent string to appear as a regular Chrome browser', + type: 'boolean', + }) + .group( + [ + 'file-download-options', + 'inject', + 'lang', + 'user-agent', + 'user-agent-honest', + ], + decorateYargOptionGroup('Internal Browser Options'), + ) + // Internal Browser Cache Options + .option('clear-cache', { + default: false, + description: 'prevent the app from preserving cache between launches', + type: 'boolean', + }) + .option('disk-cache-size', { + defaultDescription: 'chromium default', + description: + 'set the maximum disk space (in bytes) to be used by the disk cache', + type: 'number', + }) + .group( + ['clear-cache', 'disk-cache-size'], + decorateYargOptionGroup('Internal Browser Cache Options'), + ) + // URL Handling Options + .option('block-external-urls', { + default: false, + description: `forbid navigation to URLs not considered "internal" (see '--internal-urls'). Instead of opening in an external browser, attempts to navigate to external URLs will be blocked`, + type: 'boolean', + }) + .option('internal-urls', { + defaultDescription: 'URLs sharing the same base domain', + description: `regex of URLs to consider "internal"; by default matches based on domain (see '--strict-internal-urls'); all other URLs will be opened in an external browser`, + type: 'string', + }) + .option('strict-internal-urls', { + default: false, + description: 'disable domain-based matching on internal URLs', + type: 'boolean', + }) + .option('proxy-rules', { + description: + 'proxy rules; see https://www.electronjs.org/docs/api/session#sessetproxyconfig', + type: 'string', + }) + .group( + [ + 'block-external-urls', + 'internal-urls', + 'strict-internal-urls', + 'proxy-rules', + ], + decorateYargOptionGroup('URL Handling Options'), + ) + // Auth Options + .option('basic-auth-password', { + description: 'basic http(s) auth password', + type: 'string', + }) + .option('basic-auth-username', { + description: 'basic http(s) auth username', + type: 'string', + }) + .group( + ['basic-auth-password', 'basic-auth-username'], + decorateYargOptionGroup('Auth Options'), + ) + // Graphics Options + .option('disable-gpu', { + default: false, + description: 'disable hardware acceleration', + type: 'boolean', + }) + .option('enable-es3-apis', { + default: false, + description: 'force activation of WebGL 2.0', + type: 'boolean', + }) + .option('ignore-gpu-blacklist', { + default: false, + description: 'force WebGL apps to work on unsupported GPUs', + type: 'boolean', + }) + .group( + ['disable-gpu', 'enable-es3-apis', 'ignore-gpu-blacklist'], + decorateYargOptionGroup('Graphics Options'), + ) + // (In)Security Options + .option('disable-old-build-warning-yesiknowitisinsecure', { + default: false, + description: + 'disable warning shown when opening an app made too long ago; Nativefier uses the Chrome browser (through Electron), and it is dangerous to keep using an old version of it', + type: 'boolean', + }) + .option('ignore-certificate', { + default: false, + description: 'ignore certificate-related errors', + type: 'boolean', + }) + .option('insecure', { + default: false, + description: 'enable loading of insecure content', + type: 'boolean', + }) + .group( + [ + 'disable-old-build-warning-yesiknowitisinsecure', + 'ignore-certificate', + 'insecure', + ], + decorateYargOptionGroup('(In)Security Options'), + ) + // Flash Options (DEPRECATED) + .option('flash', { + default: false, + deprecated: true, + description: 'enable Adobe Flash', + hidden: true, + type: 'boolean', + }) + .option('flash-path', { + deprecated: true, + description: 'path to Chrome flash plugin; find it in `chrome://plugins`', + hidden: true, + normalize: true, + type: 'string', + }) + // Platform Specific Options + .option('app-copyright', { + description: + '(macOS, windows only) set a human-readable copyright line for the app; maps to `LegalCopyright` metadata property on Windows, and `NSHumanReadableCopyright` on macOS', + type: 'string', + }) + .option('app-version', { + description: + '(macOS, windows only) set the version of the app; maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on macOS', + type: 'string', + }) + .option('bounce', { + default: false, + description: + '(macOS only) make the dock icon bounce when the counter increases', + type: 'boolean', + }) + .option('build-version', { + description: + '(macOS, windows only) set the build version of the app; maps to `FileVersion` metadata property on Windows, and `CFBundleVersion` on macOS', + type: 'string', + }) + .option('counter', { + default: false, + description: + '(macOS only) set a dock count badge, determined by looking for a number in the window title', + type: 'boolean', + }) + .option('darwin-dark-mode-support', { + default: false, + description: '(macOS only) enable Dark Mode support on macOS 10.14+', + type: 'boolean', + }) + .option('f', { + alias: 'fast-quit', + default: false, + description: '(macOS only) quit app on window close', + type: 'boolean', + }) + .option('title-bar-style', { + choices: ['hidden', 'hiddenInset'], + description: + '(macOS only) set title bar style; consider injecting custom CSS (via --inject) for better integration', + type: 'string', + }) + .option('win32metadata', { + coerce: (value: string) => + parseJson(value), + description: + '(windows only) a JSON string of key/value pairs (ProductName, InternalName, FileDescription) to embed as executable metadata', + }) + .group( + [ + 'app-copyright', + 'app-version', + 'bounce', + 'build-version', + 'counter', + 'darwin-dark-mode-support', + 'fast-quit', + 'title-bar-style', + 'win32metadata', + ], + decorateYargOptionGroup('Platform-Specific Options'), + ) + // Debug Options + .option('crash-reporter', { + description: 'remote server URL to send crash reports', + type: 'string', + }) + .option('verbose', { + default: false, + description: 'enable verbose/debug/troubleshooting logs', + type: 'boolean', + }) + .option('quiet', { + default: false, + description: 'suppress all logging', + type: 'boolean', + }) + .group( + ['crash-reporter', 'verbose', 'quiet'], + decorateYargOptionGroup('Debug Options'), + ) + .version() + .help() + .group(['version', 'help'], 'Other Options') + .wrap(yargs.terminalWidth()); + + // We must access argv in order to get yargs to actually process args + // Do this now to go ahead and get any errors out of the way + args.argv as YargsArgvSync; + + return args as yargs.Argv; +} + +function decorateYargOptionGroup(value: string): string { + return `====== ${value} ======`; +} + +export function parseArgs(args: yargs.Argv): RawOptions { + const parsed = { ...(args.argv as YargsArgvSync) }; + // In yargs, the _ property of the parsed args is an array of the positional args + // https://github.com/yargs/yargs/blob/master/docs/examples.md#and-non-hyphenated-options-too-just-use-argv_ + // So try to extract the targetUrl and outputDirectory from these + parsed.targetUrl = parsed._.length > 0 ? parsed._[0].toString() : undefined; + parsed.out = parsed._.length > 1 ? (parsed._[1] as string) : undefined; + + if (parsed.upgrade && parsed.targetUrl) { + let targetAndUpgrade = false; + if (!parsed.out) { + // If we're upgrading, the first positional args might be the outputDirectory, so swap these if we can + try { + // If this succeeds, we have a problem + new URL(parsed.targetUrl); + targetAndUpgrade = true; + } catch { + // Cool, it's not a URL + parsed.out = parsed.targetUrl; + parsed.targetUrl = undefined; + } + } else { + // Someone supplied a targetUrl, an outputDirectory, and --upgrade. That's not cool. + targetAndUpgrade = true; + } + + if (targetAndUpgrade) { + throw new Error( + 'ERROR: Nativefier must be called with either a targetUrl or the --upgrade option, not both.\n', + ); + } + } + + if (!parsed.targetUrl && !parsed.upgrade) { + throw new Error( + 'ERROR: Nativefier must be called with either a targetUrl or the --upgrade option.\n', + ); + } + + parsed.noOverwrite = parsed['no-overwrite'] = !parsed.overwrite; + + // Since coerce in yargs seems to have broken since + // https://github.com/yargs/yargs/pull/1978 + for (const arg of [ + 'win32metadata', + 'browserwindow-options', + 'file-download-options', + ]) { + if (parsed[arg] && typeof parsed[arg] === 'string') { + parsed[arg] = parseJson(parsed[arg] as string); + // sets fileDownloadOptions and browserWindowOptions + // as parsed object as they were still strings in `nativefier.json` + // because only their snake-cased variants were being parsed above + parsed[camelCased(arg)] = parsed[arg]; + } + } + if (parsed['process-envs'] && typeof parsed['process-envs'] === 'string') { + parsed['process-envs'] = getProcessEnvs(parsed['process-envs']); + } + + return parsed; +} + +function sanitizeArgs(argv: string[]): string[] { + const sanitizedArgs: string[] = []; + argv.forEach((arg) => { + if (isArgFormatInvalid(arg)) { + throw new Error( + `Invalid argument passed: ${arg} .\nNativefier supports short options (like "-n") and long options (like "--name"), all lowercase. Run "nativefier --help" for help.\nAborting`, + ); + } + const isLastArg = sanitizedArgs.length + 1 === argv.length; + if (sanitizedArgs.length > 0) { + const previousArg = sanitizedArgs[sanitizedArgs.length - 1]; + + log.debug({ arg, previousArg, isLastArg }); + + // Work around commander.js not supporting default argument for options + if ( + previousArg === '--tray' && + !['true', 'false', 'start-in-tray'].includes(arg) + ) { + sanitizedArgs.push('true'); + } + } + sanitizedArgs.push(arg); + + if (arg === '--tray' && isLastArg) { + // Add a true if --tray is last so it gets enabled + sanitizedArgs.push('true'); + } + }); + + return sanitizedArgs; +} + +if (require.main === module) { + let args: yargs.Argv | undefined = undefined; + let parsedArgs: RawOptions; + try { + args = initArgs(process.argv.slice(2)); + parsedArgs = parseArgs(args); + } catch (err: unknown) { + if (args) { + log.error(err); + args.showHelp(); + } else { + log.error('Failed to parse command-line arguments. Aborting.', err); + } + process.exit(1); + } + + const options: RawOptions = { + ...parsedArgs, + }; + + if (options.verbose) { + log.setLevel('trace'); + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + require('debug').enable('electron-packager'); + } catch (err: unknown) { + log.debug( + 'Failed to enable electron-packager debug output. This should not happen,', + 'and suggests their internals changed. Please report an issue.', + ); + } + + log.debug( + 'Running in verbose mode! This will produce a mountain of logs and', + 'is recommended only for troubleshooting or if you like Shakespeare.', + ); + } else if (options.quiet) { + log.setLevel('silent'); + } else { + log.setLevel('info'); + } + + checkInternet(); + + if (!options.out && process.env.NATIVEFIER_APPS_DIR) { + options.out = process.env.NATIVEFIER_APPS_DIR; + } + + buildNativefierApp(options).catch((error) => { + log.error('Error during build. Run with --verbose for details.', error); + }); +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..1ff0934 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; + +export const DEFAULT_APP_NAME = 'APP'; + +// Upgrade both DEFAULT_ELECTRON_VERSION and DEFAULT_CHROME_VERSION together, and +// - upgrade app / package.json / "devDependencies" / "electron" +// - upgrade package.json / "devDependencies" / "electron" +// Doing a *major* upgrade? Read https://github.com/nativefier/nativefier/blob/master/HACKING.md#deps-major-upgrading-electron +export const DEFAULT_ELECTRON_VERSION = '25.7.0'; +// https://atom.io/download/atom-shell/index.json +// https://www.electronjs.org/releases/stable +export const DEFAULT_CHROME_VERSION = '114.0.5735.289'; + +// Update each of these periodically +// https://product-details.mozilla.org/1.0/firefox_versions.json +export const DEFAULT_FIREFOX_VERSION = '116.0.3'; + +// https://en.wikipedia.org/wiki/Safari_version_history +export const DEFAULT_SAFARI_VERSION = { + majorVersion: 16, + version: '16.6', + webkitVersion: '605.1.15', +}; + +export const ELECTRON_MAJOR_VERSION = parseInt( + DEFAULT_ELECTRON_VERSION.split('.')[0], + 10, +); +export const PLACEHOLDER_APP_DIR = path.join(__dirname, './../', 'app'); diff --git a/src/helpers/fsHelpers.ts b/src/helpers/fsHelpers.ts new file mode 100644 index 0000000..eb9397c --- /dev/null +++ b/src/helpers/fsHelpers.ts @@ -0,0 +1,19 @@ +import * as fs from 'fs'; + +export function dirExists(dirName: string): boolean { + try { + const dirStat = fs.statSync(dirName); + return dirStat.isDirectory(); + } catch { + return false; + } +} + +export function fileExists(fileName: string): boolean { + try { + const fileStat = fs.statSync(fileName); + return fileStat.isFile(); + } catch { + return false; + } +} diff --git a/src/helpers/helpers.test.ts b/src/helpers/helpers.test.ts new file mode 100644 index 0000000..0b9a9c1 --- /dev/null +++ b/src/helpers/helpers.test.ts @@ -0,0 +1,84 @@ +import { + isArgFormatInvalid, + generateRandomSuffix, + camelCased, +} from './helpers'; + +describe('isArgFormatInvalid', () => { + test('is false for correct short args', () => { + expect(isArgFormatInvalid('-t')).toBe(false); + }); + + test('is true for improperly double-dashed short args', () => { + expect(isArgFormatInvalid('--t')).toBe(true); + }); + + test('is false for --x and --y (backwards compat, we should have made these short, oh well)', () => { + expect(isArgFormatInvalid('--x')).toBe(false); + expect(isArgFormatInvalid('--y')).toBe(false); + }); + + test('is false for correct long args', () => { + expect(isArgFormatInvalid('--test')).toBe(false); + }); + + test('is true for improperly triple-dashed long args', () => { + expect(isArgFormatInvalid('---test')).toBe(true); + }); + + test('is true for improperly single-dashed long args', () => { + expect(isArgFormatInvalid('-test')).toBe(true); + }); + + test('is false for correct long args with dashes', () => { + expect(isArgFormatInvalid('--test-run')).toBe(false); + }); + + test('is false for correct long args with many dashes', () => { + expect(isArgFormatInvalid('--test-run-with-many-dashes')).toBe(false); + }); +}); + +describe('generateRandomSuffix', () => { + test('is not empty', () => { + expect(generateRandomSuffix()).not.toBe(''); + }); + + test('is not null', () => { + expect(generateRandomSuffix()).not.toBeNull(); + }); + + test('is not undefined', () => { + expect(generateRandomSuffix()).toBeDefined(); + }); + + test('is different per call', () => { + expect(generateRandomSuffix()).not.toBe(generateRandomSuffix()); + }); + + test('respects the length param', () => { + expect(generateRandomSuffix(10).length).toBe(10); + }); +}); + +describe('camelCased', () => { + test('has no hyphens in camel case', () => { + expect(camelCased('file-download')).toEqual(expect.not.stringMatching(/-/)); + }); + + test('returns camel cased string', () => { + expect(camelCased('file-download')).toBe('fileDownload'); + }); + + test('has no spaces in camel case', () => { + expect(camelCased('--file--download--')).toBe('fileDownload'); + }); + + test('handles multiple hyphens properly', () => { + expect(camelCased('file--download--options')).toBe('fileDownloadOptions'); + }); + + test('does not affect non-snake cased strings', () => { + expect(camelCased('win32options')).toBe('win32options'); + }); +}); diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts new file mode 100644 index 0000000..63bc289 --- /dev/null +++ b/src/helpers/helpers.ts @@ -0,0 +1,211 @@ +import { spawnSync } from 'child_process'; +import * as crypto from 'crypto'; +import * as os from 'os'; +import * as path from 'path'; + +import axios from 'axios'; +import * as dns from 'dns'; +import * as hasbin from 'hasbin'; +import * as log from 'loglevel'; +import * as tmp from 'tmp'; + +import { parseJson } from '../utils/parseUtils'; + +tmp.setGracefulCleanup(); // cleanup temp dirs even when an uncaught exception occurs + +const now = new Date(); +const TMP_TIME = `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`; + +export type DownloadResult = { + data: Buffer; + ext: string; +}; + +type ProcessEnvs = Record; + +export function hasWine(): boolean { + return hasbin.sync('wine'); +} + +// I tried to place this (and the other is* functions) in +// a new shared helpers, but alas eslint gets real confused +// about the type signatures and thinks they're all any. +// TODO: Figure out a way to refactor duplicate code from +// src/helpers/helpers.ts and app/src/helpers/helpers.ts +// into the shared module + +export function isLinux(): boolean { + return os.platform() === 'linux'; +} + +export function isOSX(): boolean { + return os.platform() === 'darwin'; +} + +export function isWindows(): boolean { + return os.platform() === 'win32'; +} + +export function isWindowsAdmin(): boolean { + if (process.platform !== 'win32') { + return false; + } + + // https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights + // https://stackoverflow.com/questions/57009374/check-admin-or-non-admin-users-in-nodejs-or-javascript + return spawnSync('fltmc').status === 0; +} + +/** + * Create a temp directory with a debug-friendly name, and return its path. + * Will be automatically deleted on exit. + */ +export function getTempDir(prefix: string, mode?: number): string { + return tmp.dirSync({ + mode, + unsafeCleanup: true, // recursively remove tmp dir on exit, even if not empty. + prefix: `nativefier-${TMP_TIME}-${prefix}-`, + }).name; +} + +export function downloadFile( + fileUrl: string, +): Promise { + log.debug(`Downloading ${fileUrl}`); + return axios + .get(fileUrl, { + responseType: 'arraybuffer', + }) + .then((response) => { + if (!response.data) { + return undefined; + } + return { + data: response.data, + ext: path.extname(fileUrl), + }; + }); +} + +export function getAllowedIconFormats(platform: string): string[] { + const hasIdentify = hasbin.sync('identify') || hasbin.sync('gm'); + const hasConvert = hasbin.sync('convert') || hasbin.sync('gm'); + const hasIconUtil = hasbin.sync('iconutil'); + + const pngToIcns = hasConvert && hasIconUtil; + const pngToIco = hasConvert; + const icoToIcns = pngToIcns && hasIdentify; + const icoToPng = hasConvert; + + // Unsupported + const icnsToPng = false; + const icnsToIco = false; + + const formats: string[] = []; + + // Shell scripting is not supported on windows, temporary override + if (isWindows()) { + switch (platform) { + case 'darwin': + formats.push('.icns'); + break; + case 'linux': + formats.push('.png'); + break; + case 'win32': + formats.push('.ico'); + break; + default: + throw new Error(`Unknown platform ${platform}`); + } + log.debug( + `Allowed icon formats when building for ${platform} (limited on Windows):`, + formats, + ); + return formats; + } + + switch (platform) { + case 'darwin': + formats.push('.icns'); + if (pngToIcns) { + formats.push('.png'); + } + if (icoToIcns) { + formats.push('.ico'); + } + break; + case 'linux': + formats.push('.png'); + if (icoToPng) { + formats.push('.ico'); + } + if (icnsToPng) { + formats.push('.icns'); + } + break; + case 'win32': + formats.push('.ico'); + if (pngToIco) { + formats.push('.png'); + } + if (icnsToIco) { + formats.push('.icns'); + } + break; + default: + throw new Error(`Unknown platform ${platform}`); + } + log.debug(`Allowed icon formats when building for ${platform}:`, formats); + return formats; +} + +/** + * Refuse args like '--n' or '-name', we accept either short '-n' or long '--name' + */ +export function isArgFormatInvalid(arg: string): boolean { + return ( + (arg.startsWith('---') || + /^--[a-z]$/i.exec(arg) !== null || + /^-[a-z]{2,}$/i.exec(arg) !== null) && + !['--x', '--y'].includes(arg) // exception for long args --{x,y} + ); +} + +export function generateRandomSuffix(length = 6): string { + const hash = crypto.createHash('md5'); + // Add a random salt to help avoid collisions + hash.update(crypto.randomBytes(256)); + return hash.digest('hex').substring(0, length); +} + +export function getProcessEnvs(val: string): ProcessEnvs | undefined { + if (!val) { + return undefined; + } + return parseJson(val); +} + +export function checkInternet(): void { + dns.lookup('npmjs.com', (err) => { + if (err && err.code === 'ENOTFOUND') { + log.warn( + '\nNo Internet Connection\nTo offline build, download electron from https://github.com/electron/electron/releases\nand place in ~/AppData/Local/electron/Cache/ on Windows,\n~/.cache/electron on Linux or ~/Library/Caches/electron/ on Mac\nUse --electron-version to specify the version you downloaded.', + ); + } + }); +} + +/** + * Takes in a snake-cased string and converts to camelCase + */ +export function camelCased(str: string): string { + return str + .split('-') + .filter((s) => s.length > 0) + .map((word, i) => { + if (i === 0) return word; + return `${word[0].toUpperCase()}${word.substring(1)}`; + }) + .join(''); +} diff --git a/src/helpers/iconShellHelpers.ts b/src/helpers/iconShellHelpers.ts new file mode 100644 index 0000000..457548a --- /dev/null +++ b/src/helpers/iconShellHelpers.ts @@ -0,0 +1,100 @@ +import * as path from 'path'; +import { spawnSync } from 'child_process'; + +import { isWindows, isOSX, getTempDir } from './helpers'; +import * as log from 'loglevel'; + +const SCRIPT_PATHS = { + singleIco: path.join(__dirname, '../..', 'icon-scripts/singleIco'), + convertToPng: path.join(__dirname, '../..', 'icon-scripts/convertToPng'), + convertToIco: path.join(__dirname, '../..', 'icon-scripts/convertToIco'), + convertToIcns: path.join(__dirname, '../..', 'icon-scripts/convertToIcns'), + convertToTrayIcon: path.join( + __dirname, + '../..', + 'icon-scripts/convertToTrayIcon', + ), +}; + +/** + * Executes a shell script with the form "./pathToScript param1 param2" + */ +function iconShellHelper( + shellScriptPath: string, + icoSource: string, + icoDestination: string, +): string { + if (isWindows()) { + throw new Error( + 'Icon conversion only supported on macOS or Linux. ' + + 'If building for Windows, download/create a .ico and pass it with --icon favicon.ico . ' + + 'If building for macOS/Linux, do it from macOS/Linux', + ); + } + + const shellCommand = `"${shellScriptPath}" "${icoSource}" "${icoDestination}"`; + log.debug( + `Converting icon ${icoSource} to ${icoDestination}.`, + `Calling shell command: ${shellCommand}`, + ); + const { stdout, stderr, status } = spawnSync( + shellScriptPath, + [icoSource, icoDestination], + { timeout: 10000 }, + ); + if (status) { + throw new Error( + `Icon conversion failed with status code ${status}.\nstdout: ${stdout.toString()}\nstderr: ${stderr.toString()}`, + ); + } + log.debug(`Conversion succeeded and produced icon at ${icoDestination}`); + return icoDestination; +} + +export function singleIco(icoSrc: string): string { + return iconShellHelper( + SCRIPT_PATHS.singleIco, + icoSrc, + `${getTempDir('iconconv')}/icon.ico`, + ); +} + +export function convertToPng(icoSrc: string): string { + return iconShellHelper( + SCRIPT_PATHS.convertToPng, + icoSrc, + `${getTempDir('iconconv')}/icon.png`, + ); +} + +export function convertToIco(icoSrc: string): string { + return iconShellHelper( + SCRIPT_PATHS.convertToIco, + icoSrc, + `${getTempDir('iconconv')}/icon.ico`, + ); +} + +export function convertToIcns(icoSrc: string): string { + if (!isOSX()) { + throw new Error('macOS is required to convert to a .icns icon'); + } + + return iconShellHelper( + SCRIPT_PATHS.convertToIcns, + icoSrc, + `${getTempDir('iconconv')}/icon.icns`, + ); +} + +export function convertToTrayIcon(icoSrc: string): string { + if (!isOSX()) { + throw new Error('macOS is required to convert from a .icns icon'); + } + + return iconShellHelper( + SCRIPT_PATHS.convertToTrayIcon, + icoSrc, + `${path.dirname(icoSrc)}/tray-icon.png`, + ); +} diff --git a/src/helpers/upgrade/executableHelpers.ts b/src/helpers/upgrade/executableHelpers.ts new file mode 100644 index 0000000..24d0bee --- /dev/null +++ b/src/helpers/upgrade/executableHelpers.ts @@ -0,0 +1,208 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import * as log from 'loglevel'; + +import { NativefierOptions } from '../../../shared/src/options/model'; +import { getVersionString } from './rceditGet'; +import { fileExists } from '../fsHelpers'; +type ExecutableInfo = { + arch?: string; +}; + +function getExecutableBytes(executablePath: string): Uint8Array { + return fs.readFileSync(executablePath); +} + +function getExecutableArch( + exeBytes: Uint8Array, + platform: string, +): string | undefined { + switch (platform) { + case 'linux': + // https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + switch (exeBytes[0x12]) { + case 0x03: + return 'ia32'; + case 0x28: + return 'armv7l'; + case 0x3e: + return 'x64'; + case 0xb7: + return 'arm64'; + default: + return undefined; + } + case 'darwin': + case 'mas': + // https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h + switch ((exeBytes[0x04] << 8) + exeBytes[0x05]) { + case 0x0700: + return 'x64'; + case 0x0c00: + return 'arm64'; + default: + return undefined; + } + case 'win32': + // https://en.wikibooks.org/wiki/X86_Disassembly/Windows_Executable_Files#COFF_Header + switch ((exeBytes[0x7d] << 8) + exeBytes[0x7c]) { + case 0x014c: + return 'ia32'; + case 0x8664: + return 'x64'; + case 0xaa64: + return 'arm64'; + default: + return undefined; + } + default: + return undefined; + } +} + +function getExecutableInfo( + executablePath: string, + platform: string, +): ExecutableInfo | undefined { + if (!fileExists(executablePath)) { + return undefined; + } + + const exeBytes = getExecutableBytes(executablePath); + return { + arch: getExecutableArch(exeBytes, platform), + }; +} + +export function getOptionsFromExecutable( + appResourcesDir: string, + priorOptions: NativefierOptions, +): NativefierOptions { + const newOptions: NativefierOptions = { ...priorOptions }; + if (!newOptions.name) { + throw new Error( + 'Can not extract options from executable with no name specified.', + ); + } + const name: string = newOptions.name; + let executablePath: string | undefined = undefined; + + const appRoot = path.resolve(path.join(appResourcesDir, '..', '..')); + const children = fs.readdirSync(appRoot, { withFileTypes: true }); + const looksLikeMacOS = + children.filter((c) => c.name === 'MacOS' && c.isDirectory()).length > 0; + const looksLikeWindows = + children.filter((c) => c.name.toLowerCase().endsWith('.exe') && c.isFile()) + .length > 0; + const looksLikeLinux = + children.filter((c) => c.name.toLowerCase().endsWith('.so') && c.isFile()) + .length > 0; + + if (looksLikeMacOS) { + log.debug('This looks like a MacOS app...'); + if (newOptions.platform === undefined) { + newOptions.platform = + children.filter((c) => c.name === 'Library' && c.isDirectory()).length > + 0 + ? 'mas' + : 'darwin'; + } + executablePath = path.join( + appRoot, + 'MacOS', + fs.readdirSync(path.join(appRoot, 'MacOS'))[0], + ); + } else if (looksLikeWindows) { + log.debug('This looks like a Windows app...'); + + if (newOptions.platform === undefined) { + newOptions.platform = 'win32'; + } + executablePath = path.join( + appRoot, + children.filter( + (c) => + c.name.toLowerCase() === `${name.toLowerCase()}.exe` && c.isFile(), + )[0].name, + ); + + if (newOptions.appVersion === undefined) { + // https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L46-L48 + newOptions.appVersion = getVersionString( + executablePath, + 'ProductVersion', + ); + log.debug( + `Extracted app version from executable: ${ + newOptions.appVersion as string + }`, + ); + } + + if (newOptions.buildVersion === undefined) { + //https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L50-L52 + newOptions.buildVersion = getVersionString(executablePath, 'FileVersion'); + + if (newOptions.appVersion == newOptions.buildVersion) { + newOptions.buildVersion = undefined; + } else { + log.debug( + `Extracted build version from executable: ${ + newOptions.buildVersion as string + }`, + ); + } + } + + if (newOptions.appCopyright === undefined) { + // https://github.com/electron/electron-packager/blob/f1c159f4c844d807968078ea504fba40ca7d9c73/src/win32.js#L54-L56 + newOptions.appCopyright = getVersionString( + executablePath, + 'LegalCopyright', + ); + log.debug( + `Extracted app copyright from executable: ${ + newOptions.appCopyright as string + }`, + ); + } + } else if (looksLikeLinux) { + log.debug('This looks like a Linux app...'); + if (newOptions.platform === undefined) { + newOptions.platform = 'linux'; + } + executablePath = path.join( + appRoot, + children.filter((c) => c.name == name && c.isFile())[0].name, + ); + } + + if (!executablePath || !newOptions.platform) { + throw Error( + `Could not find executablePath or platform of app in ${appRoot}`, + ); + } + + log.debug(`Executable path: ${executablePath}`); + + if (newOptions.arch === undefined) { + const executableInfo = getExecutableInfo( + executablePath, + newOptions.platform, + ); + if (!executableInfo) { + throw new Error( + `Could not get executable info for executable path: ${executablePath}`, + ); + } + + newOptions.arch = executableInfo.arch; + log.debug(`Extracted arch from executable: ${newOptions.arch as string}`); + } + if (newOptions.platform === undefined || newOptions.arch == undefined) { + throw Error(`Could not determine platform / arch of app in ${appRoot}`); + } + + return newOptions; +} diff --git a/src/helpers/upgrade/plistInfoXMLHelpers.ts b/src/helpers/upgrade/plistInfoXMLHelpers.ts new file mode 100644 index 0000000..40af710 --- /dev/null +++ b/src/helpers/upgrade/plistInfoXMLHelpers.ts @@ -0,0 +1,39 @@ +export function extractBoolean( + infoPlistXML: string, + plistKey: string, +): boolean | undefined { + const plistValue = extractRaw(infoPlistXML, plistKey); + + return plistValue === undefined + ? undefined + : plistValue.split('<')[1].split('/>')[0].toLowerCase() === 'true'; +} + +export function extractString( + infoPlistXML: string, + plistKey: string, +): string | undefined { + const plistValue = extractRaw(infoPlistXML, plistKey); + + return plistValue === undefined + ? undefined + : plistValue.split('')[1].split('')[0]; +} + +function extractRaw( + infoPlistXML: string, + plistKey: string, +): string | undefined { + // This would be easier with xml2js, but let's not add a dependency for something this minor. + const fullKey = `\n ${plistKey}`; + + if (infoPlistXML.indexOf(fullKey) === -1) { + // This value wasn't set, so we'll stay agnostic to it + return undefined; + } + + return infoPlistXML + .split(fullKey)[1] + .split('\n ')[0] // Get everything between here and the end of the main plist dict + .split('\n ')[0]; // Get everything before the next key (if it exists) +} diff --git a/src/helpers/upgrade/rceditGet.ts b/src/helpers/upgrade/rceditGet.ts new file mode 100644 index 0000000..4ba75e1 --- /dev/null +++ b/src/helpers/upgrade/rceditGet.ts @@ -0,0 +1,42 @@ +import * as os from 'os'; +import * as path from 'path'; +import { spawnSync } from 'child_process'; + +// A modification of https://github.com/electron/node-rcedit to support the retrieval +// of information. + +export function getVersionString( + executablePath: string, + versionString: string, +): string | undefined { + let rcedit = path.resolve( + __dirname, + '..', + '..', + '..', + 'node_modules', + 'rcedit', + 'bin', + process.arch === 'x64' ? 'rcedit-x64.exe' : 'rcedit.exe', + ); + const args = [executablePath, `--get-version-string`, versionString]; + + const spawnOptions = { + env: { ...process.env }, + }; + + // Use Wine on non-Windows platforms except for WSL, which doesn't need it + if (process.platform !== 'win32' && !os.release().endsWith('Microsoft')) { + args.unshift(rcedit); + rcedit = process.arch === 'x64' ? 'wine64' : 'wine'; + // Suppress "fixme:" stderr log messages + spawnOptions.env.WINEDEBUG = '-all'; + } + try { + const child = spawnSync(rcedit, args, spawnOptions); + const result = child.output?.toString().split(',wine: ')[0]; + return result.startsWith(',') ? result.substr(1) : result; + } catch { + return undefined; + } +} diff --git a/src/helpers/upgrade/upgrade.ts b/src/helpers/upgrade/upgrade.ts new file mode 100644 index 0000000..1cd6caf --- /dev/null +++ b/src/helpers/upgrade/upgrade.ts @@ -0,0 +1,234 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import * as log from 'loglevel'; + +import { + NativefierOptions, + RawOptions, +} from '../../../shared/src/options/model'; +import { dirExists, fileExists } from '../fsHelpers'; +import { extractBoolean, extractString } from './plistInfoXMLHelpers'; +import { getOptionsFromExecutable } from './executableHelpers'; +import { parseJson } from '../../utils/parseUtils'; + +export type UpgradeAppInfo = { + appResourcesDir: string; + appRoot: string; + options: NativefierOptions; +}; + +function findUpgradeAppResourcesDir(searchDir: string): string | null { + searchDir = dirExists(searchDir) ? searchDir : path.dirname(searchDir); + log.debug(`Searching for nativfier.json in ${searchDir}`); + const children = fs.readdirSync(searchDir, { withFileTypes: true }); + if (fileExists(path.join(searchDir, 'nativefier.json'))) { + // Found 'nativefier.json', so this must be it! + return path.resolve(searchDir); + } + const childDirectories = children.filter((c) => c.isDirectory()); + for (const childDir of childDirectories) { + // We must go deeper! + const result = findUpgradeAppResourcesDir( + path.join(searchDir, childDir.name, 'nativefier.json'), + ); + if (result !== null) { + return path.resolve(result); + } + } + + // Didn't find it down here + return null; +} + +function getAppRoot( + appResourcesDir: string, + options: NativefierOptions, +): string { + switch (options.platform) { + case 'darwin': + return path.resolve(path.join(appResourcesDir, '..', '..', '..', '..')); + case 'linux': + case 'win32': + return path.resolve(path.join(appResourcesDir, '..', '..')); + default: + throw new Error( + `Could not find the app root for platform: ${ + options.platform ?? 'undefined' + }`, + ); + } +} + +function getIconPath(appResourcesDir: string): string | undefined { + const icnsPath = path.join(appResourcesDir, '..', 'electron.icns'); + if (fileExists(icnsPath)) { + log.debug(`Found icon at: ${icnsPath}`); + return path.resolve(icnsPath); + } + const icoPath = path.join(appResourcesDir, 'icon.ico'); + if (fileExists(icoPath)) { + log.debug(`Found icon at: ${icoPath}`); + return path.resolve(icoPath); + } + const pngPath = path.join(appResourcesDir, 'icon.png'); + if (fileExists(pngPath)) { + log.debug(`Found icon at: ${pngPath}`); + return path.resolve(pngPath); + } + + log.debug('Could not find icon file.'); + return undefined; +} + +function getInfoPListOptions( + appResourcesDir: string, + priorOptions: NativefierOptions, +): NativefierOptions { + if (!fileExists(path.join(appResourcesDir, '..', '..', 'Info.plist'))) { + // Not a darwin/mas app, so this is irrelevant + return priorOptions; + } + + const newOptions = { ...priorOptions }; + + const infoPlistXML: string = fs + .readFileSync(path.join(appResourcesDir, '..', '..', 'Info.plist')) + .toString(); + + if (newOptions.appCopyright === undefined) { + // https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L230-L232 + newOptions.appCopyright = extractString( + infoPlistXML, + 'NSHumanReadableCopyright', + ); + log.debug( + `Extracted app copyright from Info.plist: ${ + newOptions.appCopyright as string + }`, + ); + } + + if (newOptions.appVersion === undefined) { + // https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L214-L216 + // This could also be the buildVersion, but since they end up in the same place, that SHOULDN'T matter + const bundleVersion = extractString(infoPlistXML, 'CFBundleVersion'); + newOptions.appVersion = + bundleVersion === undefined || bundleVersion === '1.0.0' // If it's 1.0.0, that's just the default + ? undefined + : bundleVersion; + (newOptions.darwinDarkModeSupport = + newOptions.darwinDarkModeSupport === undefined + ? undefined + : newOptions.darwinDarkModeSupport === false), + log.debug( + `Extracted app version from Info.plist: ${ + newOptions.appVersion as string + }`, + ); + } + + if (newOptions.darwinDarkModeSupport === undefined) { + // https://github.com/electron/electron-packager/blob/0d3f84374e9ab3741b171610735ebc6be3e5e75f/src/mac.js#L234-L236 + newOptions.darwinDarkModeSupport = extractBoolean( + infoPlistXML, + 'NSRequiresAquaSystemAppearance', + ); + log.debug( + `Extracted Darwin dark mode support from Info.plist: ${ + newOptions.darwinDarkModeSupport ? 'Yes' : 'No' + }`, + ); + } + + return newOptions; +} + +function getInjectPaths(appResourcesDir: string): string[] | undefined { + const injectDir = path.join(appResourcesDir, 'inject'); + if (!dirExists(injectDir)) { + return undefined; + } + + const injectPaths = fs + .readdirSync(injectDir, { withFileTypes: true }) + .filter( + (fd) => + fd.isFile() && + (fd.name.toLowerCase().endsWith('.css') || + fd.name.toLowerCase().endsWith('.js')), + ) + .map((fd) => path.resolve(path.join(injectDir, fd.name))); + log.debug(`CSS/JS Inject paths: ${injectPaths.join(', ')}`); + return injectPaths; +} + +function isAsar(appResourcesDir: string): boolean { + const asar = fileExists(path.join(appResourcesDir, '..', 'electron.asar')); + log.debug(`Is this app an ASAR? ${asar ? 'Yes' : 'No'}`); + return asar; +} + +export function findUpgradeApp(upgradeFrom: string): UpgradeAppInfo | null { + const searchDir = dirExists(upgradeFrom) + ? upgradeFrom + : path.dirname(upgradeFrom); + log.debug(`Looking for old options file in ${searchDir}`); + const appResourcesDir = findUpgradeAppResourcesDir(searchDir); + if (appResourcesDir === null) { + log.debug(`No nativefier.json file found in ${searchDir}`); + return null; + } + + const nativefierJSONPath = path.join(appResourcesDir, 'nativefier.json'); + + log.debug(`Loading ${nativefierJSONPath}`); + let options = parseJson( + fs.readFileSync(nativefierJSONPath, 'utf8'), + ); + + if (!options) { + throw new Error( + `Could not read Nativefier options from ${nativefierJSONPath}`, + ); + } + + options.electronVersion = undefined; + + options = { + ...options, + ...getOptionsFromExecutable(appResourcesDir, options), + }; + + const appRoot = getAppRoot(appResourcesDir, options); + + return { + appResourcesDir, + appRoot, + options: { + ...options, + ...getInfoPListOptions(appResourcesDir, options), + asar: options.asar !== undefined ? options.asar : isAsar(appResourcesDir), + icon: getIconPath(appResourcesDir), + inject: getInjectPaths(appResourcesDir), + }, + }; +} + +export function useOldAppOptions( + rawOptions: RawOptions, + oldApp: UpgradeAppInfo, +): RawOptions { + if (rawOptions.targetUrl !== undefined && dirExists(rawOptions.targetUrl)) { + // You got your ouput dir in my targetUrl! + rawOptions.out = rawOptions.targetUrl; + } + + log.debug('oldApp', oldApp); + + const combinedOptions = { ...rawOptions, ...oldApp.options }; + + log.debug('Combined options', combinedOptions); + + return combinedOptions; +} diff --git a/src/infer/browsers/inferChromeVersion.ts b/src/infer/browsers/inferChromeVersion.ts new file mode 100644 index 0000000..873eec1 --- /dev/null +++ b/src/infer/browsers/inferChromeVersion.ts @@ -0,0 +1,58 @@ +import axios from 'axios'; +import * as log from 'loglevel'; +import { + DEFAULT_CHROME_VERSION, + DEFAULT_ELECTRON_VERSION, +} from '../../constants'; + +type ElectronRelease = { + version: string; + date: string; + node: string; + v8: string; + uv: string; + zlib: string; + openssl: string; + modules: string; + chrome: string; + files: string[]; +}; + +const ELECTRON_VERSIONS_URL = 'https://releases.electronjs.org/releases.json'; + +export async function getChromeVersionForElectronVersion( + electronVersion: string, + url = ELECTRON_VERSIONS_URL, +): Promise { + if (!electronVersion || electronVersion === DEFAULT_ELECTRON_VERSION) { + // Exit quickly for the scenario that we already know about + return DEFAULT_CHROME_VERSION; + } + + try { + log.debug('Grabbing electron<->chrome versions file from', url); + const response = await axios.get(url, { timeout: 5000 }); + if (response.status !== 200) { + throw new Error(`Bad request: Status code ${response.status}`); + } + const electronReleases: ElectronRelease[] = response.data; + const electronVersionToChromeVersion: { [key: string]: string } = {}; + for (const release of electronReleases) { + electronVersionToChromeVersion[release.version] = release.chrome; + } + if (!(electronVersion in electronVersionToChromeVersion)) { + throw new Error( + `Electron version '${electronVersion}' not found in retrieved version list!`, + ); + } + const chromeVersion = electronVersionToChromeVersion[electronVersion]; + log.debug( + `Associated electron v${electronVersion} to chrome v${chromeVersion}`, + ); + return chromeVersion; + } catch (err: unknown) { + log.error('getChromeVersionForElectronVersion ERROR', err); + log.debug('Falling back to default Chrome version', DEFAULT_CHROME_VERSION); + return DEFAULT_CHROME_VERSION; + } +} diff --git a/src/infer/browsers/inferFirefoxVersion.ts b/src/infer/browsers/inferFirefoxVersion.ts new file mode 100644 index 0000000..478a299 --- /dev/null +++ b/src/infer/browsers/inferFirefoxVersion.ts @@ -0,0 +1,49 @@ +import axios from 'axios'; +import * as log from 'loglevel'; +import { DEFAULT_FIREFOX_VERSION } from '../../constants'; + +type FirefoxVersions = { + FIREFOX_AURORA: string; + FIREFOX_DEVEDITION: string; + FIREFOX_ESR: string; + FIREFOX_ESR_NEXT: string; + FIREFOX_NIGHTLY: string; + LAST_MERGE_DATE: string; + LAST_RELEASE_DATE: string; + LAST_SOFTFREEZE_DATE: string; + LATEST_FIREFOX_DEVEL_VERSION: string; + LATEST_FIREFOX_OLDER_VERSION: string; + LATEST_FIREFOX_RELEASED_DEVEL_VERSION: string; + LATEST_FIREFOX_VERSION: string; + NEXT_MERGE_DATE: string; + NEXT_RELEASE_DATE: string; + NEXT_SOFTFREEZE_DATE: string; +}; + +const FIREFOX_VERSIONS_URL = + 'https://product-details.mozilla.org/1.0/firefox_versions.json'; + +export async function getLatestFirefoxVersion( + url = FIREFOX_VERSIONS_URL, +): Promise { + try { + log.debug('Grabbing Firefox version data from', url); + const response = await axios.get(url, { timeout: 5000 }); + if (response.status !== 200) { + throw new Error(`Bad request: Status code ${response.status}`); + } + const firefoxVersions: FirefoxVersions = response.data; + + log.debug( + `Got latest Firefox version ${firefoxVersions.LATEST_FIREFOX_VERSION}`, + ); + return firefoxVersions.LATEST_FIREFOX_VERSION; + } catch (err: unknown) { + log.error('getLatestFirefoxVersion ERROR', err); + log.debug( + 'Falling back to default Firefox version', + DEFAULT_FIREFOX_VERSION, + ); + return DEFAULT_FIREFOX_VERSION; + } +} diff --git a/src/infer/browsers/inferSafariVersion.ts b/src/infer/browsers/inferSafariVersion.ts new file mode 100644 index 0000000..c35af6c --- /dev/null +++ b/src/infer/browsers/inferSafariVersion.ts @@ -0,0 +1,77 @@ +import axios from 'axios'; +import * as log from 'loglevel'; +import { DEFAULT_SAFARI_VERSION } from '../../constants'; + +export type SafariVersion = { + majorVersion: number; + version: string; + webkitVersion: string; +}; + +const SAFARI_VERSIONS_HISTORY_URL = + 'https://en.wikipedia.org/wiki/Safari_version_history'; + +export async function getLatestSafariVersion( + url = SAFARI_VERSIONS_HISTORY_URL, +): Promise { + try { + log.debug('Grabbing apple version data from', url); + const response = await axios.get(url, { timeout: 5000 }); + if (response.status !== 200) { + throw new Error(`Bad request: Status code ${response.status}`); + } + + // This would be easier with an HTML parser, but we're not going to include an extra dependency for something that dumb + const rawData: string = response.data; + + const majorVersions = [ + ...rawData.matchAll( + /class="mw-headline" id="Safari_[0-9]*">Safari ([0-9]*) match[1]); + + const majorVersion = parseInt(majorVersions[majorVersions.length - 1]); + + const majorVersionTable = rawData + .split('>Release history<')[2] + .split(' table.includes(`Safari ${majorVersion}.x`))[0]; + + const versionRows = majorVersionTable.split('\s*(([0-9]*\.){2}[0-9])\s* 0 && !version) { + version = versionMatch[0][1]; + } + + const webkitVersionMatch = [ + ...versionRow.matchAll(/>\s*(([0-9]*\.){3,4}[0-9])\s* 0 && !webkitVersion) { + webkitVersion = webkitVersionMatch[0][1]; + } + if (version && webkitVersion) { + break; + } + } + + if (version && webkitVersion) { + return { + majorVersion, + version, + webkitVersion, + }; + } + return DEFAULT_SAFARI_VERSION; + } catch (err: unknown) { + log.error('getLatestSafariVersion ERROR', err); + log.debug('Falling back to default Safari version', DEFAULT_SAFARI_VERSION); + return DEFAULT_SAFARI_VERSION; + } +} diff --git a/src/infer/inferIcon.ts b/src/infer/inferIcon.ts new file mode 100644 index 0000000..7d7b86b --- /dev/null +++ b/src/infer/inferIcon.ts @@ -0,0 +1,126 @@ +import * as path from 'path'; +import { writeFile } from 'fs'; +import { promisify } from 'util'; + +import gitCloud = require('gitcloud'); +import pageIcon from 'page-icon'; + +import { + downloadFile, + DownloadResult, + getAllowedIconFormats, + getTempDir, +} from '../helpers/helpers'; +import * as log from 'loglevel'; + +const writeFileAsync = promisify(writeFile); + +const GITCLOUD_SPACE_DELIMITER = '-'; +const GITCLOUD_URL = 'https://nativefier.github.io/nativefier-icons/'; + +type GitCloudIcon = { + ext?: string; + name?: string; + score?: number; + url?: string; +}; + +function getMaxMatchScore(iconWithScores: GitCloudIcon[]): number { + const score = iconWithScores.reduce((maxScore, currentIcon) => { + const currentScore = currentIcon.score; + if (currentScore && currentScore > maxScore) { + return currentScore; + } + return maxScore; + }, 0); + log.debug('Max icon match score:', score); + return score; +} + +function getMatchingIcons( + iconsWithScores: GitCloudIcon[], + maxScore: number, +): GitCloudIcon[] { + return iconsWithScores.filter((item) => item.score === maxScore); +} + +function mapIconWithMatchScore( + cloudIcons: { name: string; url: string }[], + targetUrl: string, +): GitCloudIcon[] { + const normalisedTargetUrl = targetUrl.toLowerCase(); + return cloudIcons.map((item) => { + const itemWords = item.name.split(GITCLOUD_SPACE_DELIMITER); + const score: number = itemWords.reduce( + (currentScore: number, word: string) => { + if (normalisedTargetUrl.includes(word)) { + return currentScore + 1; + } + return currentScore; + }, + 0, + ); + + return { ...item, ext: path.extname(item.url), score }; + }); +} + +async function inferIconFromStore( + targetUrl: string, + platform: string, +): Promise { + log.debug(`Inferring icon from store for ${targetUrl} on ${platform}`); + const allowedFormats = new Set( + getAllowedIconFormats(platform), + ); + + const cloudIcons = await gitCloud(GITCLOUD_URL); + log.debug(`Got ${cloudIcons.length} icons from gitcloud`); + const iconWithScores = mapIconWithMatchScore(cloudIcons, targetUrl); + const maxScore = getMaxMatchScore(iconWithScores); + + if (maxScore === 0) { + log.debug('No relevant icon in store.'); + return undefined; + } + + const iconsMatchingScore = getMatchingIcons(iconWithScores, maxScore); + const iconsMatchingExt = iconsMatchingScore.filter((icon) => + allowedFormats.has(icon.ext ?? path.extname(icon.url as string)), + ); + const matchingIcon = iconsMatchingExt[0]; + const iconUrl = matchingIcon && matchingIcon.url; + + if (!iconUrl) { + log.debug('Could not infer icon from store'); + return undefined; + } + return downloadFile(iconUrl); +} + +export async function inferIcon( + targetUrl: string, + platform: string, +): Promise { + log.debug(`Inferring icon for ${targetUrl} on ${platform}`); + const tmpDirPath = getTempDir('iconinfer'); + + let icon: { ext: string; data: Buffer } | undefined = + await inferIconFromStore(targetUrl, platform); + if (!icon) { + const ext = platform === 'win32' ? '.ico' : '.png'; + log.debug(`Trying to extract a ${ext} icon from the page.`); + icon = await pageIcon(targetUrl, { ext }); + } + if (!icon) { + return undefined; + } + log.debug(`Got an icon from the page.`); + + const iconPath = path.join(tmpDirPath, `/icon${icon.ext}`); + log.debug( + `Writing ${(icon.data.length / 1024).toFixed(1)} kb icon to ${iconPath}`, + ); + await writeFileAsync(iconPath, icon.data); + return iconPath; +} diff --git a/src/infer/inferOs.ts b/src/infer/inferOs.ts new file mode 100644 index 0000000..b0bc58b --- /dev/null +++ b/src/infer/inferOs.ts @@ -0,0 +1,35 @@ +import * as os from 'os'; + +import * as log from 'loglevel'; + +// Ideally we'd get this list directly from electron-packager, but it's not +// possible to convert a literal type to an array of strings in current TypeScript +export const supportedArchs = ['x64', 'armv7l', 'arm64', 'universal']; +export const supportedPlatforms = [ + 'darwin', + 'linux', + 'mac', + 'mas', + 'osx', + 'win32', + 'windows', +]; + +export function inferPlatform(): string { + const platform = os.platform(); + if (['darwin', 'linux', 'win32'].includes(platform)) { + log.debug('Inferred platform', platform); + return platform; + } + + throw new Error(`Untested platform ${platform} detected`); +} + +export function inferArch(): string { + const arch = os.arch(); + if (!supportedArchs.includes(arch)) { + throw new Error(`Incompatible architecture ${arch} detected`); + } + log.debug('Inferred arch', arch); + return arch; +} diff --git a/src/infer/inferTitle.test.ts b/src/infer/inferTitle.test.ts new file mode 100644 index 0000000..1ed8254 --- /dev/null +++ b/src/infer/inferTitle.test.ts @@ -0,0 +1,25 @@ +import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; + +import { inferTitle } from './inferTitle'; + +test('it returns the correct title', async () => { + const axiosGetMock = jest.spyOn(axios, 'get'); + const mockedResponse: AxiosResponse = { + data: ` + + + TEST_TITLE + + `, + status: 200, + statusText: 'OK', + headers: {}, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + config: {} as unknown as InternalAxiosRequestConfig, + }; + axiosGetMock.mockResolvedValue(mockedResponse); + const result = await inferTitle('someurl'); + + expect(axiosGetMock).toHaveBeenCalledTimes(1); + expect(result).toBe('TEST_TITLE'); +}); diff --git a/src/infer/inferTitle.ts b/src/infer/inferTitle.ts new file mode 100644 index 0000000..166e57b --- /dev/null +++ b/src/infer/inferTitle.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; +import * as log from 'loglevel'; + +const USER_AGENT = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15'; + +export async function inferTitle(url: string): Promise { + const { data } = await axios.get(url, { + headers: { + // Fake user agent for pages like http://messenger.com + 'User-Agent': USER_AGENT, + }, + }); + log.debug(`Fetched ${(data.length / 1024).toFixed(1)} kb page at`, url); + const inferredTitle = + /<\s*title.*?>(?.+?)<\s*\/title\s*?>/i.exec(data)?.groups?.title ?? + 'Webapp'; + log.debug('Inferred title:', inferredTitle); + return inferredTitle; +} diff --git a/src/integration-test.ts b/src/integration-test.ts new file mode 100644 index 0000000..77c7a7c --- /dev/null +++ b/src/integration-test.ts @@ -0,0 +1,220 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { DEFAULT_ELECTRON_VERSION } from './constants'; +import { getTempDir } from './helpers/helpers'; +import { getChromeVersionForElectronVersion } from './infer/browsers/inferChromeVersion'; +import { getLatestFirefoxVersion } from './infer/browsers/inferFirefoxVersion'; +import { getLatestSafariVersion } from './infer/browsers/inferSafariVersion'; +import { inferArch } from './infer/inferOs'; +import { buildNativefierApp } from './main'; +import { userAgent } from './options/fields/userAgent'; +import { + GlobalShortcut, + NativefierOptions, + RawOptions, +} from '../shared/src/options/model'; +import { parseJson } from './utils/parseUtils'; + +async function checkApp( + appRoot: string, + inputOptions: RawOptions, +): Promise<void> { + const arch = inputOptions.arch ? inputOptions.arch : inferArch(); + if (inputOptions.out !== undefined) { + expect( + path.join( + inputOptions.out, + `npm-${inputOptions.platform as string}-${arch}`, + ), + ).toBe(appRoot); + } + + let relativeResourcesDir = 'resources'; + + if (inputOptions.platform === 'darwin') { + relativeResourcesDir = path.join('npm.app', 'Contents', 'Resources'); + } + + const appPath = path.join(appRoot, relativeResourcesDir, 'app'); + + const configPath = path.join(appPath, 'nativefier.json'); + const nativefierConfig: NativefierOptions | undefined = + parseJson<NativefierOptions>(fs.readFileSync(configPath).toString()); + expect(nativefierConfig).not.toBeUndefined(); + + expect(inputOptions.targetUrl).toBe(nativefierConfig?.targetUrl); + + // Test name inferring + expect(nativefierConfig?.name).toBe('npm'); + + // Test icon writing + const iconFile = + inputOptions.platform === 'darwin' + ? path.join('..', 'electron.icns') + : inputOptions.platform === 'linux' + ? 'icon.png' + : 'icon.ico'; + const iconPath = path.join(appPath, iconFile); + expect(fs.existsSync(iconPath)).toEqual(true); + expect(fs.statSync(iconPath).size).toBeGreaterThan(1000); + + // Test arch + if (inputOptions.arch !== undefined) { + expect(inputOptions.arch).toEqual(nativefierConfig?.arch); + } else { + expect(os.arch()).toEqual(nativefierConfig?.arch); + } + + // Test electron version + expect(nativefierConfig?.electronVersionUsed).toBe( + inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION, + ); + + // Test user agent + if (inputOptions.userAgent) { + const translatedUserAgent = await userAgent({ + packager: { + platform: inputOptions.platform, + electronVersion: + inputOptions.electronVersion || DEFAULT_ELECTRON_VERSION, + }, + nativefier: { userAgent: inputOptions.userAgent }, + }); + inputOptions.userAgent = translatedUserAgent || inputOptions.userAgent; + } + + expect(nativefierConfig?.userAgent).toEqual(inputOptions.userAgent); + + // Test lang + expect(nativefierConfig?.lang).toEqual(inputOptions.lang); + + // Test global shortcuts + if (inputOptions.globalShortcuts) { + let shortcutData: GlobalShortcut[] | undefined = []; + + if (typeof inputOptions.globalShortcuts === 'string') { + shortcutData = parseJson<GlobalShortcut[]>( + fs.readFileSync(inputOptions.globalShortcuts, 'utf8'), + ); + } else { + shortcutData = inputOptions.globalShortcuts; + } + + expect(nativefierConfig?.globalShortcuts).toStrictEqual(shortcutData); + } +} + +describe('Nativefier', () => { + jest.setTimeout(300000); + + test.each(['darwin', 'linux'])( + 'builds a Nativefier app for platform %s', + async (platform) => { + const tempDirectory = getTempDir('integtest'); + const options: RawOptions = { + lang: 'en-US', + out: tempDirectory, + overwrite: true, + platform, + targetUrl: 'https://npmjs.com/', + }; + const appPath = await buildNativefierApp(options); + expect(appPath).not.toBeUndefined(); + await checkApp(appPath, options); + }, + ); +}); + +function generateShortcutsFile(dir: string): string { + const shortcuts = [ + { + key: 'MediaPlayPause', + inputEvents: [ + { + type: 'keyDown', + keyCode: 'Space', + }, + ], + }, + { + key: 'MediaNextTrack', + inputEvents: [ + { + type: 'keyDown', + keyCode: 'Right', + }, + ], + }, + ]; + + const filename = path.join(dir, 'shortcuts.json'); + fs.writeFileSync(filename, JSON.stringify(shortcuts)); + + return filename; +} + +describe('Nativefier upgrade', () => { + jest.setTimeout(300000); + + test.each([ + { platform: 'darwin', arch: 'x64' }, + { platform: 'linux', arch: 'arm64', userAgent: 'FIREFOX 60' }, + // Exhaustive integration testing here would be neat, but takes too long. + // -> For now, only testing a subset of platforms/archs + // { platform: 'win32', arch: 'x64' }, + // { platform: 'darwin', arch: 'arm64' }, + // { platform: 'linux', arch: 'x64' }, + // { platform: 'linux', arch: 'armv7l' }, + ])( + 'can upgrade a Nativefier app for platform/arch: %s', + async (baseAppOptions) => { + const tempDirectory = getTempDir('integtestUpgrade1'); + const shortcuts = generateShortcutsFile(tempDirectory); + const options: RawOptions = { + electronVersion: '11.2.3', + globalShortcuts: shortcuts, + out: tempDirectory, + overwrite: true, + targetUrl: 'https://npmjs.com/', + ...baseAppOptions, + }; + const appPath = await buildNativefierApp(options); + expect(appPath).not.toBeUndefined(); + await checkApp(appPath, options); + + const upgradeOptions: RawOptions = { + upgrade: appPath, + overwrite: true, + }; + + const upgradeAppPath = await buildNativefierApp(upgradeOptions); + options.electronVersion = DEFAULT_ELECTRON_VERSION; + options.userAgent = baseAppOptions.userAgent; + expect(upgradeAppPath).not.toBeUndefined(); + await checkApp(upgradeAppPath, options); + }, + ); +}); + +describe('Browser version retrieval', () => { + test('get chrome version with electron version', async () => { + await expect(getChromeVersionForElectronVersion('12.0.0')).resolves.toBe( + '89.0.4389.69', + ); + }); + + test('get latest firefox version', async () => { + const firefoxVersion = await getLatestFirefoxVersion(); + + const majorVersion = parseInt(firefoxVersion.split('.')[0]); + expect(majorVersion).toBeGreaterThanOrEqual(88); + }); + + test('get latest safari version', async () => { + const safariVersion = await getLatestSafariVersion(); + + expect(safariVersion.majorVersion).toBeGreaterThanOrEqual(14); + }); +}); diff --git a/src/jestSetupFiles.ts b/src/jestSetupFiles.ts new file mode 100644 index 0000000..e722b2f --- /dev/null +++ b/src/jestSetupFiles.ts @@ -0,0 +1,9 @@ +import * as log from 'loglevel'; + +if (process.env.LOGLEVEL) { + log.setLevel(process.env.LOGLEVEL as log.LogLevelDesc); +} else { + log.disableAll(); +} + +process.traceDeprecation = true; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..cc56c1b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,21 @@ +import 'source-map-support/register'; + +import { buildNativefierApp } from './build/buildNativefierApp'; +import { RawOptions } from '../shared/src/options/model'; + +export { buildNativefierApp }; + +/** + * Only for compatibility with Nativefier <= 7.7.1 ! + * Use the better, modern async `buildNativefierApp` instead if you can! + */ +function buildNativefierAppOldCallbackStyle( + options: RawOptions, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types + callback: (err?: Error, result?: string) => void, +): void { + buildNativefierApp(options) + .then((result) => callback(undefined, result)) + .catch((err: Error) => callback(err)); +} + +export default buildNativefierAppOldCallbackStyle; diff --git a/src/options/asyncConfig.ts b/src/options/asyncConfig.ts new file mode 100644 index 0000000..5f5fd1c --- /dev/null +++ b/src/options/asyncConfig.ts @@ -0,0 +1,12 @@ +import * as log from 'loglevel'; + +import { processOptions } from './fields/fields'; +import { AppOptions } from '../../shared/src/options/model'; + +/** + * Takes the options object and infers new values needing async work + */ +export async function asyncConfig(options: AppOptions): Promise<AppOptions> { + log.debug('\nPerforming async options post-processing.'); + return await processOptions(options); +} diff --git a/src/options/fields/fields.test.ts b/src/options/fields/fields.test.ts new file mode 100644 index 0000000..9bd2afc --- /dev/null +++ b/src/options/fields/fields.test.ts @@ -0,0 +1,105 @@ +import { AppOptions } from '../../../shared/src/options/model'; +import { processOptions } from './fields'; +describe('fields', () => { + let options: AppOptions; + + beforeEach(() => { + options = { + nativefier: { + accessibilityPrompt: false, + alwaysOnTop: false, + backgroundColor: undefined, + basicAuthPassword: undefined, + basicAuthUsername: undefined, + blockExternalUrls: false, + bookmarksMenu: undefined, + bounce: false, + browserwindowOptions: undefined, + clearCache: false, + counter: false, + crashReporter: undefined, + disableContextMenu: false, + disableDevTools: false, + disableGpu: false, + disableOldBuildWarning: false, + diskCacheSize: undefined, + enableEs3Apis: false, + fastQuit: true, + fileDownloadOptions: undefined, + flashPluginDir: undefined, + fullScreen: false, + globalShortcuts: undefined, + height: undefined, + hideWindowFrame: false, + ignoreCertificate: false, + ignoreGpuBlacklist: false, + inject: [], + insecure: false, + internalUrls: undefined, + maximize: false, + maxHeight: undefined, + minWidth: undefined, + minHeight: undefined, + maxWidth: undefined, + nativefierVersion: '1.0.0', + processEnvs: undefined, + proxyRules: undefined, + showMenuBar: false, + singleInstance: false, + strictInternalUrls: false, + titleBarStyle: undefined, + tray: 'false', + userAgent: undefined, + userAgentHonest: false, + verbose: false, + versionString: '1.0.0', + width: undefined, + widevine: false, + x: undefined, + y: undefined, + zoom: 1, + }, + packager: { + arch: process.arch, + dir: '', + platform: process.platform, + portable: false, + targetUrl: '', + upgrade: false, + }, + }; + }); + + test('fully-defined async options are returned as-is', async () => { + options.packager.icon = '/my/icon.png'; + options.packager.name = 'my beautiful app '; + options.packager.platform = 'darwin'; + options.nativefier.userAgent = 'random user agent'; + await processOptions(options); + + expect(options.packager.icon).toEqual('/my/icon.png'); + expect(options.packager.name).toEqual('my beautiful app'); + expect(options.nativefier.userAgent).toEqual('random user agent'); + }); + + test('name has spaces stripped in linux', async () => { + options.packager.name = 'my beautiful app '; + options.packager.platform = 'linux'; + await processOptions(options); + + expect(options.packager.name).toEqual('mybeautifulapp'); + }); + + test('user agent is ignored if not provided', async () => { + await processOptions(options); + + expect(options.nativefier.userAgent).toBeUndefined(); + }); + + test('user agent short code is populated', async () => { + options.nativefier.userAgent = 'edge'; + await processOptions(options); + + expect(options.nativefier.userAgent).not.toBe('edge'); + }); +}); diff --git a/src/options/fields/fields.ts b/src/options/fields/fields.ts new file mode 100644 index 0000000..a4ef770 --- /dev/null +++ b/src/options/fields/fields.ts @@ -0,0 +1,42 @@ +import { icon } from './icon'; +import { userAgent } from './userAgent'; +import { AppOptions } from '../../../shared/src/options/model'; +import { name } from './name'; + +type OptionPostprocessor = { + namespace: 'nativefier' | 'packager'; + option: 'icon' | 'name' | 'userAgent'; + processor: (options: AppOptions) => Promise<string | undefined>; +}; + +const OPTION_POSTPROCESSORS: OptionPostprocessor[] = [ + { namespace: 'nativefier', option: 'userAgent', processor: userAgent }, + { namespace: 'packager', option: 'icon', processor: icon }, + { namespace: 'packager', option: 'name', processor: name }, +]; + +export async function processOptions(options: AppOptions): Promise<AppOptions> { + const processedOptions = await Promise.all( + OPTION_POSTPROCESSORS.map(async ({ namespace, option, processor }) => { + const result = await processor(options); + return { + namespace, + option, + result, + }; + }), + ); + + for (const { namespace, option, result } of processedOptions) { + if ( + result && + namespace in options && + options[namespace] && + option in options[namespace] + ) { + // @ts-expect-error We're fiddling with objects at the string key level, which TS doesn't support well. + options[namespace][option] = result; + } + } + return options; +} diff --git a/src/options/fields/icon.test.ts b/src/options/fields/icon.test.ts new file mode 100644 index 0000000..64dfdbd --- /dev/null +++ b/src/options/fields/icon.test.ts @@ -0,0 +1,60 @@ +import * as log from 'loglevel'; + +import { icon } from './icon'; +import { inferIcon } from '../../infer/inferIcon'; + +jest.mock('./../../infer/inferIcon'); +jest.mock('loglevel'); + +const mockedResult = 'icon path'; +const ICON_PARAMS_PROVIDED = { + packager: { + icon: './icon.png', + targetUrl: 'https://google.com', + platform: 'mac', + }, +}; +const ICON_PARAMS_NEEDS_INFER = { + packager: { + targetUrl: 'https://google.com', + platform: 'mac', + }, +}; + +describe('when the icon parameter is passed', () => { + test('it should return the icon parameter', async () => { + expect(inferIcon).toHaveBeenCalledTimes(0); + await expect(icon(ICON_PARAMS_PROVIDED)).resolves.toBeUndefined(); + }); +}); + +describe('when the icon parameter is not passed', () => { + test('it should call inferIcon', async () => { + (inferIcon as jest.Mock).mockImplementationOnce(() => + Promise.resolve(mockedResult), + ); + const result = await icon(ICON_PARAMS_NEEDS_INFER); + + expect(result).toBe(mockedResult); + expect(inferIcon).toHaveBeenCalledWith( + ICON_PARAMS_NEEDS_INFER.packager.targetUrl, + ICON_PARAMS_NEEDS_INFER.packager.platform, + ); + }); + + describe('when inferIcon resolves with an error', () => { + test('it should handle the error', async () => { + (inferIcon as jest.Mock).mockImplementationOnce(() => + Promise.reject(new Error('some error')), + ); + const result = await icon(ICON_PARAMS_NEEDS_INFER); + + expect(result).toBeUndefined(); + expect(inferIcon).toHaveBeenCalledWith( + ICON_PARAMS_NEEDS_INFER.packager.targetUrl, + ICON_PARAMS_NEEDS_INFER.packager.platform, + ); + expect(log.warn).toHaveBeenCalledTimes(1); // eslint-disable-line @typescript-eslint/unbound-method + }); + }); +}); diff --git a/src/options/fields/icon.ts b/src/options/fields/icon.ts new file mode 100644 index 0000000..4874fa5 --- /dev/null +++ b/src/options/fields/icon.ts @@ -0,0 +1,38 @@ +import * as log from 'loglevel'; + +import { inferIcon } from '../../infer/inferIcon'; + +type IconParams = { + packager: { + icon?: string; + targetUrl: string; + platform?: string; + }; +}; + +export async function icon(options: IconParams): Promise<string | undefined> { + if (options.packager.icon) { + log.debug('Got icon from options. Using it, no inferring needed'); + return undefined; + } + + if (!options.packager.platform) { + log.error('No platform specified. Icon can not be inferrerd.'); + return undefined; + } + + try { + return await inferIcon( + options.packager.targetUrl, + options.packager.platform, + ); + } catch (err: unknown) { + // eslint-disable-next-line + const errorUrl: string = (err as any)?.config?.url; + log.warn( + 'Cannot automatically retrieve the app icon:', + errorUrl ? `${(err as Error).message} on ${errorUrl}` : err, + ); + return undefined; + } +} diff --git a/src/options/fields/name.test.ts b/src/options/fields/name.test.ts new file mode 100644 index 0000000..6d5cc39 --- /dev/null +++ b/src/options/fields/name.test.ts @@ -0,0 +1,110 @@ +import * as log from 'loglevel'; + +import { name } from './name'; +import { DEFAULT_APP_NAME } from '../../constants'; +import { inferTitle } from '../../infer/inferTitle'; +import { sanitizeFilename } from '../../utils/sanitizeFilename'; + +jest.mock('./../../infer/inferTitle'); +jest.mock('./../../utils/sanitizeFilename'); +jest.mock('loglevel'); + +const inferTitleMockedResult = 'mock name'; +const NAME_PARAMS_PROVIDED = { + packager: { + name: 'appname', + targetUrl: 'https://google.com', + platform: 'linux', + }, +}; +const NAME_PARAMS_NEEDS_INFER = { + packager: { + targetUrl: 'https://google.com', + platform: 'mac', + }, +}; +beforeAll(() => { + (sanitizeFilename as jest.Mock).mockImplementation( + (_, filename: string) => filename, + ); +}); + +describe('well formed name parameters', () => { + test('it should not call inferTitle', async () => { + const result = await name(NAME_PARAMS_PROVIDED); + + expect(inferTitle).toHaveBeenCalledTimes(0); + expect(result).toBe(NAME_PARAMS_PROVIDED.packager.name); + }); + + test('it should call sanitize filename', async () => { + const result = await name(NAME_PARAMS_PROVIDED); + + expect(sanitizeFilename).toHaveBeenCalledWith( + NAME_PARAMS_PROVIDED.packager.platform, + result, + ); + }); +}); + +describe('bad name parameters', () => { + beforeEach(() => { + (inferTitle as jest.Mock).mockResolvedValue(inferTitleMockedResult); + }); + + const params = { packager: { targetUrl: 'some url', platform: 'whatever' } }; + test('it should call inferTitle when the name is undefined', async () => { + await name(params); + expect(inferTitle).toHaveBeenCalledWith(params.packager.targetUrl); + }); + + test('it should call inferTitle when the name is an empty string', async () => { + const testParams = { + ...params, + name: '', + }; + + await name(testParams); + expect(inferTitle).toHaveBeenCalledWith(params.packager.targetUrl); + }); + + test('it should call sanitize filename', async () => { + const result = await name(params); + expect(sanitizeFilename).toHaveBeenCalledWith( + params.packager.platform, + result, + ); + }); +}); + +describe('handling inferTitle results', () => { + test('it should return the result from inferTitle', async () => { + const result = await name(NAME_PARAMS_NEEDS_INFER); + + expect(result).toEqual(inferTitleMockedResult); + expect(inferTitle).toHaveBeenCalledWith( + NAME_PARAMS_NEEDS_INFER.packager.targetUrl, + ); + }); + + test('it should return the default app name when the returned pageTitle is falsey', async () => { + (inferTitle as jest.Mock).mockResolvedValue(null); + const result = await name(NAME_PARAMS_NEEDS_INFER); + + expect(result).toEqual(DEFAULT_APP_NAME); + expect(inferTitle).toHaveBeenCalledWith( + NAME_PARAMS_NEEDS_INFER.packager.targetUrl, + ); + }); + + test('it should return the default app name when inferTitle rejects', async () => { + (inferTitle as jest.Mock).mockRejectedValue('some error'); + const result = await name(NAME_PARAMS_NEEDS_INFER); + + expect(result).toEqual(DEFAULT_APP_NAME); + expect(inferTitle).toHaveBeenCalledWith( + NAME_PARAMS_NEEDS_INFER.packager.targetUrl, + ); + expect(log.warn).toHaveBeenCalledTimes(1); // eslint-disable-line @typescript-eslint/unbound-method + }); +}); diff --git a/src/options/fields/name.ts b/src/options/fields/name.ts new file mode 100644 index 0000000..4573658 --- /dev/null +++ b/src/options/fields/name.ts @@ -0,0 +1,36 @@ +import * as log from 'loglevel'; + +import { sanitizeFilename } from '../../utils/sanitizeFilename'; +import { inferTitle } from '../../infer/inferTitle'; +import { DEFAULT_APP_NAME } from '../../constants'; + +type NameParams = { + packager: { + name?: string; + platform?: string; + targetUrl: string; + }; +}; + +async function tryToInferName(targetUrl: string): Promise<string> { + try { + log.debug('Inferring name for', targetUrl); + const pageTitle = await inferTitle(targetUrl); + return pageTitle || DEFAULT_APP_NAME; + } catch (err: unknown) { + log.warn( + `Unable to automatically determine app name, falling back to '${DEFAULT_APP_NAME}'.`, + err, + ); + return DEFAULT_APP_NAME; + } +} + +export async function name(options: NameParams): Promise<string> { + let name: string | undefined = options.packager.name; + if (!name) { + name = await tryToInferName(options.packager.targetUrl); + } + + return sanitizeFilename(options.packager.platform, name); +} diff --git a/src/options/fields/userAgent.test.ts b/src/options/fields/userAgent.test.ts new file mode 100644 index 0000000..6949801 --- /dev/null +++ b/src/options/fields/userAgent.test.ts @@ -0,0 +1,90 @@ +import { getChromeVersionForElectronVersion } from '../../infer/browsers/inferChromeVersion'; +import { getLatestFirefoxVersion } from '../../infer/browsers/inferFirefoxVersion'; +import { getLatestSafariVersion } from '../../infer/browsers/inferSafariVersion'; +import { userAgent } from './userAgent'; + +jest.mock('./../../infer/browsers/inferChromeVersion'); +jest.mock('./../../infer/browsers/inferFirefoxVersion'); +jest.mock('./../../infer/browsers/inferSafariVersion'); + +test('when a userAgent parameter is passed', async () => { + const params = { + packager: {}, + nativefier: { userAgent: 'valid user agent' }, + }; + await expect(userAgent(params)).resolves.toBeUndefined(); +}); + +test('no userAgent parameter is passed', async () => { + const params = { + packager: { platform: 'mac' }, + nativefier: {}, + }; + await expect(userAgent(params)).resolves.toBeUndefined(); +}); + +test('edge userAgent parameter is passed', async () => { + (getChromeVersionForElectronVersion as jest.Mock).mockImplementationOnce(() => + Promise.resolve('99.0.0'), + ); + const params = { + packager: { platform: 'darwin' }, + nativefier: { userAgent: 'edge' }, + }; + + const parsedUserAgent = await userAgent(params); + + expect(parsedUserAgent).not.toBe(params.nativefier.userAgent); + expect(parsedUserAgent).toContain('Edg/99.0.0'); +}); + +test('firefox userAgent parameter is passed', async () => { + (getLatestFirefoxVersion as jest.Mock).mockImplementationOnce(() => + Promise.resolve('100.0.0'), + ); + const params = { + packager: { platform: 'win32' }, + nativefier: { userAgent: 'firefox' }, + }; + + const parsedUserAgent = await userAgent(params); + + expect(parsedUserAgent).not.toBe(params.nativefier.userAgent); + expect(parsedUserAgent).toContain('Firefox/100.0.0'); +}); + +test('safari userAgent parameter is passed', async () => { + (getLatestSafariVersion as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + majorVersion: 101, + version: '101.0.0', + webkitVersion: '600.0.0.0', + }), + ); + const params = { + packager: { platform: 'linux' }, + nativefier: { userAgent: 'safari' }, + }; + + const parsedUserAgent = await userAgent(params); + + expect(parsedUserAgent).not.toBe(params.nativefier.userAgent); + expect(parsedUserAgent).toContain('Version/101.0.0 Safari'); +}); + +test('short userAgent parameter is passed with an electronVersion', async () => { + (getChromeVersionForElectronVersion as jest.Mock).mockImplementationOnce(() => + Promise.resolve('102.0.0'), + ); + + const params = { + packager: { electronVersion: '16.0.0', platform: 'darwin' }, + nativefier: { userAgent: 'edge' }, + }; + + const parsedUserAgent = await userAgent(params); + + expect(parsedUserAgent).not.toBe(params.nativefier.userAgent); + expect(parsedUserAgent).toContain('102.0.0'); + expect(getChromeVersionForElectronVersion).toHaveBeenCalledWith('16.0.0'); +}); diff --git a/src/options/fields/userAgent.ts b/src/options/fields/userAgent.ts new file mode 100644 index 0000000..de64cd6 --- /dev/null +++ b/src/options/fields/userAgent.ts @@ -0,0 +1,93 @@ +import * as log from 'loglevel'; +import { DEFAULT_ELECTRON_VERSION } from '../../constants'; + +import { getChromeVersionForElectronVersion } from '../../infer/browsers/inferChromeVersion'; +import { getLatestFirefoxVersion } from '../../infer/browsers/inferFirefoxVersion'; +import { getLatestSafariVersion } from '../../infer/browsers/inferSafariVersion'; +import { normalizePlatform } from '../optionsMain'; + +export type UserAgentOpts = { + packager: { + electronVersion?: string; + platform?: string; + }; + nativefier: { + userAgent?: string; + }; +}; + +const USER_AGENT_PLATFORM_MAPS: Record<string, string> = { + darwin: 'Macintosh; Intel Mac OS X 10_15_7', + linux: 'X11; Linux x86_64', + win32: 'Windows NT 10.0; Win64; x64', +}; + +const USER_AGENT_SHORT_CODE_MAPS: Record< + string, + (platform: string, electronVersion: string) => Promise<string> +> = { + edge: edgeUserAgent, + firefox: firefoxUserAgent, + safari: safariUserAgent, +}; + +export async function userAgent( + options: UserAgentOpts, +): Promise<string | undefined> { + if (!options.nativefier.userAgent) { + // No user agent got passed. Let's handle it with the app.userAgentFallback + return undefined; + } + + if ( + !Object.keys(USER_AGENT_SHORT_CODE_MAPS).includes( + options.nativefier.userAgent.toLowerCase(), + ) + ) { + // Real user agent got passed. No need to translate it. + log.debug( + `${options.nativefier.userAgent.toLowerCase()} not found in`, + Object.keys(USER_AGENT_SHORT_CODE_MAPS), + ); + return undefined; + } + + options.packager.platform = normalizePlatform(options.packager.platform); + + const userAgentPlatform: string = + USER_AGENT_PLATFORM_MAPS[ + options.packager.platform === 'mas' ? 'darwin' : options.packager.platform + ]; + + const mapFunction = USER_AGENT_SHORT_CODE_MAPS[options.nativefier.userAgent]; + + return await mapFunction( + userAgentPlatform, + options.packager.electronVersion ?? DEFAULT_ELECTRON_VERSION, + ); +} + +async function edgeUserAgent( + platform: string, + electronVersion: string, +): Promise<string> { + const chromeVersion = + await getChromeVersionForElectronVersion(electronVersion); + + return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36 Edg/${chromeVersion}`; +} + +async function firefoxUserAgent(platform: string): Promise<string> { + const firefoxVersion = await getLatestFirefoxVersion(); + + return `Mozilla/5.0 (${platform}; rv:${firefoxVersion}) Gecko/20100101 Firefox/${firefoxVersion}`.replace( + '10_15_7', + '10.15', + ); +} + +async function safariUserAgent(platform: string): Promise<string> { + const safariVersion = await getLatestSafariVersion(); + + return `Mozilla/5.0 (${platform}) AppleWebKit/${safariVersion.webkitVersion} (KHTML, like Gecko) Version/${safariVersion.version} Safari/${safariVersion.webkitVersion}`; +} diff --git a/src/options/normalizeUrl.test.ts b/src/options/normalizeUrl.test.ts new file mode 100644 index 0000000..2788b3d --- /dev/null +++ b/src/options/normalizeUrl.test.ts @@ -0,0 +1,17 @@ +import { normalizeUrl } from './normalizeUrl'; + +test("a proper URL shouldn't be mangled", () => { + expect(normalizeUrl('http://www.google.com')).toEqual( + 'http://www.google.com/', + ); +}); + +test('missing protocol should default to https', () => { + expect(normalizeUrl('www.google.com')).toEqual('https://www.google.com/'); +}); + +test("a proper URL shouldn't be mangled", () => { + expect(() => { + normalizeUrl('http://ssddfoo bar'); + }).toThrow('Your url "http://ssddfoo bar" is invalid'); +}); diff --git a/src/options/normalizeUrl.ts b/src/options/normalizeUrl.ts new file mode 100644 index 0000000..054bc25 --- /dev/null +++ b/src/options/normalizeUrl.ts @@ -0,0 +1,32 @@ +import * as url from 'url'; + +import * as log from 'loglevel'; + +function appendProtocol(inputUrl: string): string { + const parsed = url.parse(inputUrl); + if (!parsed.protocol) { + const urlWithProtocol = `https://${inputUrl}`; + log.warn( + `URL "${inputUrl}" lacks a protocol.`, + `Will try to parse it as HTTPS: "${urlWithProtocol}".`, + `Please pass "http://${inputUrl}" if this is what you meant.`, + ); + return urlWithProtocol; + } + return inputUrl; +} + +export function normalizeUrl(urlToNormalize: string): string { + const urlWithProtocol = appendProtocol(urlToNormalize); + + let parsedUrl: url.URL; + try { + parsedUrl = new url.URL(urlWithProtocol); + } catch (err: unknown) { + log.error('normalizeUrl ERROR', err); + throw new Error(`Your url "${urlWithProtocol}" is invalid`); + } + const normalizedUrl = parsedUrl.toString(); + log.debug(`Normalized URL ${urlToNormalize} to:`, normalizedUrl); + return normalizedUrl; +} diff --git a/src/options/optionsMain.test.ts b/src/options/optionsMain.test.ts new file mode 100644 index 0000000..4e08851 --- /dev/null +++ b/src/options/optionsMain.test.ts @@ -0,0 +1,125 @@ +import { getOptions, normalizePlatform } from './optionsMain'; +import * as asyncConfig from './asyncConfig'; +import { inferPlatform } from '../infer/inferOs'; +import { AppOptions, RawOptions } from '../../shared/src/options/model'; + +let asyncConfigMock: jest.SpyInstance; +const mockedAsyncConfig: AppOptions = { + nativefier: { + accessibilityPrompt: false, + alwaysOnTop: false, + backgroundColor: undefined, + basicAuthPassword: undefined, + basicAuthUsername: undefined, + blockExternalUrls: false, + bookmarksMenu: undefined, + bounce: false, + browserwindowOptions: undefined, + clearCache: false, + counter: false, + crashReporter: undefined, + disableContextMenu: false, + disableDevTools: false, + disableGpu: false, + disableOldBuildWarning: false, + diskCacheSize: undefined, + enableEs3Apis: false, + fastQuit: true, + fileDownloadOptions: undefined, + flashPluginDir: undefined, + fullScreen: false, + globalShortcuts: undefined, + height: undefined, + hideWindowFrame: false, + ignoreCertificate: false, + ignoreGpuBlacklist: false, + inject: [], + insecure: false, + internalUrls: undefined, + maximize: false, + maxHeight: undefined, + minWidth: undefined, + minHeight: undefined, + maxWidth: undefined, + nativefierVersion: '1.0.0', + processEnvs: undefined, + proxyRules: undefined, + showMenuBar: false, + singleInstance: false, + strictInternalUrls: false, + titleBarStyle: undefined, + tray: 'false', + userAgent: undefined, + userAgentHonest: false, + verbose: false, + versionString: '1.0.0', + width: undefined, + widevine: false, + x: undefined, + y: undefined, + zoom: 1, + }, + packager: { + arch: process.arch, + dir: '', + platform: process.platform, + portable: false, + targetUrl: '', + upgrade: false, + }, +}; + +beforeAll(() => { + asyncConfigMock = jest + .spyOn(asyncConfig, 'asyncConfig') + .mockResolvedValue(mockedAsyncConfig); +}); + +test('it should call the async config', async () => { + const params: RawOptions = { + targetUrl: 'https://example.com/', + tray: 'false', + }; + const result = await getOptions(params); + expect(asyncConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + packager: expect.anything() as AppOptions['packager'], + nativefier: expect.anything() as AppOptions['nativefier'], + }), + ); + expect(result.packager.targetUrl).toEqual(params.targetUrl); +}); + +test('it should set the accessibility prompt option to true by default', async () => { + const params: RawOptions = { + targetUrl: 'https://example.com/', + tray: 'false', + }; + const result = await getOptions(params); + expect(asyncConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + nativefier: expect.objectContaining({ + accessibilityPrompt: true, + }) as AppOptions['nativefier'], + }), + ); + expect(result.nativefier.accessibilityPrompt).toEqual(true); +}); + +test.each([ + { platform: 'darwin', expectedPlatform: 'darwin' }, + { platform: 'mAc', expectedPlatform: 'darwin' }, + { platform: 'osx', expectedPlatform: 'darwin' }, + { platform: 'liNux', expectedPlatform: 'linux' }, + { platform: 'mas', expectedPlatform: 'mas' }, + { platform: 'WIN32', expectedPlatform: 'win32' }, + { platform: 'windows', expectedPlatform: 'win32' }, + {}, +])('it should be able to normalize the platform %s', (platformOptions) => { + if (!platformOptions.expectedPlatform) { + platformOptions.expectedPlatform = inferPlatform(); + } + expect(normalizePlatform(platformOptions.platform)).toBe( + platformOptions.expectedPlatform, + ); +}); diff --git a/src/options/optionsMain.ts b/src/options/optionsMain.ts new file mode 100644 index 0000000..9ab55a3 --- /dev/null +++ b/src/options/optionsMain.ts @@ -0,0 +1,273 @@ +import * as fs from 'fs'; + +import axios from 'axios'; +import * as debug from 'debug'; +import * as log from 'loglevel'; + +// package.json is `require`d to let tsc strip the `src` folder by determining +// baseUrl=src. A static import would prevent that and cause an ugly extra `src` folder in `lib` +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const packageJson: { + name: string; + version: string; + // eslint-disable-next-line @typescript-eslint/no-var-requires +} = require('../../package.json'); +import { + DEFAULT_ELECTRON_VERSION, + PLACEHOLDER_APP_DIR, + ELECTRON_MAJOR_VERSION, +} from '../constants'; +import { inferPlatform, inferArch } from '../infer/inferOs'; +import { asyncConfig } from './asyncConfig'; +import { + AppOptions, + GlobalShortcut, + RawOptions, +} from '../../shared/src/options/model'; +import { normalizeUrl } from './normalizeUrl'; +import { parseJson } from '../utils/parseUtils'; + +const SEMVER_VERSION_NUMBER_REGEX = /\d+\.\d+\.\d+[-_\w\d.]*/; + +/** + * Process and validate raw user arguments + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function getOptions(rawOptions: RawOptions): Promise<AppOptions> { + const options: AppOptions = { + packager: { + appCopyright: rawOptions.appCopyright, + appVersion: rawOptions.appVersion, + arch: rawOptions.arch ?? inferArch(), + asar: rawOptions.asar ?? rawOptions.conceal ?? false, + buildVersion: rawOptions.buildVersion, + darwinDarkModeSupport: rawOptions.darwinDarkModeSupport ?? false, + dir: PLACEHOLDER_APP_DIR, + electronVersion: rawOptions.electronVersion ?? DEFAULT_ELECTRON_VERSION, + icon: rawOptions.icon, + name: typeof rawOptions.name === 'string' ? rawOptions.name : '', + out: rawOptions.out ?? process.cwd(), + overwrite: rawOptions.overwrite, + quiet: rawOptions.quiet ?? false, + platform: rawOptions.platform, + portable: rawOptions.portable ?? false, + targetUrl: + rawOptions.targetUrl === undefined + ? '' // We'll plug this in later via upgrade + : normalizeUrl(rawOptions.targetUrl), + tmpdir: false, // workaround for electron-packager#375 + upgrade: rawOptions.upgrade !== undefined ? true : false, + upgradeFrom: + (rawOptions.upgradeFrom as string) ?? + ((rawOptions.upgrade as string) || undefined), + win32metadata: rawOptions.win32metadata ?? { + ProductName: rawOptions.name, + InternalName: rawOptions.name, + FileDescription: rawOptions.name, + }, + }, + nativefier: { + accessibilityPrompt: true, + alwaysOnTop: rawOptions.alwaysOnTop ?? false, + backgroundColor: rawOptions.backgroundColor, + basicAuthPassword: rawOptions.basicAuthPassword, + basicAuthUsername: rawOptions.basicAuthUsername, + blockExternalUrls: rawOptions.blockExternalUrls ?? false, + bookmarksMenu: rawOptions.bookmarksMenu, + bounce: rawOptions.bounce ?? false, + browserwindowOptions: rawOptions.browserwindowOptions, + clearCache: rawOptions.clearCache ?? false, + counter: rawOptions.counter ?? false, + crashReporter: rawOptions.crashReporter, + disableContextMenu: rawOptions.disableContextMenu ?? false, + disableDevTools: rawOptions.disableDevTools ?? false, + disableGpu: rawOptions.disableGpu ?? false, + diskCacheSize: rawOptions.diskCacheSize, + disableOldBuildWarning: + rawOptions.disableOldBuildWarningYesiknowitisinsecure ?? false, + enableEs3Apis: rawOptions.enableEs3Apis ?? false, + fastQuit: rawOptions.fastQuit ?? false, + fileDownloadOptions: rawOptions.fileDownloadOptions, + flashPluginDir: rawOptions.flashPath, + fullScreen: rawOptions.fullScreen ?? false, + globalShortcuts: undefined, + hideWindowFrame: rawOptions.hideWindowFrame ?? false, + ignoreCertificate: rawOptions.ignoreCertificate ?? false, + ignoreGpuBlacklist: rawOptions.ignoreGpuBlacklist ?? false, + inject: rawOptions.inject ?? [], + insecure: rawOptions.insecure ?? false, + internalUrls: rawOptions.internalUrls, + lang: rawOptions.lang, + maximize: rawOptions.maximize ?? false, + nativefierVersion: packageJson.version, + quiet: rawOptions.quiet ?? false, + processEnvs: rawOptions.processEnvs, + proxyRules: rawOptions.proxyRules, + showMenuBar: rawOptions.showMenuBar ?? false, + singleInstance: rawOptions.singleInstance ?? false, + strictInternalUrls: rawOptions.strictInternalUrls ?? false, + titleBarStyle: rawOptions.titleBarStyle, + tray: rawOptions.tray ?? 'false', + userAgent: rawOptions.userAgent, + userAgentHonest: rawOptions.userAgentHonest ?? false, + verbose: rawOptions.verbose ?? false, + versionString: rawOptions.versionString, + width: rawOptions.width ?? 1280, + height: rawOptions.height ?? 800, + minWidth: rawOptions.minWidth, + minHeight: rawOptions.minHeight, + maxWidth: rawOptions.maxWidth, + maxHeight: rawOptions.maxHeight, + widevine: rawOptions.widevine ?? false, + x: rawOptions.x, + y: rawOptions.y, + zoom: rawOptions.zoom ?? 1.0, + }, + }; + + if (options.nativefier.verbose) { + log.setLevel('trace'); + try { + debug.enable('electron-packager'); + } catch (err: unknown) { + log.error( + 'Failed to enable electron-packager debug output. This should not happen,', + 'and suggests their internals changed. Please report an issue.', + err, + ); + } + + log.debug( + 'Running in verbose mode! This will produce a mountain of logs and', + 'is recommended only for troubleshooting or if you like Shakespeare.', + ); + } else if (options.nativefier.quiet) { + log.setLevel('silent'); + } else { + log.setLevel('info'); + } + + let requestedElectronBefore16 = false; + if (options.packager.electronVersion) { + const requestedVersion: string = options.packager.electronVersion; + if (!SEMVER_VERSION_NUMBER_REGEX.exec(requestedVersion)) { + throw `Invalid Electron version number "${requestedVersion}". Aborting.`; + } + const requestedMajorVersion = parseInt(requestedVersion.split('.')[0], 10); + if (requestedMajorVersion < ELECTRON_MAJOR_VERSION) { + log.warn( + `\nATTENTION: Using **old** Electron version ${requestedVersion} as requested.`, + "\nIt's untested, bugs and horror will happen, you're on your own.", + `\nSimply abort & re-run without passing the version flag to default to ${DEFAULT_ELECTRON_VERSION}`, + ); + } + if (requestedMajorVersion < 16) { + requestedElectronBefore16 = true; + } + } + + if (options.nativefier.widevine) { + const widevineSuffix = requestedElectronBefore16 ? '-wvvmp' : '+wvcus'; + log.debug(`Using widevine release suffix "${widevineSuffix}"`); + const widevineElectronVersion = `${ + options.packager.electronVersion as string + }${widevineSuffix}`; + + try { + await axios.get( + `https://github.com/castlabs/electron-releases/releases/tag/v${widevineElectronVersion}`, + ); + } catch { + throw new Error( + `\nERROR: castLabs Electron version "${widevineElectronVersion}" does not exist. \nVerify versions at https://github.com/castlabs/electron-releases/releases. \nAborting.`, + ); + } + + options.packager.electronVersion = widevineElectronVersion; + process.env.ELECTRON_MIRROR = + 'https://github.com/castlabs/electron-releases/releases/download/'; + log.warn( + `\nATTENTION: Using the **unofficial** Electron from castLabs`, + "\nIt implements Google's Widevine Content Decryption Module (CDM) for DRM-enabled playback.", + `\nSimply abort & re-run without passing the widevine flag to default to ${ + options.packager.electronVersion !== undefined + ? options.packager.electronVersion + : DEFAULT_ELECTRON_VERSION + }`, + ); + } + + if (options.nativefier.flashPluginDir) { + options.nativefier.insecure = true; + } + + if (options.nativefier.userAgentHonest && options.nativefier.userAgent) { + options.nativefier.userAgent = undefined; + log.warn( + `\nATTENTION: user-agent AND user-agent-honest/honest were provided. In this case, honesty wins. user-agent will be ignored`, + ); + } + + options.packager.platform = normalizePlatform(options.packager.platform); + + if ( + options.nativefier.maxWidth && + options.nativefier.width && + options.nativefier.width > options.nativefier.maxWidth + ) { + options.nativefier.width = options.nativefier.maxWidth; + } + + if ( + options.nativefier.maxHeight && + options.nativefier.height && + options.nativefier.height > options.nativefier.maxHeight + ) { + options.nativefier.height = options.nativefier.maxHeight; + } + + if (options.packager.portable) { + log.info( + 'Building app as portable.', + 'SECURITY WARNING: all data accumulated in the app folder after running it', + '(including login information, cache, cookies) will be saved', + 'in the app folder. If this app is then shared with others,', + 'THEY WILL HAVE THAT ACCUMULATED DATA, POTENTIALLY INCLUDING ACCESS', + 'TO ANY ACCOUNTS YOU LOGGED INTO.', + ); + } + + if (rawOptions.globalShortcuts) { + if (typeof rawOptions.globalShortcuts === 'string') { + // This is a file we got over the command line + log.debug('Using global shortcuts file at', rawOptions.globalShortcuts); + const globalShortcuts = parseJson<GlobalShortcut[]>( + fs.readFileSync(rawOptions.globalShortcuts).toString(), + ); + options.nativefier.globalShortcuts = globalShortcuts; + } else { + // This is an object we got from an existing config in an upgrade + log.debug('Using global shortcuts object', rawOptions.globalShortcuts); + options.nativefier.globalShortcuts = rawOptions.globalShortcuts; + } + } + + await asyncConfig(options); + + return options; +} + +export function normalizePlatform(platform: string | undefined): string { + if (!platform) { + return inferPlatform(); + } + if (platform.toLowerCase() === 'windows') { + return 'win32'; + } + + if (['osx', 'mac', 'macos'].includes(platform.toLowerCase())) { + return 'darwin'; + } + + return platform.toLowerCase(); +} diff --git a/src/playwright-test.ts b/src/playwright-test.ts new file mode 100644 index 0000000..949041a --- /dev/null +++ b/src/playwright-test.ts @@ -0,0 +1,431 @@ +import { once } from 'events'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { Shell } from 'electron'; +import { + _electron, + ConsoleMessage, + Dialog, + ElectronApplication, + Page, +} from 'playwright'; + +import { getTempDir, isLinux } from './helpers/helpers'; +import { NativefierOptions } from '../shared/src/options/model'; + +const INJECT_DIR = path.join(__dirname, '..', 'app', 'inject'); + +const log = console; + +function sleep(milliseconds: number): Promise<void> { + return new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +} + +/** + * Debugging this? Run your playwright tests in debug mode: + * DEBUG='pw:browser*' npm run test:playwright + */ +describe('Application launch', () => { + jest.setTimeout(60000); + + let app: ElectronApplication; + let appClosed = true; + + const appMainJSPath = path.join(__dirname, '..', 'app', 'lib', 'main.js'); + const DEFAULT_CONFIG: NativefierOptions = { + targetUrl: 'https://npmjs.com', + }; + + const logFileDir = getTempDir('playwright'); + + const metaOrAlt = process.platform === 'darwin' ? 'Meta' : 'Alt'; + const metaOrCtrl = process.platform === 'darwin' ? 'Meta' : 'Control'; + + const spawnApp = async ( + playwrightConfig: NativefierOptions = { ...DEFAULT_CONFIG }, + awaitFirstWindow = true, + preventNavigation = false, + ): Promise<Page | undefined> => { + const consoleListener = (consoleMessage: ConsoleMessage): void => { + const consoleMethods: Record<string, (...args: unknown[]) => unknown> = { + debug: log.debug.bind(console), + error: log.error.bind(console), + info: log.info.bind(console), + log: log.log.bind(console), + trace: log.trace.bind(console), + warn: log.warn.bind(console), + }; + Promise.all(consoleMessage.args().map((x) => x.jsonValue())) + .then((args) => { + if (consoleMessage.type() in consoleMethods) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + consoleMethods[consoleMessage.type()]('window.console', args); + } else { + log.log('window.console', args); + } + }) + .catch(() => log.log('window.console', consoleMessage)); + }; + app = await _electron.launch({ + // Workaround for the following errors in some linux distros: + // pw:browser [pid=24716][err] [24718:0100/000000.660708:ERROR:zygote_linux.cc(650)] write: Broken pipe (32) +16ms + // pw:browser [pid=24719][err] [24719:0725/114519.722060:FATAL:setuid_sandbox_host.cc(157)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /home/parallels/Dev/nativefier/node_modules/electron/dist/chrome-sandbox is owned by root and has mode 4755. +61ms + args: isLinux() + ? ['--no-sandbox', '--disable-setuid-sandbox', appMainJSPath] + : [appMainJSPath], + env: { + LOG_FILE_DIR: logFileDir, + PLAYWRIGHT_TEST: '1', + PLAYWRIGHT_CONFIG: JSON.stringify({ + ...playwrightConfig, + // disableGpu and process.env.DISPLAY forwarding solve the following errors on Linux: + // pw:browser [pid=286188][err] [286188:0724/102939.938248:ERROR:ozone_platform_x11.cc(248)] Missing X server or $DISPLAY +77ms + // pw:browser [pid=286188][err] [286188:0724/102939.938299:ERROR:env.cc(225)] The platform failed to initialize. Exiting. +2ms + disableGpu: isLinux() ? true : undefined, + processEnvs: + isLinux() && process.env.DISPLAY + ? JSON.stringify({ DISPLAY: process.env.DISPLAY }) + : undefined, + } as NativefierOptions), + USE_LOG_FILE: '1', + VERBOSE: '1', + }, + timeout: 60000, + }); + app.on('window', (page: Page) => { + page.on('console', consoleListener); + if (preventNavigation) { + // Prevent page navigation so we can have a reliable test + page + .route('*', (route): void => { + log.info(`Preventing route: ${route.request().url()}`); + route.abort().catch((error) => { + log.error('ERROR', error); + }); + }) + .catch((error) => { + log.error('ERROR', error); + }); + } + }); + app.on('close', () => (appClosed = true)); + appClosed = false; + if (!awaitFirstWindow) { + return undefined; + } + const window = await app.firstWindow(); + // Wait for our initial page to finish loading, otherwise some tests will break + let waited = 0; + while ( + window.url() === 'about:blank' && + playwrightConfig.targetUrl !== 'about:blank' && + waited < 2000 + ) { + waited += 100; + await sleep(100); + } + return window; + }; + + beforeEach(() => { + nukeInjects(); + nukeLogs(logFileDir); + }); + + afterEach(async () => { + if (app && !appClosed) { + await app.close(); + } + if (process.env.DEBUG) { + showLogs(logFileDir); + } + }); + + test('shows an initial window', async () => { + const mainWindow = (await spawnApp()) as Page; + await mainWindow.waitForLoadState('domcontentloaded'); + expect(app.windows()).toHaveLength(1); + expect(await mainWindow.title()).toBe('npm'); + }); + + test('can inject some CSS', async () => { + const fuschia = 'rgb(255, 0, 255)'; + createInject( + 'inject.css', + `* { background-color: ${fuschia} !important; }`, + ); + const mainWindow = (await spawnApp()) as Page; + await mainWindow.waitForLoadState('domcontentloaded'); + const headerStyle = await mainWindow.$eval('header', (el) => + window.getComputedStyle(el), + ); + expect(headerStyle.backgroundColor).toBe(fuschia); + + await mainWindow.click('#nav-pricing-link'); + await mainWindow.waitForLoadState('domcontentloaded'); + const headerStylePostNavigate = await mainWindow.$eval('header', (el) => + window.getComputedStyle(el), + ); + expect(headerStylePostNavigate.backgroundColor).toBe(fuschia); + }); + + test('can inject some JS', async () => { + const alertMsg = 'hello world from inject'; + createInject( + 'inject.js', + `setTimeout(() => {alert("${alertMsg}"); }, 5000);`, // Buy ourselves 5 seconds to get the dialog handler setup + ); + const mainWindow = (await spawnApp( + { ...DEFAULT_CONFIG }, + true, + true, + )) as Page; + const [dialogPromise] = (await once( + mainWindow, + 'dialog', + )) as unknown as Promise<Dialog>[]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const dialog: Dialog = await dialogPromise; + await dialog.dismiss(); + expect(dialog.message()).toBe(alertMsg); + }); + + test('can open internal links', async () => { + const mainWindow = (await spawnApp()) as Page; + await mainWindow.waitForLoadState('domcontentloaded'); + await mainWindow.click('#nav-pricing-link'); + await mainWindow.waitForLoadState('domcontentloaded'); + expect(app.windows()).toHaveLength(1); + }); + + test('tries to open external links', async () => { + const mainWindow = (await spawnApp()) as Page; + await mainWindow.waitForLoadState('domcontentloaded'); + + // Install the mock first + await app.evaluate(({ shell }: { shell: Shell }) => { + // @ts-expect-error injecting into shell so that this promise + // can be accessed outside of this anonymous function's scope + // Not my favorite thing to do, but I could not find another way + process.openExternalPromise = new Promise((resolve) => { + shell.openExternal = async (url: string): Promise<void> => { + resolve(url); + return Promise.resolve(); + }; + }); + }); + + // Click, but don't await it - Playwright waits for stuff that does not happen when Electron does openExternal. + mainWindow + .click('#footer > div:nth-child(2) > ul > li:nth-child(2) > a') + .catch((err: unknown) => { + expect(err).toBeUndefined(); + }); + + // Go pull out our value returned by our hacky global promise + const openExternalUrl = await app.evaluate('process.openExternalPromise'); + expect(openExternalUrl).not.toBe('https://www.npmjs.com/'); + + expect(openExternalUrl).not.toBe(DEFAULT_CONFIG.targetUrl); + }); + + // Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts. + // Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved. + test.skip('keyboard shortcuts: zoom', async () => { + const mainWindow = (await spawnApp()) as Page; + await mainWindow.waitForLoadState('domcontentloaded'); + + const defaultZoom: number | undefined = await app.evaluate( + ({ BrowserWindow }) => + BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor, + ); + + expect(defaultZoom).toBeDefined(); + + await mainWindow.keyboard.press(`${metaOrCtrl}+Equal`); + const postZoomIn = await app.evaluate( + ({ BrowserWindow }): number | undefined => + BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor, + ); + + expect(postZoomIn).toBeGreaterThan(defaultZoom as number); + + await mainWindow.keyboard.press(`${metaOrCtrl}+0`); + const postZoomReset = await app.evaluate( + ({ BrowserWindow }): number | undefined => + BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor, + ); + + expect(postZoomReset).toEqual(defaultZoom); + + await mainWindow.keyboard.press(`${metaOrCtrl}+Minus`); + const postZoomOut: number | undefined = await app.evaluate( + ({ BrowserWindow }) => + BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor, + ); + + expect(postZoomOut).toBeLessThan(defaultZoom as number); + }); + + // Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts. + // Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved. + test.skip('keyboard shortcuts: back and forward', async () => { + const mainWindow = (await spawnApp()) as Page; + await mainWindow.waitForLoadState('domcontentloaded'); + + await Promise.all([ + mainWindow.click('#nav-pricing-link'), + mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' }), + ]); + + // Go back + // console.log(`${metaOrAlt}+ArrowLeft`); + await mainWindow.keyboard.press(`${metaOrAlt}+ArrowLeft`); + await mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' }); + + const backUrl = await mainWindow.evaluate(() => window.location.href); + + expect(backUrl).toBe(DEFAULT_CONFIG.targetUrl); + + // Go forward + // console.log(`${metaOrAlt}+ArrowRight`); + await mainWindow.keyboard.press(`${metaOrAlt}+ArrowRight`); + await mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' }); + + const forwardUrl = await mainWindow.evaluate(() => window.location.href); + + expect(forwardUrl).not.toBe(DEFAULT_CONFIG.targetUrl); + }); + + test('no errors thrown in console', async () => { + await spawnApp({ ...DEFAULT_CONFIG }, false); + const mainWindow = await app.firstWindow(); + mainWindow.addListener('console', (consoleMessage: ConsoleMessage) => { + try { + expect(consoleMessage.type()).not.toBe('error'); + } catch { + // Do it this way so we'll see the whole message, not just + // expect('error').not.toBe('error') + // which isn't particularly useful + expect({ + message: 'console.error called unexpectedly with', + consoleMessage: { ...consoleMessage }, + }).toBeUndefined(); + } + }); + // Give the app 5 seconds to spin up and ensure no errors happened + await new Promise((resolve) => setTimeout(resolve, 5000)); + }); + + test('basic auth', async () => { + const mainWindow = (await spawnApp({ + targetUrl: 'https://authenticationtest.com/HTTPAuth/', + basicAuthUsername: 'user', + basicAuthPassword: 'pass', + })) as Page; + await mainWindow.waitForLoadState('networkidle'); + + const documentText = await mainWindow.evaluate<string>( + 'document.documentElement.innerText', + ); + + expect(documentText).toContain('Success'); + + expect(documentText).not.toContain('Failure'); + }); + + test('basic auth - bad login', async () => { + const mainWindow = (await spawnApp({ + targetUrl: 'https://authenticationtest.com/HTTPAuth/', + basicAuthUsername: 'userbad', + basicAuthPassword: 'passbad', + })) as Page; + await mainWindow.waitForLoadState('networkidle'); + + const documentText = await mainWindow.evaluate<string>( + 'document.documentElement.innerText', + ); + + expect(documentText).not.toContain('Success'); + + expect(documentText).toContain('Failure'); + }); + + test('basic auth without pre-providing', async () => { + const mainWindow = (await spawnApp({ + targetUrl: 'https://authenticationtest.com/HTTPAuth/', + })) as Page; + await mainWindow.waitForLoadState('load'); + + // Give the app a few seconds to open the login window + await new Promise((resolve) => setTimeout(resolve, 5000)); + + const appWindows = app.windows(); + + expect(appWindows).toHaveLength(2); + + const loginWindow = appWindows.filter((x) => x !== mainWindow)[0]; + + await loginWindow.waitForLoadState('domcontentloaded'); + await loginWindow.waitForLoadState('load'); + + const usernameField = await loginWindow.$('#username-input'); + expect(usernameField).not.toBeNull(); + await usernameField?.fill('user'); + + const passwordField = await loginWindow.$('#password-input'); + expect(passwordField).not.toBeNull(); + await passwordField?.fill('pass'); + + const submitButton = await loginWindow.$('#submit-form-button'); + expect(submitButton).not.toBeNull(); + + // "Why is this here?" you may be asking yourself. + // Because for some reason, on some linux boxes, + // the click function will not work until this is done. + // Why? I do not have access to the dark incantation + // that would allow me to know such information. + log.log({ submitButton }); + + await submitButton?.click(); + + await mainWindow.waitForEvent('load'); + + const documentText = await mainWindow.evaluate<string>( + 'document.documentElement.innerText', + ); + + expect(documentText).toContain('Success'); + + expect(documentText).not.toContain('Failure'); + }); +}); + +function createInject(filename: string, contents: string): void { + fs.writeFileSync(path.join(INJECT_DIR, filename), contents); +} + +function nukeInjects(): void { + if (!fs.existsSync(INJECT_DIR)) { + return; + } + const injected = fs + .readdirSync(INJECT_DIR) + .filter((x) => x !== '_placeholder'); + injected.forEach((x) => fs.unlinkSync(path.join(INJECT_DIR, x))); +} + +function nukeLogs(logFileDir: string): void { + const logs = fs.readdirSync(logFileDir).filter((x) => x.endsWith('.log')); + logs.forEach((x) => fs.unlinkSync(path.join(logFileDir, x))); +} + +function showLogs(logFileDir: string): void { + const logs = fs.readdirSync(logFileDir).filter((x) => x.endsWith('.log')); + for (const logFile of logs) { + log.log(fs.readFileSync(path.join(logFileDir, logFile)).toString()); + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..190994b --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "outDir": "../lib", + "rootDir": ".", + // Bumping the minimum required Node version? You must bump: + // 1. package.json -> engines.node + // 2. package.json -> devDependencies.@types/node + // 3. tsconfig.json -> {target, lib} + // 4. .github/workflows/ci.yml -> node-version + // + // Here in tsconfig.json, we want to set the `target` and `lib` keys + // to the "best" values for the minimum/required version of node. + // TS doesn't offer any easy "preset" for this, so the best we have is to + // believe people who know which {syntax, library} parts of current EcmaScript + // are supported for our version of Node, and use what they recommend. + // For the current Node version, I followed + // https://stackoverflow.com/questions/59787574/typescript-tsconfig-settings-for-node-js-12 + "target": "es2019", + // In `lib` we add `dom`, to tell tsc it's okay to use the URL object (which is in Node >= 7) + "lib": [ + "es2020", + "dom" + ], + }, + "references": [ + { + "path": "../shared" + } + ] +} diff --git a/src/utils/parseUtils.test.ts b/src/utils/parseUtils.test.ts new file mode 100644 index 0000000..990d685 --- /dev/null +++ b/src/utils/parseUtils.test.ts @@ -0,0 +1,25 @@ +import { parseBoolean } from './parseUtils'; + +test.each([ + ['true', true, true], + ['1', true, true], + ['yes', true, true], + [1, true, true], + [true, true, true], + ['false', false, true], + ['0', false, true], + ['no', false, true], + [0, false, true], + [false, false, true], + [undefined, true, true], + [undefined, false, false], +])( + 'parseBoolean("%s") === %s (default = %s)', + ( + testValue: boolean | string | number | undefined, + expectedResult: boolean, + _default: boolean, + ) => { + expect(parseBoolean(testValue, _default)).toBe(expectedResult); + }, +); diff --git a/src/utils/parseUtils.ts b/src/utils/parseUtils.ts new file mode 100644 index 0000000..a853a62 --- /dev/null +++ b/src/utils/parseUtils.ts @@ -0,0 +1,63 @@ +import * as log from 'loglevel'; + +import { isWindows } from '../helpers/helpers'; + +export function parseBoolean( + val: boolean | string | number | undefined, + _default: boolean, +): boolean { + if (val === undefined) { + return _default; + } + try { + if (typeof val === 'boolean') { + return val; + } + val = String(val); + switch (val.toLocaleLowerCase()) { + case 'true': + case '1': + case 'yes': + return true; + case 'false': + case '0': + case 'no': + return false; + default: + return _default; + } + } catch { + return _default; + } +} + +export function parseBooleanOrString(val: string): boolean | string { + switch (val) { + case 'true': + return true; + case 'false': + return false; + default: + return val; + } +} + +export function parseJson<Type>(val: string): Type | undefined { + if (!val) return undefined; + try { + return JSON.parse(val) as Type; + } catch (err: unknown) { + const windowsShellHint = isWindows() + ? `\n In particular, Windows cmd doesn't have single quotes, so you have to use only double-quotes plus escaping: "{\\"someKey\\": \\"someValue\\"}"` + : ''; + + log.error( + `Unable to parse JSON value: ${val}\n` + + `JSON should look like {"someString": "someValue", "someBoolean": true, "someArray": [1,2,3]}.\n` + + ` - Only double quotes are allowed, single quotes are not.\n` + + ` - Learn how your shell behaves and escapes characters.${windowsShellHint}\n` + + ` - If unsure, validate your JSON using an online service.`, + ); + throw err; + } +} diff --git a/src/utils/sanitizeFilename.test.ts b/src/utils/sanitizeFilename.test.ts new file mode 100644 index 0000000..55cf69a --- /dev/null +++ b/src/utils/sanitizeFilename.test.ts @@ -0,0 +1,34 @@ +import { sanitizeFilename } from './sanitizeFilename'; +import { DEFAULT_APP_NAME } from '../constants'; + +describe('replacing reserved characters', () => { + const reserved = '\\/?*<>:|'; + + test('it should return a result without reserved characters', () => { + const expectedResult = 'abc'; + const param = `${reserved}${expectedResult}`; + const result = sanitizeFilename('', param); + expect(result).toBe(expectedResult); + }); + + test('it should allow non-ascii characters', () => { + const expectedResult = '微信读书'; + const param = `${reserved}${expectedResult}`; + const result = sanitizeFilename('', param); + expect(result).toBe(expectedResult); + }); + + test('when the result of replacing these characters is empty, use default', () => { + const result = sanitizeFilename('', reserved); + expect(result).toBe(DEFAULT_APP_NAME); + }); +}); + +describe('when the platform is linux', () => { + test('it should return a name without spaces', () => { + const param = 'some name'; + const expectedResult = 'somename'; + const result = sanitizeFilename('linux', param); + expect(result).toBe(expectedResult); + }); +}); diff --git a/src/utils/sanitizeFilename.ts b/src/utils/sanitizeFilename.ts new file mode 100644 index 0000000..d784fe6 --- /dev/null +++ b/src/utils/sanitizeFilename.ts @@ -0,0 +1,25 @@ +import * as log from 'loglevel'; +import sanitize = require('sanitize-filename'); + +import { DEFAULT_APP_NAME } from '../constants'; + +export function sanitizeFilename( + platform: string | undefined, + filenameToSanitize: string, +): string { + let result: string = sanitize(filenameToSanitize); + + // spaces will cause problems with Ubuntu when pinned to the dock + if (platform === 'linux') { + result = result.replace(/[\s\u200e\u200f]/g, ''); + } + + if (!result || result === '') { + result = DEFAULT_APP_NAME; + log.warn( + 'Falling back to default app name as result of filename sanitization. Use flag "--name" to set a name', + ); + } + log.debug(`Sanitized filename for ${filenameToSanitize} : ${result}`); + return result; +} diff --git a/tsconfig-base.json b/tsconfig-base.json new file mode 100644 index 0000000..e6b34d1 --- /dev/null +++ b/tsconfig-base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowJs": false, + "declaration": true, + "esModuleInterop": true, + "incremental": true, + "module": "commonjs", + "moduleResolution": "node", + "noImplicitAny": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + }, +}