diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index b579a9a24671..000000000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,46 +0,0 @@ -parser: "@typescript-eslint/parser" -env: - browser: true - es6: true # Map, etc. - jest: true - node: true - -parserOptions: - ecmaVersion: 2018 - sourceType: module - -extends: - - eslint:recommended - - plugin:@typescript-eslint/recommended - - plugin:import/recommended - - plugin:import/typescript - - plugin:prettier/recommended - # Prettier should always be last - # Removes eslint rules that conflict with prettier. - - prettier - -rules: - # Sometimes you need to add args to implement a function signature even - # if they are unused. - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }] - # For overloads. - no-dupe-class-members: off - "@typescript-eslint/no-use-before-define": off - "@typescript-eslint/no-non-null-assertion": off - "@typescript-eslint/ban-types": off - "@typescript-eslint/no-var-requires": off - "@typescript-eslint/explicit-module-boundary-types": off - "@typescript-eslint/no-explicit-any": off - "@typescript-eslint/no-extra-semi": off - eqeqeq: error - import/order: - [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] - no-async-promise-executor: off - # This isn't a real module, just types, which apparently doesn't resolve. - import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }] - -settings: - # Does not work with CommonJS unfortunately. - import/ignore: - - env-paths - - xdg-basedir diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..793a923d16eb --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Prettier 3.4.2 +9b0340a09276f93c054d705d1b9a5f24cc5dbc97 \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 91217f12cbe5..04718d97bc09 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,7 @@ -* @cdr/code-server-reviewers +* @coder/code-server ci/helm-chart/ @Matthew-Beckett @alexgorbatchev + +docs/install.md @GNUxeava + +src/node/i18n/locales/zh-cn.json @zhaozhiming diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index c0e804de52e0..000000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -name: Bug report -about: Report a bug and help us improve -title: "" -labels: "" -assignees: "" ---- - - - -## OS/Web Information - -- Web Browser: -- Local OS: -- Remote OS: -- Remote Architecture: -- `code-server --version`: - -## Steps to Reproduce - -1. -2. -3. - -## Expected - - - -## Actual - - - -## Logs - - - - - -## Screenshot - - - -## Notes - - - -This issue can be reproduced in VS Code: Yes/No diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000000..c43d5c65e23b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,126 @@ +name: Bug report +description: File a bug report +labels: ["bug", "triage"] +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + + - type: textarea + attributes: + label: OS/Web Information + description: | + examples: + - **Web Browser**: Chrome + - **Local OS**: macOS + - **Remote OS**: Ubuntu + - **Remote Architecture**: amd64 + - **`code-server --version`**: 4.0.1 + + Please do not just put "latest" for the version. + value: | + - Web Browser: + - Local OS: + - Remote OS: + - Remote Architecture: + - `code-server --version`: + validations: + required: true + + - type: textarea + attributes: + label: Steps to Reproduce + description: | + Please describe exactly how to reproduce the bug. For example: + 1. Open code-server in Firefox + 2. Install extension `foo.bar` from the extensions sidebar + 3. Run command `foo.bar.baz` + value: | + 1. + 2. + 3. + validations: + required: true + + - type: textarea + attributes: + label: Expected + description: What should happen? + validations: + required: true + + - type: textarea + attributes: + label: Actual + description: What actually happens? + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs + description: Run code-server with the --verbose flag and then paste any relevant logs from the server, from the browser console and/or the browser network tab. For issues with installation, include installation logs (i.e. output of `npm install -g code-server`). + render: shell + + - type: textarea + attributes: + label: Screenshot/Video + description: Please include a screenshot, gif or screen recording of your issue. + validations: + required: false + + - type: dropdown + attributes: + label: Does this bug reproduce in native VS Code? + description: If the bug reproduces in native VS Code, submit the issue upstream instead (https://github.com/microsoft/vscode). + options: + - Yes, this is also broken in native VS Code + - No, this works as expected in native VS Code + - This cannot be tested in native VS Code + - I did not test native VS Code + validations: + required: true + + - type: dropdown + attributes: + label: Does this bug reproduce in VS Code web? + description: If the bug reproduces in VS Code web, submit the issue upstream instead (https://github.com/microsoft/vscode). You can run VS Code web with `code serve-web` (this is not the same as vscode.dev). + options: + - Yes, this is also broken in VS Code web + - No, this works as expected in VS Code web + - This cannot be tested in VS Code web + - I did not test VS Code web + validations: + required: true + + - type: dropdown + attributes: + label: Does this bug reproduce in GitHub Codespaces? + description: If the bug reproduces in GitHub Codespaces, submit the issue upstream instead (https://github.com/microsoft/vscode). + options: + - Yes, this is also broken in GitHub Codespaces + - No, this works as expected in GitHub Codespaces + - This cannot be tested in GitHub Codespaces + - I did not test GitHub Codespaces + validations: + required: true + + - type: checkboxes + attributes: + label: Are you accessing code-server over a secure context? + description: code-server relies on service workers (which only work in secure contexts) for many features. Double-check that you are using a secure context like HTTPS or localhost. + options: + - label: I am using a secure context. + required: false + + - type: textarea + attributes: + label: Notes + description: Please include any addition notes that will help us resolve this issue. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2f567fce310b..e24498346339 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Question - url: https://github.com/cdr/code-server/discussions/new?category_id=22503114 + - name: Question? + url: https://github.com/coder/code-server/discussions/new?category_id=22503114 about: Ask the community for help on our GitHub Discussions board - - name: Chat - about: Need immediate help or just want to talk? Hop in our Slack + - name: code-server Slack Community + about: Need immediate help or just want to talk? Hop in our Slack. Note - this Slack is not actively monitored by code-server maintainers. url: https://cdr.co/join-community diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md index ba63b11bdf8f..75240e7f7631 100644 --- a/.github/ISSUE_TEMPLATE/doc.md +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -1,7 +1,11 @@ --- name: Documentation improvement about: Suggest a documentation improvement -title: "" labels: "docs" -assignees: "" --- + +## What is your suggestion? + +## How will this improve the docs? + +## Are you interested in submitting a PR for this? diff --git a/.github/ISSUE_TEMPLATE/extension-request.md b/.github/ISSUE_TEMPLATE/extension-request.md deleted file mode 100644 index 97f6059ae045..000000000000 --- a/.github/ISSUE_TEMPLATE/extension-request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Extension request -about: Request an extension missing from the code-server marketplace -title: "" -labels: extension-request -assignees: "" ---- - - - -- [ ] Extension name: -- [ ] Extension GitHub or homepage: diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 3f7411eefb6b..89837e3441e6 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,13 +1,13 @@ --- name: Feature request -about: Suggest an idea -title: "" -labels: feature -assignees: "" +about: Suggest an idea to improve code-server +labels: enhancement --- - +## Why do you want this feature? + +## Are there any workarounds to get this functionality today? + +## Are you interested in submitting a PR for this? diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md deleted file mode 100644 index 2ca6b100bbf9..000000000000 --- a/.github/ISSUE_TEMPLATE/release.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Release -about: "*For maintainers only*" -title: "release: 0.0.0" -labels: "" -assignees: "@cdr/code-server-reviewers" ---- - - - -## Checklist - -- [ ] Assign to next release manager -- [ ] Close previous release milestone -- [ ] Create next release milestone -- [ ] Associate issue with next release milestone diff --git a/.github/PULL_REQUEST_TEMPLATE/release_template.md b/.github/PULL_REQUEST_TEMPLATE/release_template.md deleted file mode 100644 index 1712e320beb8..000000000000 --- a/.github/PULL_REQUEST_TEMPLATE/release_template.md +++ /dev/null @@ -1,16 +0,0 @@ - - -This PR is to generate a new release of `code-server` at `$CODE_SERVER_VERSION_TO_UPDATE` - -## Screenshot - -TODO - -## TODOs - -Follow "Publishing a release" steps in `ci/README.md` - - - -- [ ] publish release and merge PR -- [ ] update the AUR package diff --git a/.github/codecov.yml b/.github/codecov.yml index 7d791f3d772a..c2e23821411d 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -8,6 +8,14 @@ coverage: range: "40...70" status: patch: off + notify: + slack: + default: + url: secret:v1::tXC7VwEIKYjNU8HRgRv2GdKOSCt5UzpykKZb+o1eCDqBgb2PEqwE3A26QUPYMLo4BO2qtrJhFIvwhUvlPwyzDCNGoNiuZfXr0UeZZ0y1TcZu672R/NBNMwEPO/e1Ye0pHxjzKHnuH7HqbjFucox/RBQLtiL3J56SWGE3JtbkC6o= + threshold: 1% + only_pulls: false + branches: + - "main" parsers: gcov: diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000000..aabb28ecc8d9 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,33 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + time: "06:00" + timezone: "America/Chicago" + cooldown: + default-days: 7 + labels: [] + commit-message: + prefix: "chore" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + time: "06:00" + timezone: "America/Chicago" + cooldown: + default-days: 7 + labels: [] + ignore: + # Ignore patch updates for all dependencies + - dependency-name: "*" + update-types: + - version-update:semver-patch + # Ignore major updates to Node.js types, because they need to + # correspond to the Node.js engine version + - dependency-name: "@types/node" + update-types: + - version-update:semver-major diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 2dfb6cf38862..000000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 90 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: false - -# Issues and pull requests with these labels will be ignored. Set to `[]` to disable -exemptLabels: [] - -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: false - -# Comment to post before locking. Set to `false` to disable -lockComment: > - This thread has been automatically locked since there has not been - any recent activity after it was closed. Please open a new issue for - related bugs. - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: true -# Limit to only `issues` or `pulls` -# only: issues - -# Optionally, specify configuration settings just for `issues` or `pulls` -# issues: -# exemptLabels: -# - help-wanted -# lockLabel: outdated - -# pulls: -# daysUntilLock: 30 - -# Repository to extend settings from -# _extends: repo diff --git a/.github/ranger.yml b/.github/ranger.yml deleted file mode 100644 index 6cba1cce575d..000000000000 --- a/.github/ranger.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Configuration for the repo ranger bot -# See docs: https://www.notion.so/Documentation-8d7627bb1f3c42b7b1820e8d6f157a57#9879d1374fab4d1f9c607c230fd5123d -default: - close: - # Default time to wait before closing the label. Can either be a number in milliseconds - # or a string specified by the `ms` package (https://www.npmjs.com/package/ms) - delay: "2 days" - - # Default comment to post when an issue is first marked with a closing label - comment: "⚠️ This issue has been marked $LABEL and will be closed in $DELAY." - -labels: - duplicate: close - wontfix: close - "squash when passing": merge - "rebase when passing": merge - "merge when passing": merge - "new contributor": - action: comment - delay: 5s - message: "Thanks for making your first contribution! :slightly_smiling_face:" - extension-request: - action: close - delay: 5s - comment: > - Thanks for opening an extension request! - We are currently in the process of switching extension - marketplaces and transitioning over to [Open VSX](https://open-vsx.org/). - Once https://github.com/eclipse/openvsx/issues/249 is implemented, we - can fully make this transition. Therefore, we are no longer accepting - new requests for extension requests. We suggest installing the VSIX - file and then installing into code-server as a temporary workaround. - See [docs](https://github.com/cdr/code-server/blob/main/docs/FAQ.md#installing-vsix-extensions-via-the-command-line) for more info. - "upstream:vscode": - action: close - delay: 5s - comment: > - This issue has been marked as 'upstream:vscode'. - Please file this upstream: [link to open issue](https://github.com/microsoft/vscode/issues/new/choose) - - This issue will automatically close in $DELAY. diff --git a/.github/semantic.yaml b/.github/semantic.yaml new file mode 100644 index 000000000000..55d345cc2ac9 --- /dev/null +++ b/.github/semantic.yaml @@ -0,0 +1,66 @@ +############################################################################### +# This file configures "Semantic Pull Requests", which is documented here: +# https://github.com/zeke/semantic-pull-requests +############################################################################### + +# Scopes are optionally supplied after a 'type'. For example, in +# +# feat(docs): autostart ui +# +# '(docs)' is the scope. Scopes are used to signify where the change occurred. +scopes: + # docs: changes to the code-server documentation. + - docs + + # vendor: changes to vendored dependencies. + - vendor + + # deps: changes to code-server's dependencies. + - deps + + # cs: changes to code specific to code-server. + - cs + + # cli: changes to the command-line interface. + - cli + +# We only check that the PR title is semantic. The PR title is automatically +# applied to the "Squash & Merge" flow as the suggested commit message, so this +# should suffice unless someone drastically alters the message in that flow. +titleOnly: true + +# Types are the 'tag' types in a commit or PR title. For example, in +# +# chore: fix thing +# +# 'chore' is the type. +types: + # A build of any kind. + - build + + # A user-facing change that corrects a defect in code-server. + - fix + + # Any code task that is ignored for changelog purposes. Examples include + # devbin scripts and internal-only configurations. + - chore + + # Any work performed on CI. + - ci + + # Work that directly implements or supports the implementation of a feature. + - feat + + # A refactor changes code structure without any behavioral change. + - refactor + + # A git revert for any style of commit. + - revert + + # Adding tests of any kind. Should be separate from feature or fix + # implementations. For example, if a commit adds a fix + test, it's a fix + # commit. If a commit is simply bumping coverage, it's a test commit. + - test + + # A new release. + - release diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000000..2950d0ac8de7 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,297 @@ +name: Build + +on: + push: + branches: + - main + pull_request: + branches: + - main + +# Cancel in-progress runs for pull requests when developers push +# additional changes, and serialize builds in branches. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + changes: + runs-on: ubuntu-latest + outputs: + ci: ${{ steps.filter.outputs.ci }} + code: ${{ steps.filter.outputs.code }} + deps: ${{ steps.filter.outputs.deps }} + docs: ${{ steps.filter.outputs.docs }} + helm: ${{ steps.filter.outputs.helm }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + with: + filters: | + ci: + - ".github/**" + - "ci/**" + docs: + - "docs/**" + - "README.md" + - "CHANGELOG.md" + helm: + - "ci/helm-chart/**" + code: + - "src/**" + - "test/**" + deps: + - "lib/**" + - "patches/**" + - "package-lock.json" + - "test/package-lock.json" + - id: debug + run: | + echo "${{ toJSON(steps.filter )}}" + + prettier: + name: Run prettier check + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + - run: SKIP_SUBMODULE_DEPS=1 npm ci + - run: npx prettier --check . + + doctoc: + name: Doctoc markdown files + runs-on: ubuntu-22.04 + needs: changes + if: needs.changes.outputs.docs == 'true' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + - run: SKIP_SUBMODULE_DEPS=1 npm ci + - run: npm run doctoc + + lint-helm: + name: Lint Helm chart + runs-on: ubuntu-22.04 + needs: changes + if: needs.changes.outputs.helm == 'true' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: bmuschko/setup-kubeconform@e5d264ec1eafe2314e750c429a9da7639dd5a003 # v1.1.0 + with: + kubeconform-version: "0.8.0" + - run: kubeconform ci/helm-chart + + lint-ts: + name: Lint TypeScript files + runs-on: ubuntu-22.04 + needs: changes + if: needs.changes.outputs.code == 'true' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + - run: SKIP_SUBMODULE_DEPS=1 npm ci + - run: npm run lint:ts + + lint-actions: + name: Lint GitHub Actions + runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.ci == 'true' + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - name: Check workflow files + run: | + bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.7.9 + ./actionlint -color -shellcheck= -ignore "softprops/action-gh-release" + shell: bash + + test-unit: + name: Run unit tests + runs-on: ubuntu-22.04 + needs: changes + if: needs.changes.outputs.code == 'true' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + - run: SKIP_SUBMODULE_DEPS=1 npm ci + - run: npm run test:unit + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + if: success() + with: + token: ${{ secrets.CODECOV_TOKEN }} + + build: + name: linux-x64 + runs-on: ubuntu-22.04 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + DISABLE_V8_COMPILE_CACHE: 1 + VERSION: 0.0.0 + VSCODE_TARGET: linux-x64 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + + steps: + - run: sudo apt update && sudo apt install -y libkrb5-dev + - uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # latest + with: + packages: quilt + version: 1.0 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + submodules: true + - run: quilt push -a + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + - run: SKIP_SUBMODULE_DEPS=1 npm ci + - run: npm run build + # Get Code's git hash. When this changes it means the content is + # different and we need to rebuild. + - name: Get latest lib/vscode rev + id: vscode-rev + run: echo "rev=$(git rev-parse HEAD:./lib/vscode)" >> $GITHUB_OUTPUT + # We need to rebuild when we have a new version of Code, when any of the + # patches changed, or when the code-server version changes (since it gets + # embedded into the code). Use VSCODE_CACHE_VERSION to force a rebuild. + - name: Fetch prebuilt linux-x64 Code package from cache + id: cache-vscode + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: lib/vscode-reh-web-linux-x64 + key: vscode-linux-x64-package-${{ secrets.VSCODE_CACHE_VERSION }}-${{ steps.vscode-rev.outputs.rev }}-${{ hashFiles('patches/*.diff', 'ci/build/build-vscode.sh') }} + - name: Build vscode + if: steps.cache-vscode.outputs.cache-hit != 'true' + run: | + pushd lib/vscode + npm ci + popd + npm run build:vscode + # Push up an artifact containing the linux-x64 release. + - run: KEEP_MODULES=1 npm run release + - run: tar -czf package.tar.gz release + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: linux-x64-package + path: ./package.tar.gz + + test-e2e: + name: Run e2e tests + runs-on: ubuntu-22.04 + env: + LOG_LEVEL: debug + needs: [changes, build] + if: needs.changes.outputs.code == 'true' || needs.changes.outputs.deps == 'true' || needs.changes.outputs.ci == 'true' + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + - run: SKIP_SUBMODULE_DEPS=1 npm ci + - name: Install Playwright OS dependencies + run: | + ./test/node_modules/.bin/playwright install-deps + ./test/node_modules/.bin/playwright install + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: linux-x64-package + - run: tar -xzf package.tar.gz + + - run: CODE_SERVER_TEST_ENTRY=./release npm run test:e2e + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: failed-test-videos + path: ./test/test-results + + test-e2e-proxy: + name: Run e2e tests behind proxy + runs-on: ubuntu-22.04 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LOG_LEVEL: debug + needs: [changes, build] + if: needs.changes.outputs.code == 'true' || needs.changes.outputs.deps == 'true' || needs.changes.outputs.ci == 'true' + + steps: + - name: Cache Caddy + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: caddy-cache + with: + path: | + ~/.cache/caddy + key: cache-caddy-2.5.2 + - name: Install Caddy + if: steps.caddy-cache.outputs.cache-hit != 'true' + run: | + gh release download v2.5.2 --repo caddyserver/caddy --pattern "caddy_2.5.2_linux_amd64.tar.gz" + mkdir -p ~/.cache/caddy + tar -xzf caddy_2.5.2_linux_amd64.tar.gz --directory ~/.cache/caddy + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + - run: SKIP_SUBMODULE_DEPS=1 npm ci + - name: Install Playwright OS dependencies + run: | + ./test/node_modules/.bin/playwright install-deps + ./test/node_modules/.bin/playwright install + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: linux-x64-package + - run: tar -xzf package.tar.gz + + - run: ~/.cache/caddy/caddy start --config ./ci/Caddyfile + - run: CODE_SERVER_TEST_ENTRY=./release npm run test:e2e:proxy + - run: ~/.cache/caddy/caddy stop --config ./ci/Caddyfile + if: always() + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: failed-test-videos-proxy + path: ./test/test-results diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index bf02bc67eabc..000000000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,443 +0,0 @@ -name: Build - -on: - push: - branches: - - main - pull_request: - branches: - - main - -# Note: if: success() is used in several jobs - -# this ensures that it only executes if all previous jobs succeeded. - -# if: steps.cache-yarn.outputs.cache-hit != 'true' -# will skip running `yarn install` if it successfully fetched from cache - -jobs: - prebuild: - name: Pre-build checks - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install Node.js v14 - uses: actions/setup-node@v2 - with: - node-version: "14" - - - name: Install helm - uses: azure/setup-helm@v1.1 - - - name: Fetch dependencies from cache - id: cache-yarn - uses: actions/cache@v2 - with: - path: "**/node_modules" - key: yarn-build-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - yarn-build- - - - name: Install dependencies - if: steps.cache-yarn.outputs.cache-hit != 'true' - run: yarn --frozen-lockfile - - - name: Run yarn fmt - run: yarn fmt - if: success() - - - name: Run yarn lint - run: yarn lint - if: success() - - - name: Run code-server unit tests - run: yarn test:unit - if: success() - - - name: Upload coverage report to Codecov - run: yarn coverage - if: success() - - audit-ci: - name: Run audit-ci - needs: prebuild - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install Node.js v14 - uses: actions/setup-node@v2 - with: - node-version: "14" - - - name: Fetch dependencies from cache - id: cache-yarn - uses: actions/cache@v2 - with: - path: "**/node_modules" - key: yarn-build-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - yarn-build- - - - name: Install dependencies - if: steps.cache-yarn.outputs.cache-hit != 'true' - run: yarn --frozen-lockfile - - - name: Audit for vulnerabilities - run: yarn _audit - if: success() - - build: - name: Build - needs: prebuild - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Install Node.js v14 - uses: actions/setup-node@v2 - with: - node-version: "14" - - # TODO@Teffen investigate why this omits code-oss-dev/node_modules - # - name: Fetch dependencies from cache - # id: cache-yarn - # uses: actions/cache@v2 - # with: - # path: | - # "**/node_modules" - # "**/vendor/modules" - # "**/vendor/modules/code-oss-dev/node_modules" - # key: yarn-build-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/vendor/yarn.lock') }} - # restore-keys: | - # yarn-build- - - - name: Install dependencies - # if: steps.cache-yarn.outputs.cache-hit != 'true' - run: yarn --frozen-lockfile - - - name: Build code-server - run: yarn build - - # Parse the hash of the latest commit inside vendor/modules/code-oss-dev - # use this to avoid rebuilding it if nothing changed - # How it works: the `git log` command fetches the hash of the last commit - # that changed a file inside `vendor/modules/code-oss-dev`. If a commit changes any file in there, - # the hash returned will change, and we rebuild vscode. If the hash did not change, - # (for example, a change to `src/` or `docs/`), we reuse the same build as last time. - # This saves a lot of time in CI, as compiling VSCode can take anywhere from 5-10 minutes. - - name: Get latest vendor/modules/code-oss-dev rev - id: vscode-rev - run: echo "::set-output name=rev::$(jq -r '.devDependencies["code-oss-dev"]' vendor/package.json | sed -r 's|.*#(.*)$|\1|')" - - - name: Attempt to fetch vscode build from cache - id: cache-vscode - uses: actions/cache@v2 - with: - path: | - vendor/modules/code-oss-dev/.build - vendor/modules/code-oss-dev/out-build - vendor/modules/code-oss-dev/out-vscode - vendor/modules/code-oss-dev/out-vscode-min - key: vscode-build-${{ steps.vscode-rev.outputs.rev }} - - - name: Build vscode - if: steps.cache-vscode.outputs.cache-hit != 'true' - run: yarn build:vscode - - # The release package does not contain any native modules - # and is neutral to architecture/os/libc version. - - name: Create release package - run: yarn release - if: success() - - # https://github.com/actions/upload-artifact/issues/38 - - name: Compress release package - run: tar -czf package.tar.gz release - - - name: Upload npm package artifact - uses: actions/upload-artifact@v2 - with: - name: npm-package - path: ./package.tar.gz - - # TODO: cache building yarn --production - # possibly 2m30s of savings(?) - # this requires refactoring our release scripts - package-linux-amd64: - name: x86-64 Linux build - needs: build - runs-on: ubuntu-latest - timeout-minutes: 15 - container: "centos:7" - - steps: - - uses: actions/checkout@v2 - - - name: Install Node.js v14 - uses: actions/setup-node@v2 - with: - node-version: "14" - - - name: Install development tools - run: | - yum install -y epel-release centos-release-scl - yum install -y devtoolset-9-{make,gcc,gcc-c++} jq rsync - - - name: Install nfpm and envsubst - run: | - curl -sfL https://install.goreleaser.com/github.com/goreleaser/nfpm.sh | sh -s -- -b ~/.local/bin v2.3.1 - curl -L https://github.com/a8m/envsubst/releases/download/v1.1.0/envsubst-`uname -s`-`uname -m` -o envsubst - chmod +x envsubst - mv envsubst ~/.local/bin - echo "$HOME/.local/bin" >> $GITHUB_PATH - - - name: Install yarn - run: npm install -g yarn - - - name: Download npm package - uses: actions/download-artifact@v2 - with: - name: npm-package - - - name: Decompress npm package - run: tar -xzf package.tar.gz - - # NOTE: && here is deliberate - GitHub puts each line in its own `.sh` - # file when running inside a docker container. - - name: Build standalone release - run: source scl_source enable devtoolset-9 && yarn release:standalone - - - name: Sanity test standalone release - run: yarn test:standalone-release - - - name: Build packages with nfpm - run: yarn package - - - name: Upload release artifacts - uses: actions/upload-artifact@v2 - with: - name: release-packages - path: ./release-packages - - # NOTE@oxy: - # We use Ubuntu 16.04 here, so that our build is more compatible - # with older libc versions. We used to (Q1'20) use CentOS 7 here, - # but it has a full update EOL of Q4'20 and a 'critical security' - # update EOL of 2024. We're dropping full support a few years before - # the final EOL, but I don't believe CentOS 7 has a large arm64 userbase. - # It is not feasible to cross-compile with CentOS. - - # Cross-compile notes: To compile native dependencies for arm64, - # we install the aarch64/armv7l cross toolchain and then set it as the default - # compiler/linker/etc. with the AR/CC/CXX/LINK environment variables. - # qemu-user-static on ubuntu-16.04 currently doesn't run Node correctly, - # so we just build with "native"/x86_64 node, then download arm64/armv7l node - # and then put it in our release. We can't smoke test the cross build this way, - # but this means we don't need to maintain a self-hosted runner! - - # NOTE@jsjoeio: - # We used to use 16.04 until GitHub deprecated it on September 20, 2021 - # See here: https://github.com/actions/virtual-environments/pull/3862/files - package-linux-cross: - name: Linux cross-compile builds - needs: build - runs-on: ubuntu-18.04 - timeout-minutes: 15 - strategy: - matrix: - include: - - prefix: aarch64-linux-gnu - arch: arm64 - - prefix: arm-linux-gnueabihf - arch: armv7l - - env: - AR: ${{ format('{0}-ar', matrix.prefix) }} - CC: ${{ format('{0}-gcc', matrix.prefix) }} - CXX: ${{ format('{0}-g++', matrix.prefix) }} - LINK: ${{ format('{0}-g++', matrix.prefix) }} - NPM_CONFIG_ARCH: ${{ matrix.arch }} - NODE_VERSION: v14.17.4 - - steps: - - uses: actions/checkout@v2 - - - name: Install Node.js v14 - uses: actions/setup-node@v2 - with: - node-version: "14" - - - name: Install nfpm - run: | - curl -sfL https://install.goreleaser.com/github.com/goreleaser/nfpm.sh | sh -s -- -b ~/.local/bin v2.3.1 - echo "$HOME/.local/bin" >> $GITHUB_PATH - - - name: Install cross-compiler - run: sudo apt install $PACKAGE - env: - PACKAGE: ${{ format('g++-{0}', matrix.prefix) }} - - - name: Download npm package - uses: actions/download-artifact@v2 - with: - name: npm-package - - - name: Decompress npm package - run: tar -xzf package.tar.gz - - - name: Build standalone release - run: yarn release:standalone - - - name: Replace node with cross-compile equivalent - run: | - wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NPM_CONFIG_ARCH}.tar.xz - tar -xf node-${NODE_VERSION}-linux-${NPM_CONFIG_ARCH}.tar.xz node-${NODE_VERSION}-linux-${NPM_CONFIG_ARCH}/bin/node --strip-components=2 - mv ./node ./release-standalone/lib/node - - - name: Build packages with nfpm - run: yarn package ${NPM_CONFIG_ARCH} - - - name: Upload release artifacts - uses: actions/upload-artifact@v2 - with: - name: release-packages - path: ./release-packages - - package-macos-amd64: - name: x86-64 macOS build - needs: build - runs-on: macos-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v2 - - - name: Install Node.js v14 - uses: actions/setup-node@v2 - with: - node-version: "14" - - - name: Install nfpm - run: | - curl -sfL https://install.goreleaser.com/github.com/goreleaser/nfpm.sh | sh -s -- -b ~/.local/bin v2.3.1 - echo "$HOME/.local/bin" >> $GITHUB_PATH - - - name: Download npm package - uses: actions/download-artifact@v2 - with: - name: npm-package - - - name: Decompress npm package - run: tar -xzf package.tar.gz - - - name: Build standalone release - run: yarn release:standalone - - - name: Sanity test standalone release - run: yarn test:standalone-release - - - name: Build packages with nfpm - run: yarn package - - - name: Upload release artifacts - uses: actions/upload-artifact@v2 - with: - name: release-packages - path: ./release-packages - - test-e2e: - name: End-to-end tests - needs: package-linux-amd64 - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - # Since we build code-server we might as well run tests from the release - # since VS Code will load faster due to the bundling. - CODE_SERVER_TEST_ENTRY: "./release-packages/code-server-linux-amd64" - steps: - - uses: actions/checkout@v2 - - - name: Install Node.js v14 - uses: actions/setup-node@v2 - with: - node-version: "14" - - - name: Install playwright OS dependencies - run: npx playwright install-deps - - - name: Fetch dependencies from cache - id: cache-yarn - uses: actions/cache@v2 - with: - path: "**/node_modules" - key: yarn-build-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - yarn-build- - - - name: Download release packages - uses: actions/download-artifact@v2 - with: - name: release-packages - path: ./release-packages - - - name: Untar code-server release - run: | - cd release-packages - tar -xzf code-server*-linux-amd64.tar.gz - mv code-server*-linux-amd64 code-server-linux-amd64 - - - name: Install dependencies - if: steps.cache-yarn.outputs.cache-hit != 'true' - run: yarn --frozen-lockfile - - # HACK: this shouldn't need to exist, but put it here anyway - # in an attempt to solve Playwright cache failures. - - name: Reinstall playwright - if: steps.cache-yarn.outputs.cache-hit == 'true' - run: | - cd test/ - rm -r node_modules/playwright - yarn install --check-files - - - name: Run end-to-end tests - run: yarn test:e2e - - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v2 - with: - name: failed-test-videos - path: ./test/test-results - - - name: Remove release packages and test artifacts - run: rm -rf ./release-packages ./test/test-results - - trivy-scan-repo: - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Run Trivy vulnerability scanner in repo mode - #Commit SHA for v0.0.17 - uses: aquasecurity/trivy-action@8eccb5539730451af599c84f444c6d6cf0fc2bb0 - with: - scan-type: "fs" - scan-ref: "." - ignore-unfixed: true - format: "template" - template: "@/contrib/sarif.tpl" - output: "trivy-repo-results.sarif" - severity: "HIGH,CRITICAL" - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v1 - with: - sarif_file: "trivy-repo-results.sarif" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 3b41e3d9b2ad..000000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "Code Scanning" - -on: - push: - branches: [main] - pull_request: - # The branches below must be a subset of the branches above - branches: [main] - schedule: - # Runs every Monday morning PST - - cron: "17 15 * * 1" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-20.04 - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - config-file: ./.github/codeql-config.yml - languages: javascript - - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml deleted file mode 100644 index 12a22853f8b2..000000000000 --- a/.github/workflows/docker.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Publish on Docker - -on: - # Shows the manual trigger in GitHub UI - # helpful as a back-up in case the GitHub Actions Workflow fails - workflow_dispatch: - - release: - types: [published] - -jobs: - docker-images: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Run ./ci/steps/docker-buildx-push.sh - run: ./ci/steps/docker-buildx-push.sh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/docs-preview.yaml b/.github/workflows/docs-preview.yaml deleted file mode 100644 index 3f4ed05ce5a0..000000000000 --- a/.github/workflows/docs-preview.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: Docs preview - -on: - pull_request: - branches: - - main - -permissions: - actions: none - checks: none - contents: read - deployments: none - issues: none - packages: none - pull-requests: write - repository-projects: none - security-events: none - statuses: none - -jobs: - preview: - name: Docs preview - runs-on: ubuntu-20.04 - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 - - - name: Checkout m - uses: actions/checkout@v2 - with: - repository: cdr/m - ref: refs/heads/master - token: ${{ secrets.GH_ACCESS_TOKEN }} - submodules: true - fetch-depth: 0 - - - name: Install Node.js - uses: actions/setup-node@v2 - with: - node-version: 12.x - - - name: Cache Node Modules - uses: actions/cache@v2 - with: - path: "/node_modules" - key: node-${{ hashFiles('yarn.lock') }} - - - name: Create Deployment - id: deployment - run: ./ci/scripts/github_deployment.sh create - env: - GITHUB_TOKEN: ${{ github.token }} - DEPLOY_ENVIRONMENT: codercom-preview-docs - - - name: Deploy Preview to Vercel - id: preview - run: ./ci/scripts/deploy_vercel.sh - env: - VERCEL_ORG_ID: team_tGkWfhEGGelkkqUUm9nXq17r - VERCEL_PROJECT_ID: QmZRucMRh3GFk1817ZgXjRVuw5fhTspHPHKct3JNQDEPGd - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - CODE_SERVER_DOCS_MAIN_BRANCH: ${{ github.event.pull_request.head.sha }} - - - name: Install node_modules - run: yarn install - - - name: Check docs - run: yarn ts-node ./product/coder.com/site/scripts/checkDocs.ts - env: - BASE_URL: ${{ steps.preview.outputs.url }} - - - name: Update Deployment - # If we don't specify always, it won't run this check if failed. - # This means the deployment would be stuck pending. - if: always() - run: ./ci/scripts/github_deployment.sh update - env: - GITHUB_DEPLOYMENT: ${{ steps.deployment.outputs.id }} - GITHUB_TOKEN: ${{ github.token }} - DEPLOY_STATUS: ${{ steps.preview.outcome }} - DEPLOY_URL: ${{ steps.preview.outputs.url }} - - - name: Comment Credentials - uses: marocchino/sticky-pull-request-comment@v2 - if: always() - with: - header: codercom-preview-docs - message: | - ✨ Coder.com for PR #${{ github.event.number }} deployed! It will be updated on every commit. - - * _Host_: ${{ steps.preview.outputs.url }}/docs/code-server - * _Last deploy status_: ${{ steps.preview.outcome }} - * _Commit_: ${{ github.event.pull_request.head.sha }} - * _Workflow status_: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/installer.yaml b/.github/workflows/installer.yaml new file mode 100644 index 000000000000..1777844dd11b --- /dev/null +++ b/.github/workflows/installer.yaml @@ -0,0 +1,76 @@ +name: Installer integration + +on: + push: + branches: + - main + paths: + - "install.sh" + - ".github/workflows/installer.yaml" + pull_request: + branches: + - main + paths: + - "install.sh" + - ".github/workflows/installer.yaml" + +# Cancel in-progress runs for pull requests when developers push +# additional changes, and serialize builds in branches. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + ubuntu: + name: Test installer on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + - name: Install code-server + run: ./install.sh + + - name: Test code-server was installed globally + run: code-server --help + + alpine: + name: Test installer on Alpine + runs-on: ubuntu-latest + container: "alpine:3.17" + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + - name: Install curl + run: apk add curl + + - name: Add user + run: adduser coder --disabled-password + + # Standalone should work without root. + - name: Test standalone to a non-existent prefix + run: su coder -c "./install.sh --method standalone --prefix /tmp/does/not/yet/exist" + + # We do not actually have Alpine standalone builds so running code-server + # will not work. + - name: Test code-server was installed to prefix + run: test -f /tmp/does/not/yet/exist/bin/code-server + + macos: + name: Test installer on macOS + runs-on: macos-latest + + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + - name: Install code-server + run: ./install.sh + + - name: Test code-server was installed globally + run: code-server --help diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml deleted file mode 100644 index 959397f4fbb8..000000000000 --- a/.github/workflows/installer.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Installer integration - -on: - push: - branches: - - main - paths: - - "installer.sh" - pull_request: - branches: - - main - -jobs: - ubuntu: - name: Test installer on Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install code-server - run: ./install.sh - - - name: Test code-server - run: yarn test:standalone-release code-server - - alpine: - name: Test installer on Alpine - runs-on: ubuntu-latest - container: "alpine:3.14" - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install curl - run: apk add curl - - - name: Add user - run: adduser coder --disabled-password - - # Standalone should work without root. - - name: Test standalone to a non-existent prefix - run: su coder -c "./install.sh --method standalone --prefix /tmp/does/not/yet/exist" - - macos: - name: Test installer on macOS - runs-on: macos-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install code-server - run: ./install.sh - - - name: Test code-server - run: yarn test:standalone-release code-server diff --git a/.github/workflows/npm-brew.yaml b/.github/workflows/npm-brew.yaml deleted file mode 100644 index 107f5ea05e2c..000000000000 --- a/.github/workflows/npm-brew.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Publish on npm and brew - -on: - # Shows the manual trigger in GitHub UI - # helpful as a back-up in case the GitHub Actions Workflow fails - workflow_dispatch: - - release: - types: [published] - -jobs: - # NOTE: this job requires curl, jq and yarn - # All of them are included in ubuntu-latest. - npm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Run ./ci/steps/publish-npm.sh - run: ./ci/steps/publish-npm.sh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - homebrew: - # The newest version of code-server needs to be available on npm when this runs - # otherwise, it will 404 and won't open a PR to bump version on homebrew/homebrew-core - needs: npm - runs-on: macos-latest - steps: - # Ensure things are up to date - # Suggested by homebrew maintainers - # https://github.com/Homebrew/discussions/discussions/1532#discussioncomment-782633 - - name: Set up Homebrew - id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@master - - - uses: actions/checkout@v2 - - name: Configure git - run: | - git config user.name github-actions - git config user.email github-actions@github.com - - name: Bump code-server homebrew version - env: - HOMEBREW_GITHUB_API_TOKEN: ${{secrets.HOMEBREW_GITHUB_API_TOKEN}} - run: ./ci/steps/brew-bump.sh diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000000..95299ce9e93b --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,167 @@ +name: Publish code-server + +on: + # Shows the manual trigger in GitHub UI + # helpful as a back-up in case the GitHub Actions Workflow fails + workflow_dispatch: + inputs: + version: + type: string + required: true + + release: + types: [released] + +# Cancel in-progress runs for pull requests when developers push +# additional changes, and serialize builds in branches. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + npm: + runs-on: ubuntu-latest + env: + TAG: ${{ inputs.version || github.ref_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_ENVIRONMENT: "production" + + steps: + - name: Set version to tag without leading v + run: | + echo "VERSION=${TAG#v}" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + + - uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1.13 + with: + repository: "coder/code-server" + tag: ${{ env.TAG }} + fileName: "package.tar.gz" + out-file-path: "release-npm-package" + + - run: tar -xzf release-npm-package/package.tar.gz + - run: | + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc + pushd release + npm publish --tag latest --access public + + aur: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} + TAG: ${{ inputs.version || github.ref_name }} + + steps: + - name: Set version to tag without leading v + run: | + echo "VERSION=${TAG#v}" >> $GITHUB_ENV + + - name: Checkout code-server-aur repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + repository: "cdrci/code-server-aur" + token: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} + ref: "master" + + - name: Configure git + run: | + git config --global user.name cdrci + git config --global user.email opensource@coder.com + + - name: Fetch and reset master + run: | + git remote add upstream https://github.com/coder/code-server-aur.git + git fetch upstream + git reset --hard upstream/master + git push --force + + - name: Validate package + uses: heyhusen/archlinux-package-action@c9f94059ccbebe8710d31d582f33ef4e84fe575c # v3.0.0 + with: + pkgver: ${{ env.VERSION }} + updpkgsums: true + srcinfo: true + + - name: Open PR + run: | + git checkout -b update-version-${{ env.VERSION }} + git add . + git commit -m "Update to ${{ env.VERSION }}" + git push -u origin $(git branch --show) + gh pr create --repo coder/code-server-aur --title "Update to ${{ env.VERSION }}" --body "PR opened by @$GITHUB_ACTOR" --assignee $GITHUB_ACTOR + + docker: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ github.token }} + TAG: ${{ inputs.version || github.ref_name }} + + steps: + - name: Set version to tag without leading v + run: | + echo "VERSION=${TAG#v}" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1.13 + with: + repository: "coder/code-server" + tag: v${{ env.VERSION }} + fileName: "*.deb" + out-file-path: "release-packages" + - uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1.13 + with: + repository: "coder/code-server" + tag: v${{ env.VERSION }} + fileName: "*.rpm" + out-file-path: "release-packages" + + - run: npm run publish:docker + + repo: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} + TAG: ${{ inputs.version || github.ref_name }} + needs: docker + + steps: + - name: Set version to tag without leading v + run: | + echo "VERSION=${TAG#v}" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + - run: ./ci/build/update-repo.sh + + - name: Open PR + run: | + git config --global user.name cdrci + git config --global user.email opensource@coder.com + git checkout -b "helm/$VERSION" + git add . + git commit -m "Update Helm chart and changelog with $VERSION" + git push -u origin "$(git branch --show)" + gh pr create \ + --repo coder/code-server \ + --body-file .cache/checklist \ + --title "Update Helm chart and changelog with $VERSION" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000000..fc4a126fb125 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,198 @@ +name: Draft release + +on: + workflow_dispatch: + inputs: + version: + type: string + required: true + pull_request_target: + types: + - closed + branches: + - main + +permissions: + contents: write # For creating releases. + discussions: write # For creating a discussion. + +# Cancel in-progress runs for pull requests when developers push +# additional changes +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + package-linux: + name: ${{ format('linux-{0}', matrix.vscode_arch) }} + runs-on: ubuntu-22.04 + if: >- + (github.event_name == 'workflow_dispatch') || + (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && startsWith(github.head_ref, 'update/')) + + strategy: + matrix: + include: + - npm_arch: x64 + vscode_arch: x64 + package_arch: amd64 + - npm_arch: arm64 + vscode_arch: arm64 + package_arch: arm64 + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + TAG: ${{ inputs.version || github.event.pull_request.head.ref || github.ref_name }} + # Set release package name. + ARCH: ${{ matrix.package_arch }} + # Cross-compile target. + VSCODE_ARCH: ${{ matrix.vscode_arch }} + npm_config_arch: ${{ matrix.npm_arch }} + # Ensure native modules are built from source to avoid prebuilds and use + # the correct version of glibc. + npm_config_build_from_source: true + # Gulp target name. + # TODO: Pull from VSCODE_ARCH instead. + VSCODE_TARGET: ${{ format('linux-{0}', matrix.vscode_arch) }} + + steps: + - run: sudo apt update && sudo apt install -y libkrb5-dev + - uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # latest + with: + packages: quilt + version: 1.0 + - name: Install nfpm + run: | + mkdir -p ~/.local/bin + curl -sSfL https://github.com/goreleaser/nfpm/releases/download/v2.3.1/nfpm_2.3.1_`uname -s`_`uname -m`.tar.gz | tar -C ~/.local/bin -zxv nfpm + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Strip update/ and v from tag and set major version + run: | + version=${TAG#update/} + version=${version#v} + version=4${version:1} + echo "VERSION=$version" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + submodules: true + - run: quilt push -a + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + + - name: Build + run: | + cd lib/vscode/build + npm ci + cd .. + source ./build/azure-pipelines/linux/setup-env.sh + # Run preinstall script before root dependencies are installed + # so that v8 headers are patched correctly for native modules. + node build/npm/preinstall.ts + cd ../.. + npm ci + npm run build + npm run build:vscode + + # Platform-agnostic NPM package. + - run: npm run release + if: ${{ matrix.vscode_arch == 'x64' }} + - run: tar -czf package.tar.gz release + if: ${{ matrix.vscode_arch == 'x64' }} + - run: | + sed "/^## Unreleased/,/^## / ! d" CHANGELOG.md | head -n -2 | tail -n +3 > .cache/release-notes + if: ${{ matrix.vscode_arch == 'x64' }} + - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 + if: ${{ matrix.vscode_arch == 'x64' }} + with: + draft: true + discussion_category_name: "📣 Announcements" + files: package.tar.gz + tag_name: v${{ env.VERSION }} + name: v${{ env.VERSION }} + body_path: .cache/release-notes + + # Platform-specific release. + - run: KEEP_MODULES=1 npm run release + - run: npm run package + - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 + with: + draft: true + discussion_category_name: "📣 Announcements" + files: ./release-packages/* + tag_name: v${{ env.VERSION }} + name: v${{ env.VERSION }} + + package-macos: + name: ${{ matrix.vscode_target }} + runs-on: ${{ matrix.os }} + if: >- + (github.event_name == 'workflow_dispatch') || + (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && startsWith(github.head_ref, 'update/')) + strategy: + matrix: + include: + - os: macos-15-intel + vscode_target: darwin-x64 + - os: macos-latest + vscode_target: darwin-arm64 + env: + VSCODE_TARGET: ${{ matrix.vscode_target }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.version || github.event.pull_request.head.ref || github.ref_name }} + # Ensure native modules are built from source to avoid prebuilds. + npm_config_build_from_source: true + + steps: + # The version of node-gyp we use depends on distutils but it was removed + # in Python 3.12. It seems to be fixed in the latest node-gyp so when we + # next update Node we can probably remove this. For now, install + # setuptools since it contains distutils. + - run: brew install python-setuptools quilt + - name: Install nfpm + run: | + mkdir -p ~/.local/bin + curl -sSfL https://github.com/goreleaser/nfpm/releases/download/v2.3.1/nfpm_2.3.1_`uname -s`_`uname -m`.tar.gz | tar -C ~/.local/bin -zxv nfpm + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Strip update/ and v from tag and set major version + run: | + version=${TAG#update/} + version=${version#v} + version=4${version:1} + echo "VERSION=$version" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + submodules: true + - run: quilt push -a + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + + - run: npm ci + - run: npm run build + - run: npm run build:vscode + - run: KEEP_MODULES=1 npm run release + - run: npm run test:native + + - run: npm run package + - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 + with: + draft: true + discussion_category_name: "📣 Announcements" + files: ./release-packages/* + tag_name: v${{ env.VERSION }} + name: v${{ env.VERSION }} diff --git a/.github/workflows/scripts.yaml b/.github/workflows/scripts.yaml new file mode 100644 index 000000000000..180975866b12 --- /dev/null +++ b/.github/workflows/scripts.yaml @@ -0,0 +1,67 @@ +name: Script unit tests + +on: + push: + branches: + - main + paths: + - "**.sh" + - "**.bats" + pull_request: + branches: + - main + paths: + - "**.sh" + - "**.bats" + +permissions: + actions: none + checks: none + contents: read + deployments: none + issues: none + packages: none + pull-requests: none + repository-projects: none + security-events: none + statuses: none + +# Cancel in-progress runs for pull requests when developers push +# additional changes, and serialize builds in branches. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + test: + name: Run script unit tests + runs-on: ubuntu-latest + # This runs on Alpine to make sure we're testing with actual sh. + container: "alpine:3.17" + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + - name: Install test utilities + run: apk add bats checkbashisms + + - name: Check Bashisms + run: checkbashisms ./install.sh + + - name: Run script unit tests + run: ./ci/dev/test-scripts.sh + + lint: + name: Lint shell files + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + - name: Install lint utilities + run: sudo apt install shellcheck + + - name: Lint shell files + run: ./ci/dev/lint-scripts.sh diff --git a/.github/workflows/scripts.yml b/.github/workflows/scripts.yml deleted file mode 100644 index 7b12147b9783..000000000000 --- a/.github/workflows/scripts.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Script unit tests - -on: - push: - branches: - - main - paths: - - "installer.sh" - pull_request: - branches: - - main - -jobs: - test: - name: Run script unit tests - runs-on: ubuntu-latest - # This runs on Alpine to make sure we're testing with actual sh. - container: "alpine:3.14" - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Install test utilities - run: apk add bats checkbashisms - - - name: Check Bashisms - run: checkbashisms ./install.sh - - - name: Run script unit tests - run: ./ci/dev/test-scripts.sh diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 000000000000..fc1664965f55 --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,92 @@ +name: Security + +on: + push: + branches: [main] + paths: + - "package.json" + pull_request: + paths: + - "package.json" + schedule: + # Runs every Monday morning PST + - cron: "17 15 * * 1" + +# Cancel in-progress runs for pull requests when developers push additional +# changes, and serialize builds in branches. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + audit: + name: Audit node modules + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .node-version + + - name: Audit npm for vulnerabilities + run: npm audit + if: success() + + trivy-scan-repo: + name: Scan repo with Trivy + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + runs-on: ubuntu-22.04 + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + fetch-depth: 0 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@314ff8b43182423b84c50b1670b0e10f858f2d98 # latest + with: + scan-type: "fs" + scan-ref: "." + ignore-unfixed: true + format: "template" + template: "@/contrib/sarif.tpl" + output: "trivy-repo-results.sarif" + severity: "HIGH,CRITICAL" + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + with: + sarif_file: "trivy-repo-results.sarif" + + codeql-analyze: + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report + name: Analyze with CodeQL + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + with: + config-file: ./.github/codeql-config.yml + languages: javascript + + - name: Autobuild + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 diff --git a/.github/workflows/trivy-docker.yaml b/.github/workflows/trivy-docker.yaml new file mode 100644 index 000000000000..8ceb95d60b04 --- /dev/null +++ b/.github/workflows/trivy-docker.yaml @@ -0,0 +1,63 @@ +name: Trivy Nightly Docker Scan + +on: + # Run scans if the workflow is modified, in order to test the + # workflow itself. This results in some spurious notifications, + # but seems okay for testing. + pull_request: + branches: + - main + paths: + - .github/workflows/trivy-docker.yaml + + # Run scans against master whenever changes are merged. + push: + branches: + - main + paths: + - .github/workflows/trivy-docker.yaml + + schedule: + - cron: "15 10 * * *" + + workflow_dispatch: + +permissions: + actions: none + checks: none + contents: read + deployments: none + issues: none + packages: none + pull-requests: none + repository-projects: none + security-events: write + statuses: none + +# Cancel in-progress runs for pull requests when developers push +# additional changes, and serialize builds in branches. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + trivy-scan-image: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + + - name: Run Trivy vulnerability scanner in image mode + uses: aquasecurity/trivy-action@314ff8b43182423b84c50b1670b0e10f858f2d98 # latest + with: + image-ref: "docker.io/codercom/code-server:latest" + ignore-unfixed: true + format: "sarif" + output: "trivy-image-results.sarif" + severity: "HIGH,CRITICAL" + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + with: + sarif_file: "trivy-image-results.sarif" diff --git a/.github/workflows/update.yaml b/.github/workflows/update.yaml new file mode 100644 index 000000000000..b54826c79f70 --- /dev/null +++ b/.github/workflows/update.yaml @@ -0,0 +1,72 @@ +name: Update code-server + +on: + workflow_dispatch: + inputs: + version: + type: string + required: true + schedule: + - cron: "0 16,21 * * *" + +jobs: + update: + runs-on: ubuntu-latest + env: + TAG: ${{ inputs.version }} + GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} + + steps: + - name: Fetch latest tag + if: env.TAG == '' + run: | + tag=$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/microsoft/vscode/releases/latest) + tag="${tag#https://github.com/microsoft/vscode/releases/tag/}" + echo "TAG=$tag" >> $GITHUB_ENV + + - name: Remove leading v from tag + run: | + echo "VERSION=${TAG#v}" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + submodules: true + + - name: Check current version + id: check + run: | + commit="$(git -C lib/vscode rev-parse HEAD)" + if [[ $(git -C lib/vscode ls-remote --tags | grep "$commit") == */"$VERSION" ]] ; then + echo "$VERSION update has already been merged into $(git rev-parse --abbrev-ref HEAD)" + echo done=true >> $GITHUB_OUTPUT + elif git ls-remote --exit-code --heads origin "update/$VERSION" ; then + echo "There is already a PR for updating to $VERSION" + echo done=true >> $GITHUB_OUTPUT + else + echo "$VERSION update has not started yet" + echo done=false >> $GITHUB_OUTPUT + fi + + - uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # latest + if: steps.check.outputs.done == 'false' + with: + packages: quilt + version: 1.0 + + - run: ./ci/build/update-vscode.sh + if: steps.check.outputs.done == 'false' + + - name: Open PR + if: steps.check.outputs.done == 'false' + run: | + git config --global user.name cdrci + git config --global user.email opensource@coder.com + git checkout -b "update/$VERSION" + git add . + git commit -m "Update Code to $VERSION" + git push -u origin "$(git branch --show)" + gh pr create \ + --repo coder/code-server \ + --title "Update Code to $VERSION" \ + --body-file .cache/checklist \ + --draft diff --git a/.gitignore b/.gitignore index 3cc6e31d7af3..7fea491b82ac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,18 +2,23 @@ .cache /out*/ release/ -release-npm-package/ -release-standalone/ release-packages/ release-gcp/ release-images/ node_modules -vendor/modules -node-* /plugins /lib/coder-cloud-agent .home coverage **/.DS_Store +*.bak + +# Code packages itself here. +/lib/vscode-reh-web-* + # Failed e2e test videos are saved here test/test-results + +# Quilt's internal data. +/.pc +/patches/*.diff~ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..9854a1b1dd30 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/vscode"] + path = lib/vscode + url = https://github.com/microsoft/vscode diff --git a/.node-version b/.node-version index 8351c19397f4..ca5c350055cc 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14 +24.18.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..40cfb028f4d0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +lib +release-packages +release +helm-chart +test/scripts +test/e2e/extensions/test-extension +.pc +package-lock.json diff --git a/.prettierrc.yaml b/.prettierrc.yaml index a0634116d20d..b6114bf4613b 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -2,3 +2,5 @@ printWidth: 120 semi: false trailingComma: all arrowParens: always +singleQuote: false +useTabs: false diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml deleted file mode 100644 index af5c94bc4f3e..000000000000 --- a/.stylelintrc.yaml +++ /dev/null @@ -1,2 +0,0 @@ -extends: - - stylelint-config-recommended diff --git a/.tours/contributing.tour b/.tours/contributing.tour index 53befe2000e1..aaff2b500c99 100644 --- a/.tours/contributing.tour +++ b/.tours/contributing.tour @@ -50,7 +50,7 @@ { "file": "src/node/heart.ts", "line": 7, - "description": "code-server's heart beats to indicate recent activity.\n\nAlso documented here: [https://github.com/cdr/code-server/blob/master/docs/FAQ.md#heartbeat-file](https://github.com/cdr/code-server/blob/master/docs/FAQ.md#heartbeat-file)" + "description": "code-server's heart beats to indicate recent activity.\n\nAlso documented here: [https://github.com/coder/code-server/blob/main/docs/FAQ.md#heartbeat-file](https://github.com/coder/code-server/blob/main/docs/FAQ.md#heartbeat-file)" }, { "file": "src/node/socket.ts", @@ -80,12 +80,12 @@ { "file": "src/node/routes/domainProxy.ts", "line": 18, - "description": "code-server provides a built-in proxy to help in developing web-based applications. This is the code for the domain-based proxy.\n\nAlso documented here: [https://github.com/cdr/code-server/blob/master/docs/FAQ.md#how-do-i-securely-access-web-services](https://github.com/cdr/code-server/blob/master/docs/FAQ.md#how-do-i-securely-access-web-services)" + "description": "code-server provides a built-in proxy to help in developing web-based applications. This is the code for the domain-based proxy.\n\nAlso documented here: [https://github.com/coder/code-server/blob/main/docs/FAQ.md#how-do-i-securely-access-web-services](https://github.com/coder/code-server/blob/main/docs/FAQ.md#how-do-i-securely-access-web-services)" }, { "file": "src/node/routes/pathProxy.ts", "line": 19, - "description": "Here is the path-based version of the proxy.\n\nAlso documented here: [https://github.com/cdr/code-server/blob/master/docs/FAQ.md#how-do-i-securely-access-web-services](https://github.com/cdr/code-server/blob/master/docs/FAQ.md#how-do-i-securely-access-web-services)" + "description": "Here is the path-based version of the proxy.\n\nAlso documented here: [https://github.com/coder/code-server/blob/main/docs/FAQ.md#how-do-i-securely-access-web-services](https://github.com/coder/code-server/blob/main/docs/FAQ.md#how-do-i-securely-access-web-services)" }, { "file": "src/node/proxy.ts", @@ -95,7 +95,7 @@ { "file": "src/node/routes/health.ts", "line": 5, - "description": "A simple endpoint that lets you see if code-server is up.\n\nAlso documented here: [https://github.com/cdr/code-server/blob/master/docs/FAQ.md#healthz-endpoint](https://github.com/cdr/code-server/blob/master/docs/FAQ.md#healthz-endpoint)" + "description": "A simple endpoint that lets you see if code-server is up.\n\nAlso documented here: [https://github.com/coder/code-server/blob/main/docs/FAQ.md#healthz-endpoint](https://github.com/coder/code-server/blob/main/docs/FAQ.md#healthz-endpoint)" }, { "file": "src/node/routes/login.ts", @@ -143,9 +143,9 @@ "description": "Static images and the manifest live here in `src/browser/media` (see the explorer)." }, { - "directory": "vendor/modules/code-oss-dev", + "directory": "lib/vscode", "line": 1, - "description": "code-server makes use of VS Code's frontend web/remote support. Most of the modifications implement the remote server since that portion of the code is closed source and not released with VS Code.\n\nWe also have a few bug fixes and have added some features (like client-side extensions). See [https://github.com/cdr/code-server/blob/master/docs/CONTRIBUTING.md#modifications-to-vs-code](https://github.com/cdr/code-server/blob/master/docs/CONTRIBUTING.md#modifications-to-vs-code) for a list.\n\nWe make an effort to keep the modifications as few as possible." + "description": "code-server makes use of VS Code's frontend web/remote support. Most of the modifications implement the remote server since that portion of the code is closed source and not released with VS Code.\n\nWe also have a few bug fixes and have added some features (like client-side extensions). See [https://github.com/coder/code-server/blob/main/docs/CONTRIBUTING.md#modifications-to-vs-code](https://github.com/coder/code-server/blob/main/docs/CONTRIBUTING.md#modifications-to-vs-code) for a list.\n\nWe make an effort to keep the modifications as few as possible." } ] } diff --git a/.tours/start-development.tour b/.tours/start-development.tour index 03f32927ebbc..999adba06125 100644 --- a/.tours/start-development.tour +++ b/.tours/start-development.tour @@ -5,7 +5,7 @@ { "file": "package.json", "line": 31, - "description": "## Commands\n\nTo start developing, make sure you have Node 14+ and the [required dependencies](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites) installed. Then, run the following commands:\n\n1. Install dependencies:\n>> yarn\n\n3. Start development mode (and watch for changes):\n>> yarn watch" + "description": "## Commands\n\nTo start developing, make sure you have Node 16+ and the [required dependencies](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites) installed. Then, run the following commands:\n\n1. Install dependencies:\n>> npm\n\n3. Start development mode (and watch for changes):\n>> npm run watch" }, { "file": "src/node/app.ts", @@ -20,7 +20,7 @@ { "file": "src/node/app.ts", "line": 62, - "description": "## That's it!\n\n\nThat's all there is to it! When this tour ends, your terminal session may stop, but just use `yarn watch` to start developing from here on out!\n\n\nIf you haven't already, be sure to check out these resources:\n- [Tour: Contributing](command:codetour.startTourByTitle?[\"Contributing\")\n- [Docs: FAQ.md](https://github.com/cdr/code-server/blob/master/docs/FAQ.md)\n- [Docs: CONTRIBUTING.md](https://github.com/cdr/code-server/blob/master/docs/CONTRIBUTING.md)\n- [Community: GitHub Discussions](https://github.com/cdr/code-server/discussions)\n- [Community: Slack](https://community.coder.com)" + "description": "## That's it!\n\n\nThat's all there is to it! When this tour ends, your terminal session may stop, but just use `npm run watch` to start developing from here on out!\n\n\nIf you haven't already, be sure to check out these resources:\n- [Tour: Contributing](command:codetour.startTourByTitle?[\"Contributing\"])\n- [Docs: FAQ.md](https://github.com/coder/code-server/blob/main/docs/FAQ.md)\n- [Docs: CONTRIBUTING.md](https://github.com/coder/code-server/blob/main/docs/CONTRIBUTING.md)\n- [Community: GitHub Discussions](https://github.com/coder/code-server/discussions)\n- [Community: Slack](https://community.coder.com)" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b7ba2851c6f..e7768348d00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,175 +1,1546 @@ # Changelog - +## [9.99.999] - 9090-09-09 - + +## Unreleased + +## [4.133.0](https://github.com/coder/code-server/releases/tag/v4.133.0) - 2026-08-17 + +Code v1.133.0 + +### Changed + +- Update to Code 1.133.0 + +## [4.132.0](https://github.com/coder/code-server/releases/tag/v4.132.0) - 2026-08-10 + +Code v1.132.0 + +### Changed + +- Update to Code 1.132.0 + +### Fixed + +- When proxying, cookies were being decoded and re-encoded, which could cause + issues for applications that encode differently. Cookies are now passed + through unchanged (aside from stripping out code-server's session token). + +## [4.131.0](https://github.com/coder/code-server/releases/tag/v4.131.0) - 2026-07-30 + +Code v1.131.0 + +### Changed + +- Update to Code 1.131.0 + +## [4.130.0](https://github.com/coder/code-server/releases/tag/v4.130.0) - 2026-07-24 + +Code v1.130.0 + +### Changed + +- Update to Code 1.130.0 + +## [4.129.0](https://github.com/coder/code-server/releases/tag/v4.129.0) - 2026-07-17 + +Code v1.129.0 + +### Changed + +- Update to Code 1.129.0 + +## [4.128.0](https://github.com/coder/code-server/releases/tag/v4.128.0) - 2026-07-11 + +Code v1.128.0 + +### Changed + +- Update to Code 1.128.0 + +## [4.127.0](https://github.com/coder/code-server/releases/tag/v4.127.0) - 2026-07-02 + +Code v1.127.0 + +### Changed + +- Update to Code 1.127.0 + +## [4.126.0](https://github.com/coder/code-server/releases/tag/v4.126.0) - 2026-06-24 + +Code v1.126.0 + +### Changed + +- Update to Code 1.126.0 + +## [4.125.0](https://github.com/coder/code-server/releases/tag/v4.125.0) - 2026-06-18 + +Code v1.125.0 + +### Changed + +- Update to Code 1.125.0 + +## [4.124.2](https://github.com/coder/code-server/releases/tag/v4.124.2) - 2026-06-16 + +Code v1.124.2 + +### Security + +- Strip code-server's session token from the cookie before proxying to a local + port. Previously, when you used built-in password authentication, the cookie + would be sent to the local proxied port, which meant if the service was + malicious and not already running as your code-server user it could use the + cookie to log into code-server and execute commands as your code-server user. + +### Changed + +- Update to Code 1.124.2 + +## [4.123.0](https://github.com/coder/code-server/releases/tag/v4.123.0) - 2026-06-03 + +Code v1.123.0 + +### Changed + +- Update to Code 1.123.0 +- Microsoft dropped support for armhf remotes so there will no longer be any + builds for armhf. + +## [4.122.1](https://github.com/coder/code-server/releases/tag/v4.122.1) - 2026-06-02 + +Code v1.122.1 + +### Changed + +- Update to Code 1.122.1 + +## [4.122.0](https://github.com/coder/code-server/releases/tag/v4.122.0) - 2026-05-29 + +Code v1.122.0 + +### Changed + +- Update to Code 1.122.0 + +### Fixed + +- `--app-name` will now affect window titles within the editor (it is now used + as the value for `${appName}` in the title template) as well as some other + places like the help > about dialog. + +### Added + +- App name can now be set with the `CODE_SERVER_APP_NAME` environment variable. + +## [4.121.0](https://github.com/coder/code-server/releases/tag/v4.121.0) - 2026-05-20 + +Code v1.121.0 + +### Changed + +- Update to Code 1.121.0 + +## [4.118.0](https://github.com/coder/code-server/releases/tag/v4.118.0) - 2026-05-06 + +Code v1.118.0 + +### Changed + +- Update to Code 1.118.0 + +## [4.117.0](https://github.com/coder/code-server/releases/tag/v4.117.0) - 2026-04-22 + +Code v1.117.0 + +### Changed + +- Update to Code 1.117.0 + +## [4.116.0](https://github.com/coder/code-server/releases/tag/v4.116.0) - 2026-04-16 + +Code v1.116.0 + +### Changed + +- Update to Code 1.116.0 + +## [4.115.0](https://github.com/coder/code-server/releases/tag/v4.115.0) - 2026-04-08 + +Code v1.115.0 + +### Changed + +- Update to Code 1.115.0 + +## [4.114.1](https://github.com/coder/code-server/releases/tag/v4.114.1) - 2026-04-06 + +Code v1.114.0 + +### Changed + +- Ensure native modules are built from source so they use the correct version of + glibc. This should bring down the requirement from 2.34 back down to 2.28. + +## [4.114.0](https://github.com/coder/code-server/releases/tag/v4.114.0) - 2026-04-04 + +Code v1.114.0 + +### Changed + +- Update to Code 1.114.0. + +## [4.113.1](https://github.com/coder/code-server/releases/tag/v4.113.1) - 2026-04-03 + +Code v1.113.0 + +This is a re-release of v4.113.0 but with the correct Node binaries for arm64 +and armv7l. Previously they were packaging the amd64 Node binary due to a +mistake while refactoring CI to use more of the upstream build scripts. + +## [4.113.0](https://github.com/coder/code-server/releases/tag/v4.113.0) - 2026-04-02 + +Code v1.113.0 + +### Changed + +- Update to Code 1.113.0 + +## [4.112.0](https://github.com/coder/code-server/releases/tag/v4.112.0) - 2026-03-19 + +Code v1.112.0 + +### Changed + +- Update to Code 1.112.0 + +## [4.111.0](https://github.com/coder/code-server/releases/tag/v4.111.0) - 2026-03-11 + +Code v1.111.0 + +### Changed + +- Update to Code 1.111.0 +- `--app-name` now affects the error page title. + +## [4.110.1](https://github.com/coder/code-server/releases/tag/v4.110.1) - 2026-03-10 + +Code v1.110.1 + +### Changed + +- Update to Code 1.110.1 + +## [4.110.0](https://github.com/coder/code-server/releases/tag/v4.110.0) - 2026-03-09 + +Code v1.110.0 + +### Changed + +- Update to Code 1.110.0 + +## [4.109.5](https://github.com/coder/code-server/releases/tag/v4.109.5) - 2026-03-02 + +Code v1.109.5 + +### Changed + +- Update to Code 1.109.5 + +## [4.109.2](https://github.com/coder/code-server/releases/tag/v4.109.2) - 2026-02-12 + +Code v1.109.2 + +### Changed + +- Update to Code 1.109.2 + +## [4.109.0](https://github.com/coder/code-server/releases/tag/v4.109.0) - 2026-02-12 + +Code v1.109.0 + +### Changed + +- Update to Code 1.109.0 + +## [4.108.2](https://github.com/coder/code-server/releases/tag/v4.108.2) - 2026-01-26 + +Code v1.108.2 + +### Changed + +- Update to Code 1.108.2 + +## [4.108.1](https://github.com/coder/code-server/releases/tag/v4.108.1) - 2026-01-16 + +Code v1.108.1 + +### Changed + +- Update to Code 1.108.1 + +## [4.108.0](https://github.com/coder/code-server/releases/tag/v4.108.0) - 2026-01-12 + +Code v1.108.0 + +### Changed + +- Update to Code 1.108.0 + +## [4.107.1](https://github.com/coder/code-server/releases/tag/v4.107.1) - 2026-01-09 + +Code v1.107.1 + +### Changed + +- Update to Code 1.107.1 + +## [4.107.0](https://github.com/coder/code-server/releases/tag/v4.107.0) - 2026-12-17 + +Code v1.107.0 + +### Changed + +- Update to Code 1.107.0 + +### Added + +- New `--cookie-suffix` flag that can be used to add a suffix to the cookie when + using the built-in password authentication. You can use this to avoid + collisions with other instances of code-server. +- Support a new property `authorizationHeaderToken` on the extension gallery + configuration. This will be added to marketplace requests as a bearer token + using the `Authorization` header. + +## [4.106.3](https://github.com/coder/code-server/releases/tag/v4.106.3) - 2025-12-01 + +Code v1.106.3 + +### Changed + +- Update to Code 1.106.3 + +## [4.106.2](https://github.com/coder/code-server/releases/tag/v4.106.2) - 2025-11-19 + +Code v1.106.2 + +### Changed + +- Update to Code 1.106.2 + +## [4.106.0](https://github.com/coder/code-server/releases/tag/v4.106.0) - 2025-11-19 + +Code v1.106.0 + +### Changed + +- Update to Code 1.106.0 + +## [4.105.1](https://github.com/coder/code-server/releases/tag/v4.105.1) - 2025-10-20 + +Code v1.105.1 + +### Changed + +- Update to Code 1.105.1 + +## [4.105.0](https://github.com/coder/code-server/releases/tag/v4.105.0) - 2025-10-17 + +Code v1.105.0 + +### Changed + +- Update to Code 1.105.0 + +## [4.104.3](https://github.com/coder/code-server/releases/tag/v4.104.3) - 2025-10-07 + +Code v1.104.3 + +### Changed + +- Update to Code 1.104.3. + +## [4.104.2](https://github.com/coder/code-server/releases/tag/v4.104.2) - 2025-09-26 + +Code v1.104.2 + +### Changed + +- Update to Code 1.104.2. + +## [4.104.1](https://github.com/coder/code-server/releases/tag/v4.104.1) - 2025-09-19 + +Code v1.104.1 + +### Changed + +- Update to Code 1.104.1. + +## [4.104.0](https://github.com/coder/code-server/releases/tag/v4.104.0) - 2025-09-15 + +Code v1.104.0 + +### Fixed + +- Fix "extension not found" errors from Open VSX when trying to install the + latest version of an extension. + +### Changed + +- Update to Code 1.104.0. + +## [4.103.2](https://github.com/coder/code-server/releases/tag/v4.103.2) - 2025-08-25 + +Code v1.103.2 + +### Changed + +- Update to Code 1.103.2. + +## [4.103.1](https://github.com/coder/code-server/releases/tag/v4.103.1) - 2025-08-15 + +Code v1.103.1 + +### Changed + +- Update to Code 1.103.1. + +## [4.103.0](https://github.com/coder/code-server/releases/tag/v4.103.0) - 2025-08-12 + +Code v1.103.0 + +### Changed + +- Update to Code 1.103.0. + +## [4.102.2](https://github.com/coder/code-server/releases/tag/v4.102.2) - 2025-07-24 + +Code v1.102.2 + +### Changed + +- Update to Code 1.102.2. + +## [4.102.1](https://github.com/coder/code-server/releases/tag/v4.102.1) - 2025-07-17 + +Code v1.102.1 + +### Changed + +- Update to Code 1.102.1. + +## [4.102.0](https://github.com/coder/code-server/releases/tag/v4.102.0) - 2025-07-16 + +Code v1.102.0 + +### Changed + +- Update to Code 1.102.0. + +### Added + +- Custom strings can be configured using the `--i18n` flag set to a JSON + file. This can be used for either translation (and can be used alongside + `--locale`) or for customizing the strings. See + [./src/node/i18n/locales/en.json](./src/node/i18n/locales/en.json) for the + available keys. + +## [4.101.2](https://github.com/coder/code-server/releases/tag/v4.101.2) - 2025-06-25 + +Code v1.101.2 + +### Changed + +- Update to Code 1.101.2. + +### Fixed + +- Fix web views not loading due to 401 when requesting the service worker. + +## [4.101.1](https://github.com/coder/code-server/releases/tag/v4.101.1) - 2025-06-20 + +Code v1.101.1 + +### Changed + +- Update to Code 1.101.1. + +## [4.101.0](https://github.com/coder/code-server/releases/tag/v4.101.0) - 2025-06-20 + +Code v1.101.0 + +### Changed + +- Update to Code 1.101.0. + +## [4.100.3](https://github.com/coder/code-server/releases/tag/v4.100.3) - 2025-06-03 + +Code v1.100.3 + +### Changed + +- Update to Code 1.100.3. + +## [4.100.2](https://github.com/coder/code-server/releases/tag/v4.100.2) - 2025-05-15 + +Code v1.100.2 + +### Changed + +- Update to Code 1.100.2. + +## [4.100.1](https://github.com/coder/code-server/releases/tag/v4.100.1) - 2025-05-13 + +Code v1.100.1 + +### Changed + +- Update to Code 1.100.1. + +## [4.100.0](https://github.com/coder/code-server/releases/tag/v4.100.0) - 2025-05-12 + +Code v1.100.0 + +### Added + +- Trusted domains for links can now be set at run-time by configuring + `linkProtectionTrustedDomains` in the `lib/vscode/product.json` file or via + the `--link-protection-trusted-domains` flag. + +### Changed + +- Update to Code 1.100.0. +- Disable extension signature verification, which previously was skipped by + default (the package used for verification is not available to OSS builds of + VS Code) but now reportedly throws hard errors making it impossible to install + extensions. + +### Fixed + +- Flags with repeatable options now work via the config file. + +## [4.99.4](https://github.com/coder/code-server/releases/tag/v4.99.4) - 2025-05-02 + +Code v1.99.3 + +### Security + +- Validate that ports in the path proxy are numbers, to prevent proxying to + arbitrary domains. + +## [4.99.3](https://github.com/coder/code-server/releases/tag/v4.99.3) - 2025-04-17 + +Code v1.99.3 + +### Added + +- Added `--skip-auth-preflight` flag to let preflight requests through the + proxy. + +### Changed + +- Update to Code 1.99.3. + +## [4.99.2](https://github.com/coder/code-server/releases/tag/v4.99.2) - 2025-04-10 + +Code v1.99.2 + +### Changed + +- Update to Code 1.99.2. + +## [4.99.1](https://github.com/coder/code-server/releases/tag/v4.99.1) - 2025-04-08 + +Code v1.99.1 + +### Changed + +- Update to Code 1.99.1. + +## [4.99.0](https://github.com/coder/code-server/releases/tag/v4.99.0) - 2025-04-07 + +Code v1.99.0 + +### Changed + +- Update to Code 1.99.0. + +## [4.98.0](https://github.com/coder/code-server/releases/tag/v4.98.0) - 2025-03-07 + +Code v1.98.0 + +### Changed + +- Update to Code 1.98.0. + +## [4.97.2](https://github.com/coder/code-server/releases/tag/v4.96.4) - 2025-02-18 + +Code v1.97.2 + +### Added + +- Added back macOS amd64 builds. + +### Changed + +- Update to Code 1.97.2. +- Softened dark mode login page colors. + +## [4.96.4](https://github.com/coder/code-server/releases/tag/v4.96.4) - 2025-01-20 + +Code v1.96.4 + +### Changed + +- Update to Code 1.96.4. + +## [4.96.2](https://github.com/coder/code-server/releases/tag/v4.96.2) - 2024-12-20 + +Code v1.96.2 + +### Changed + +- Update to Code 1.96.2. + +## [4.96.1](https://github.com/coder/code-server/releases/tag/v4.96.1) - 2024-12-18 + +Code v1.96.1 + +### Added + +- Dark color scheme for login and error pages. + +### Changed + +- Update to Code 1.96.1. + +## [4.95.3](https://github.com/coder/code-server/releases/tag/v4.95.3) - 2024-11-18 + +Code v1.95.3 + +### Changed + +- Update to Code 1.95.3. + +## [4.95.2](https://github.com/coder/code-server/releases/tag/v4.95.2) - 2024-11-12 + +Code v1.95.2 + +### Changed + +- Update to Code 1.95.2. + +## [4.95.1](https://github.com/coder/code-server/releases/tag/v4.95.1) - 2024-11-06 + +Code v1.95.1 + +### Changed + +- Update to Code 1.95.1. + +## [4.93.1](https://github.com/coder/code-server/releases/tag/v4.93.1) - 2024-09-23 + +Code v1.93.1 + +### Changed + +- Updated to Code 1.93.1. + +### Added + +- Added `--abs-proxy-base-path` flag for when code-server is not at the root. + +## [4.92.2](https://github.com/coder/code-server/releases/tag/v4.92.2) - 2024-08-19 + +Code v1.92.2 + +### Breaking changes + +- Dropped a patch that changed the compile target from es2022 to es2020 because + it no longer works with the way VS Code uses static properties. This may break + older browsers, so those browsers will either have to be updated or use an + older version of code-server. + +### Changed + +- Updated to Code 1.92.2. + +## [4.91.0](https://github.com/coder/code-server/releases/tag/v4.91.0) - 2024-07-10 + +Code v1.91.0 + +### Changed + +- Updated to Code 1.91.0. + +## [4.90.3](https://github.com/coder/code-server/releases/tag/v4.90.3) - 2024-06-21 + +Code v1.90.2 + +### Changed + +- Updated to Code 1.90.2. + +### Fixed + +- When the log gets rotated it will no longer incorrectly be moved to a new + directory created in the current working directory named with a date. + Instead, the file itself is prepended with the date and kept in the same + directory, as originally intended. + +## [4.90.2](https://github.com/coder/code-server/releases/tag/v4.90.2) - 2024-06-14 + +Code v1.90.1 + +### Changed + +- Updated to Code 1.90.1. + +## [4.90.1](https://github.com/coder/code-server/releases/tag/v4.90.1) - 2024-06-12 + +Code v1.90.0 + +### Fixed + +- Cache a call to get CPU information used in telemetry that could result in a + lack responsiveness if it was particularly slow. + +## [4.90.0](https://github.com/coder/code-server/releases/tag/v4.90.0) - 2024-06-11 + +Code v1.90.0 + +### Changed + +- Updated to Code 1.90.0. +- Updated Node to 20.11.1. + +### Added + +- Send contents to the clipboard in the integrated terminal by piping to + `code-server --stdin-to-clipboard` or `code-server -c`. + + You may want to make this an alias: + + ``` + alias xclip="code-server --stdin-to-clipboard" + echo -n "hello world" | xclip + ``` + +## [4.89.1](https://github.com/coder/code-server/releases/tag/v4.89.1) - 2024-04-14 + +Code v1.89.1 + +### Changed + +- Updated to Code 1.89.1. + +## [4.89.0](https://github.com/coder/code-server/releases/tag/v4.89.0) - 2024-04-08 + +Code v1.89.0 + +### Changed + +- Updated to Code 1.89.0. + +## [4.23.1](https://github.com/coder/code-server/releases/tag/v4.23.1) - 2024-04-15 + +Code v1.88.1 -### New Features +### Changed -- item +- Updated to Code 1.88.1. -### Bug Fixes +## [4.23.0](https://github.com/coder/code-server/releases/tag/v4.23.0) - 2024-04-08 -- fix(socket): did this thing #321 @githubuser +Code v1.88.0 -### Documentation +### Changed -- item +- Updated to Code 1.88.0. +- Updated Node to 18.18.2. -### Development +### Fixed -- item +- Fix masking the exit code when failing to install extensions on the command + line outside the integrated terminal. Installing extensions inside the + integrated terminal still masks the exit code and is an upstream bug. ---> +## [4.22.1](https://github.com/coder/code-server/releases/tag/v4.22.1) - 2024-03-14 + +Code v1.87.2 + +### Changed + +- Updated to Code 1.87.2. +- Enable keep-alive for proxy agent. + +## [4.22.0](https://github.com/coder/code-server/releases/tag/v4.22.0) - 2024-03-03 + +Code v1.87.0 + +### Changed + +- Updated to Code 1.87.0. + +## [4.21.2](https://github.com/coder/code-server/releases/tag/v4.21.2) - 2024-02-28 + +Code v1.86.2 + +### Changed + +- Updated to Code 1.86.2. + +## [4.21.1](https://github.com/coder/code-server/releases/tag/v4.21.1) - 2024-02-09 + +Code v1.86.1 + +### Changed + +- Updated to Code 1.86.1. +- Updated to Node 18.17.1. + +### Added + +- Docker images for Fedora and openSUSE. + +## [4.21.0](https://github.com/coder/code-server/releases/tag/v4.21.0) - 2024-02-05 + +Code v1.86.0 + +### Changed + +- Updated to Code 1.86.0. + +## [4.20.1](https://github.com/coder/code-server/releases/tag/v4.20.1) - 2024-01-22 + +Code v1.85.2 + +### Changed + +- Updated to Code 1.85.2. + +### Fixed + +- Query variables are no longer double-encoded when going over the path proxy. + +## [4.20.0](https://github.com/coder/code-server/releases/tag/v4.20.0) - 2023-12-21 + +Code v1.85.1 + +### Added + +- New flag `--disable-file-uploads` to disable uploading files to the remote by + drag and drop and to disable opening local files via the "show local" button + in the file open prompt. Note that you can still open local files by drag and + dropping the file onto the editor pane. +- Added `wget` to the release image. + +### Changed + +- Updated to Code 1.85.1. +- The `--disable-file-downloads` flag will now disable the "show local" button + in the file save prompt as well. +- Debian release image updated to use Bookworm. + +## [4.19.1](https://github.com/coder/code-server/releases/tag/v4.19.1) - 2023-11-29 + +Code v1.84.2 + +### Fixed + +- Fixed an issue where parts of the editor would not load (like the file + explorer, source control, etc) when using a workspace file. + +## [4.19.0](https://github.com/coder/code-server/releases/tag/v4.19.0) - 2023-11-18 + +Code v1.84.2 + +### Changed + +- Updated to Code 1.84.2. + +## [4.18.0](https://github.com/coder/code-server/releases/tag/v4.18.0) - 2023-10-20 + +Code v1.83.1 + +### Changed + +- Updated to Code 1.83.1. + +## [4.17.1](https://github.com/coder/code-server/releases/tag/v4.17.1) - 2023-09-29 + +Code v1.82.2 + +### Fixed + +- Make secret storage persistent. For example, logging in with GitHub should + persist between browser refreshes and code-server restarts. +- Issues with argon2 on arm builds should be fixed now. + +## [4.17.0](https://github.com/coder/code-server/releases/tag/v4.17.0) - 2023-09-22 + +Code v1.82.2 + +### Added + +- Japanese locale. +- `CODE_SERVER_HOST` environment variable. + +### Changed + +- Update to Code 1.82.2. This includes an update to Node 18, which also means + that the minimum glibc is now 2.28. If you need to maintain a lower glibc then + you can take a version of Node 18 that is compiled with a lower glibc and use + that to build code-server (or at a minimum rebuild the native modules). +- Display paths to config files in full rather than abbreviated. If you have + trouble with the password not working please update and make sure the + displayed config paths are what you expect. + +### Fixed + +- Fix some dependency issues for the standalone arm64 and armv7l releases. If + you had issues with missing or failing modules please try these new builds. + +## [4.16.1](https://github.com/coder/code-server/releases/tag/v4.16.1) - 2023-07-31 + +Code v1.80.2 + +### Changed + +- Updated to Code 1.80.2. + +## [4.16.0](https://github.com/coder/code-server/releases/tag/v4.16.0) - 2023-07-28 + +Code v1.80.1 + +### Added + +- `--disable-proxy` flag. This disables the domain and path proxies but it does + not disable the ports panel in Code. That can be disabled by using + `remote.autoForwardPorts=false` in your settings. + +## [4.15.0](https://github.com/coder/code-server/releases/tag/v4.15.0) - 2023-07-21 + +Code v1.80.1 + +### Changed + +- Updated to Code 1.80.1. + +### Added + +- `--trusted-origin` flag for specifying origins that you trust but do not + control (for example a reverse proxy). + +Code v1.79.2 + +## [4.14.1](https://github.com/coder/code-server/releases/tag/v4.14.1) - 2023-06-26 + +Code v1.79.2 + +### Security + +- Remove extra write permissions on the Node binary bundled with the linux-amd64 + tarball. If you extract the tar without a umask this could mean the Node + binary would be unexpectedly writable. + +### Fixed + +- Inability to launch multiple instances of code-server for different users. + +### Added + +- `--session-socket` CLI flag to configure the location of the session socket. + By default it will be placed in `/code-server-ipc.sock`. + +## [4.14.0](https://github.com/coder/code-server/releases/tag/v4.14.0) - 2023-06-16 + +Code v1.79.2 + +### Added + +- `--domain-proxy` now supports `{{port}}` and `{{host}}` template variables. + +### Changed + +- Updated to Code 1.79.2 +- Files opened from an external terminal will now open in the most closely + related window rather than in the last opened window. + +## [4.13.0](https://github.com/coder/code-server/releases/tag/v4.13.0) - 2023-05-19 + +Code v1.78.2 + +### Changed + +- Updated to Code 1.78.2. + +### Fixed + +- Proxying files that contain non-ASCII characters. +- Origin check when X-Forwarded-Host contains comma-separated hosts. + +## [4.12.0](https://github.com/coder/code-server/releases/tag/v4.12.0) - 2023-04-21 + +Code v1.77.3 + +### Changed + +- Updated to Code 1.77.3 +- Ports panel will use domain-based proxy (instead of the default path-based + proxy) when set via --proxy-domain. +- Apply --app-name to the PWA title. + +### Added + +- Thai translation for login page. +- Debug logs around the origin security check. If you are getting forbidden + errors on web sockets please run code-server with `--log debug` to see why the + requests are being blocked. + +## [4.11.0](https://github.com/coder/code-server/releases/tag/v4.11.0) - 2023-03-16 + +Code v1.76.1 + +### Changed + +- Updated to Code 1.76.1 + +## [4.10.1](https://github.com/coder/code-server/releases/tag/v4.10.1) - 2023-03-04 + +Code v1.75.1 + +### Security + +Added an origin check to web sockets to prevent cross-site hijacking attacks on +users using older or niche browser that do not support SameSite cookies and +attacks across sub-domains that share the same root domain. + +The check requires the host header to be set so if you use a reverse proxy +ensure it forwards that information otherwise web sockets will be blocked. + +## [4.10.0](https://github.com/coder/code-server/releases/tag/v4.10.0) - 2023-02-15 + +Code v1.75.1 + +### Changed + +- Updated to Code 1.75.1 + +### Removed + +- Removed `--link` (was deprecated over thirteen months ago in 4.0.1). + +## [4.9.1](https://github.com/coder/code-server/releases/tag/v4.9.1) - 2022-12-15 + +Code v1.73.1 + +### Changed + +- Updated a couple steps in the build and release process to ensure we're using + `npm` and `yarn` consistently depending on the step. + +### Fixed + +- Fixed an issue with code-server version not displaying in the Help > About window. +- Fixed terminal not loading on macOS clients. + +## [4.9.0](https://github.com/coder/code-server/releases/tag/v4.9.0) - 2022-12-06 + +Code v1.73.1 + +### Changed + +- Upgraded to Code 1.73.1 + +### Added + +- `/security.txt` added as a route with info on our security policy information thanks to @ghuntley + +### Fixed + +- Installing on majaro images should now work thanks to @MrPeacockNLB for + adding the `--noconfirm` flag in `install.sh` + +### Known Issues + +- `--cert` on Ubuntu 22.04: OpenSSL v3 is used which breaks `pem` meaning the + `--cert` feature will not work. [Reference](https://github.com/adobe/fetch/pull/318#issuecomment-1306070259) + +## [4.8.3](https://github.com/coder/code-server/releases/tag/v4.8.3) - 2022-11-07 + +Code v1.72.1 + +### Added + +- install script now supports arch-like (i.e. manjaro, endeavourous, etc.) + architectures + +### Changed + +- Updated text in the Getting Started page. + +## [4.8.2](https://github.com/coder/code-server/releases/tag/v4.8.2) - 2022-11-02 + +Code v1.72.1 + +### Added + +- New text in the Getting Started page with info about + `coder/coder`. This is enabled by default but can be disabled by passing the CLI + flag `--disable-getting-started-override` or setting + `CS_DISABLE_GETTING_STARTED_OVERRIDE=1` or + `CS_DISABLE_GETTING_STARTED_OVERRIDE=true`. + +## [4.8.1](https://github.com/coder/code-server/releases/tag/v4.8.1) - 2022-10-28 + +Code v1.72.1 + +### Fixed + +- Fixed CSP error introduced in 4.8.0 that caused issues with webviews and most + extensions. + +## [4.8.0](https://github.com/coder/code-server/releases/tag/v4.8.0) - 2022-10-24 + +Code v1.72.1 + +### Added + +- Support for the Ports panel which leverages code-server's built-in proxy. It + also uses `VSCODE_PROXY_URI` where `{{port}}` is replace when forwarding a port. + Example: `VSCODE_PROXY_URI=https://{{port}}.kyle.dev` would forward an + application running on localhost:3000 to https://3000.kyle.dev +- Support for `--disable-workspace-trust` CLI flag +- Support for `--goto` flag to open file @ line:column +- Added Ubuntu-based images for Docker releases. If you run into issues with + `PATH` being overwritten in Docker please try the Ubuntu image as this is a + problem in the Debian base image. + +### Changed + +- Updated Code to 1.72.1 + +### Fixed + +- Enabled `BROWSER` environment variable +- Patched `asExternalUri` to work so now extensions run inside code-server can use it + +## [4.7.1](https://github.com/coder/code-server/releases/tag/v4.7.1) - 2022-09-30 + +Code v1.71.2 + +### Changed + +- Updated Code to 1.71.2 + +### Fixed + +- Fixed install script not upgrading code-server when already installed on RPM-based machines +- Fixed install script failing to gain root permissions on FreeBSD + +## [4.7.0](https://github.com/coder/code-server/releases/tag/v4.7.0) - 2022-09-09 + +Code v1.71.0 + +### Changed + +- Updated Code to 1.71.0 + +### Removed + +- Dropped heartbeat patch because it was implemented upstream + +### Fixed + +- Add flags --unsafe-perm --legacy-peer-deps in `npm-postinstall.sh` which ensures installing with npm works correctly + +## [4.6.1](https://github.com/coder/code-server/releases/tag/v4.6.1) - 2022-09-31 + +Code v1.70.2 + +### Changed + +- Updated Code to 1.70.2 +- Updated `argon2` to 0.29.0 which should fix issues on FreeBSD +- Updated docs to suggest using `npm` instead of `yarn` + +### Removed + +- Dropped database migration patch affected to 4.0.2 versions and earlier. + +### Fixed + +- Fixed preservation of `process.execArgv` which means you can pass `--prof` to profile code-server + +## [4.6.0](https://github.com/coder/code-server/releases/tag/v4.6.0) - 2022-08-17 + +Code v1.70.1 + +### Changed + +- Updated Code to 1.70.1. + +### Added + +- Added a heartbeat to sockets. This should prevent them from getting closed by + reverse proxy timeouts when idle like NGINX's default 60-second timeout. + +### Fixed + +- Fixed logout option appearing even when authentication is disabled. + +## [4.5.2](https://github.com/coder/code-server/releases/tag/v4.5.2) - 2022-08-15 + +Code v1.68.1 + +### Security + +- Fixed the proxy route not performing authentication. For example if you were + to run a development HTTP server using `python -m http.server 8000` then it + would be accessible at `my.domain/proxy/8000/` without any authentication. + + If all of the following apply to you please update as soon as possible: + - You run code-server with the built-in password authentication. + - You run unprotected HTTP services on ports accessible by code-server. + +### Changed + +- Invoking `code-server` in the integrated terminal will now use the script that + comes with upstream Code. This means flags like `--wait` will be + automatically supported now. However the upstream script only has the ability + to interact with the running code-server and cannot spawn new instances. If + you need to spawn a new code-server from the integrated terminal please + specify the full path to code-server's usual script (for example + `/usr/bin/code-server`). + +### Fixed + +- Invoking `code-server` in the integrated terminal will now work instead of + erroring about not finding Node. + +## [4.5.1](https://github.com/coder/code-server/releases/tag/v4.5.1) - 2022-07-18 + +Code v1.68.1 + +### Changed + +- We now use `release/v<0.0.0>` for the release branch name so it doesn't + conflict with the tag name +- Added `.prettierignore` to ignore formatting files in `lib/vscode` + +### Added + +- Allow more comprehensive affinity config in Helm chart +- Added custom message in Homebrew PR to make sure code-server maintainers are + tagged +- Allow setting `priorityClassName` via Helm chart +- Added troubleshooting docs to `CONTRIBUTING.md` + +### Fixed + +- Removed default memory limit which was set via `NODE_OPTIONS` +- Changed output in pipe to make it easier to debug code-server when doing live + edits +- Fixed display-language patch to use correct path which broke in 4.5.0 +- Fixed multiple code-server windows opening when using the code-server CLI in + the Integrated Terminal +- Fixed Integrated Terminal not working when web base was not the root path + +### Security + +- Updated `glob-parent` version in dependencies + +## [4.5.0](https://github.com/coder/code-server/releases/tag/v4.5.0) - 2022-06-29 + +Code v1.68.1 + +### Changed + +- Updated codecov to use codecov uploader +- Moved integration tests to Jest +- Fixed docker release to only download .deb +- Upgraded to Code 1.68.1 +- Install `nfpm` from GitHub +- Upgraded to TypeScript 4.6 + +### Added + +- Added tests for `open`, `isWsl`, `handlePasswordValidation` +- Provided alternate image registry to dockerhub +- Allowed users to have scripts run on container with `ENTRYPOINTD` environment + variable + +### Fixed + +- Fixed open CLI command to work on macOS + +## [4.4.0](https://github.com/coder/code-server/releases/tag/v4.4.0) - 2022-05-06 + +Code v1.66.2 + +### Changed + +- Refactored methods in `Heart` class and made `Heart.beat()` async to make + testing easier. +- Upgraded to Code 1.66.2. + +### Added + +- Added back telemetry patch which was removed in the Code reachitecture. +- Added support to use `true` for `CS_DISABLE_FILE_DOWNLOADS` environment + variable. This means you can disable file downloads by setting + `CS_DISABLE_FILE_DOWNLOADS` to `true` or `1`. +- Added tests for `Heart` class. + +### Fixed + +- Fixed installation issue in AUR after LICENSE rename. +- Fixed issue with listening on IPv6 addresses. +- Fixed issue with Docker publish action not being able to find artifacts. Now + it downloads the release assets from the release. + +## [4.3.0](https://github.com/coder/code-server/releases/tag/v4.3.0) - 2022-04-14 + +Code v1.65.2 + +### Changed + +- Excluded .deb files from release Docker image which drops the compressed and + uncompressed size by 58% and 34%. +- Upgraded to Code 1.65.2. + +### Added + +- Added a new CLI flag called `--disable-file-downloads` which allows you to + disable the "Download..." option that shows in the UI when right-clicking on a + file. This can also set by running `CS_DISABLE_FILE_DOWNLOADS=1`. +- Aligned the dependencies for binary and npm release artifacts. + +### Fixed + +- Fixed the code-server version from not displaying in the Help > About dialog. +- Fixed issues with the TypeScript and JavaScript Language Features Extension + failing to activate. +- Fixed missing files in ipynb extension. +- Fixed the homebrew release workflow. +- Fixed the Docker release workflow from not always publishing version tags. + +## [4.2.0](https://github.com/coder/code-server/releases/tag/v4.2.0) - 2022-03-22 + +Code v1.64.2 + +### Added + +- Added tests for `handleArgsSocketCatchError`, `setDefaults` and + `optionDescriptions`. + +### Changed + +- We switched from using the fork `coder/vscode` to a submodule of + `microsoft/vscode` + patches managed by `quilt` for how Code sits inside the + code-server codebase. +- Upgraded to Code 1.64.2. + +### Fixed + +- Update popup notification through `--disable-update-check` is now fixed. +- Fixed PWA icons not loading on iPad +- Fixed the homebrew release process. Our `cdrci` bot should now automatically + update the version as part of the release pipeline. +- Fixed titleBar color setting being ignored in PWA. + +### Security + +- Updated to `minimist-list`. +- Updated `cloud-agent` to `v0.2.4` which uses `nhooyr.io/webscoket` `v1.8.7`. + +## [4.1.0](https://github.com/coder/code-server/releases/tag/v4.1.0) - 2022-03-03 + +Code v1.63.0 + +### Added + +- Support for injecting GitHub token into Code so extensions can make use of it. + This can be done with the `GITHUB_TOKEN` environment variable or `github-auth` + in the config file. +- New flag `--socket-mode` allows setting the mode (file permissions) of the + socket created when using `--socket`. +- The version of Code bundled with code-server now appears when using the + `--version` flag. For example: `4.0.2 5cdfe74686aa73e023f8354a9a6014eb30caa7dd with Code 1.63.0`. + If you have been parsing this flag for the version you might want to use + `--version --json` instead as doing that will be more stable. + +### Changed + +- The workspace or folder passed on the CLI will now use the same redirect + method that the last opened workspace or folder uses. This means if you use + something like `code-server /path/to/dir` you will now get a query parameter + added (like so: `my-domain.tld?folder=/path/to/dir`), making it easier to edit + by hand and making it consistent with the last opened and menu open behaviors. +- The folder/workspace query parameter no longer has encoded slashes, making + them more readable and editable by hand. This was only affecting the last + opened behavior, not opens from the menu. + +### Fixed + +- Fix web sockets not connecting when using `--cert`. +- Prevent workspace state collisions when opening a workspace that shares the + same file path with another workspace on a different machine that shares the + same domain. This was causing files opened in one workspace to be "re-"opened + in the other workspace when the other workspace is opened. +- Pin the Express version which should make installing from npm work again. +- Propagate signals to code-server in the Docker image which means it should + stop more quickly and gracefully. +- Fix missing argon binaries in the standalone releases on arm machines. + +## [4.0.2](https://github.com/coder/code-server/releases/tag/v4.0.2) - 2022-01-27 + +Code v1.63.0 + +### Fixed + +- Unset the `BROWSER` environment variable. This fixes applications that hard + exit when trying to spawn the helper script `BROWSER` points to because the + file is missing. While we do include the script now we are leaving the + variable omitted because the script does not work yet. + +## [4.0.1](https://github.com/coder/code-server/releases/tag/v4.0.1) - 2022-01-04 + +Code v1.63.0 + +code-server has been rebased on upstream's newly open-sourced server +implementation (#4414). + +### Changed + +- Web socket compression has been made the default (when supported). This means + the `--enable` flag will no longer take `permessage-deflate` as an option. +- The static endpoint can no longer reach outside code-server. However the + vscode-remote-resource endpoint still can. +- OpenVSX has been made the default marketplace. +- The last opened folder/workspace is no longer stored separately in the + settings file (we rely on the already-existing query object instead). +- The marketplace override environment variables `SERVICE_URL` and `ITEM_URL` + have been replaced with a single `EXTENSIONS_GALLERY` variable that + corresponds to `extensionsGallery` in Code's `product.json`. + +### Added + +- `VSCODE_PROXY_URI` env var for use in the terminal and extensions. + +### Removed + +- Extra extension directories have been removed. The `--extra-extensions-dir` + and `--extra-builtin-extensions-dir` flags will no longer be accepted. +- The `--install-source` flag has been removed. -## Next Version +### Deprecated -VS Code v0.00.0 +- `--link` is now deprecated (#4562). -### New Features +### Security -- item +- We fixed a XSS vulnerability by escaping HTML from messages in the error page (#4430). -### Bug Fixes +## [3.12.0](https://github.com/coder/code-server/releases/tag/v3.12.0) - 2021-09-15 -- Fix logout when using a base path (#3608) +Code v1.60.0 -### Documentation +### Changed -- docs: add Pomerium #3424 @desimone -- docs: fix confusing sentence in pull requests section #3460 @shiv-tyagi -- docs: remove toc from changelog @oxy @jsjoeio -- docs(MAINTAINING): add information about CHANGELOG #3467 @jsjoeio -- docs: move release process to MAINTAINING.md #3441 @oxy @Prashant168 -- docs: format 'Caddy' from guide.md @PisecesPeng +- Upgrade Code to 1.60.0. -### Development +### Fixed -- chore: cross-compile docker images with buildx #3166 @oxy -- chore: update node to v14 #3458 @oxy -- chore: update .gitignore #3557 @cuining -- fix: use sufficient computational effort for password hash #3422 @jsjoeio -- docs(CONTRIBUTING): add section on testing #3629 @jsjoeio +- Fix logout when using a base path (#3608). -### Development +## [3.11.1](https://github.com/coder/code-server/releases/tag/v3.11.1) - 2021-08-06 -- fix(publish): update cdrci fork in brew-bump.sh #3468 @jsjoeio -- chore(dev): migrate away from parcel #3578 @jsjoeio +Undocumented (see releases page). -## 3.10.2 +## [3.11.0](https://github.com/coder/code-server/releases/tag/v3.11.0) - 2021-06-14 -VS Code v1.56.1 +Undocumented (see releases page). -### New Features +## [3.10.2](https://github.com/coder/code-server/releases/tag/v3.10.2) - 2021-05-21 -- feat: support `extraInitContainers` in helm chart values #3393 @strowk -- feat: change `extraContainers` to support templating in helm chart #3393 @strowk +Code v1.56.1 -### Bug Fixes +### Added -- fix: use correct command to Open Folder on Welcome page #3437 @jsjoeio +- Support `extraInitContainers` in helm chart values (#3393). -### Development +### Changed -- fix(ci): update brew-bump.sh to update remote first #3438 @jsjoeio +- Change `extraContainers` to support templating in helm chart (#3393). -## 3.10.1 +### Fixed -VS Code v1.56.1 +- Fix "Open Folder" on welcome page (#3437). -### Bug Fixes +## [3.10.1](https://github.com/coder/code-server/releases/tag/v3.10.1) - 2021-05-17 -- fix: Check the logged user instead of $USER #3330 @videlanicolas -- fix: Fix broken node_modules.asar symlink in npm package #3355 @code-asher -- fix: Update cloud agent to fix version issue #3342 @oxy +Code v1.56.1 -### Documentation +### Fixed -- docs(install): add raspberry pi section #3376 @jsjoeio -- docs(maintaining): add pull requests section #3378 @jsjoeio -- docs(maintaining): add merge strategies section #3379 @jsjoeio -- refactor: move default PR template #3375 @jsjoeio -- docs(contributing): add commits section #3377 @jsjoeio +- Check the logged user instead of $USER (#3330). +- Fix broken node_modules.asar symlink in npm package (#3355). +- Update cloud agent to fix version issue (#3342). -### Development +### Changed -- chore: ignore updates to microsoft/playwright-github-action -- fix(socket): use xdgBasedir.runtime instead of tmp #3304 @jsjoeio -- fix(ci): re-enable trivy-scan-repo #3368 @jsjoeio +- Use xdgBasedir.runtime instead of tmp (#3304). -## 3.10.0 +## [3.10.0](https://github.com/coder/code-server/releases/tag/v3.10.0) - 2021-05-10 -VS Code v1.56.0 +Code v1.56.0 -### New Features +### Changed -- feat: minor connections refactor #3178 @code-asher -- feat(security): add code-scanning with CodeQL #3229 @jsjoeio -- feat(ci): add trivy job for security #3261 @jsjoeio -- feat(vscode): update to version 1.56.0 #3269 @oxy -- feat: use ptyHostService #3308 @code-asher +- Update to Code 1.56.0 (#3269). +- Minor connections refactor (#3178). Improves connection stability. +- Use ptyHostService (#3308). This brings us closer to upstream Code. -### Bug Fixes +### Added -- fix(socket): did this thing #321 @githubuser -- fix(login): rate limiter shouldn't count successful logins #3141 @jsjoeio -- chore(lib/vscode): update netmask #3187 @oxy -- chore(deps): update dependencies with CVEs #3223 @oxy -- fix: refactor logout #3277 @code-asher -- fix: add flag for toggling permessage-deflate #3286 @code-asher -- fix: make sure directories exist #3309 @code-asher +- Add flag for toggling permessage-deflate (#3286). The default is off so + compression will no longer be used by default. Use the --enable flag to + toggle it back on. -### Documentation +### Fixed -- docs(FAQ): add mention of sysbox #3087 @bpmct -- docs: add security policy #3148 @jsjoeio -- docs(guide.md): add `caddy` example for serving from sub-path #3217 @catthehacker -- docs: revamp debugging section #3224 @code-asher -- docs(readme): refactor to use codecov shield #3227 @jsjoeio -- docs(maintaining): use milestones over boards #3228 @jsjoeio -- docs(faq): add entry for accessing OSX folders #3247 @bpmct -- docs(termux): add workaround for Android backspace issue #3251 @jsjoeio -- docs(maintaining): add triage to workflow #3284 @jsjoeio -- docs(security): add section for tools #3287 @jsjoeio -- docs(maintaining): add versioning #3288 @jsjoeio -- docs: add changelog #3337 @jsjoeio +- Make rate limiter not count against successful logins (#3141). +- Refactor logout (#3277). This fixes logging out in some scenarios. +- Make sure directories exist (#3309). This fixes some errors on startup. -### Development +### Security -- fix(update-vscode): add check/docs for git-subtree #3129 @oxy -- refactor(testing): migrate to playwright-test from jest-playwright #3133 @jsjoeio -- refactor(ci): remove unmaintained CI images and update release workflow #3147 @oxy -- chore(ci): migrate from hub to gh #3168 @oxy -- feat(testing): add e2e tests for code-server and terminal #3169 @jsjoeio -- chore(ranger): fix syntax for extension-request #3172 @oxy -- feat(testing): add codecov to generate test coverage reports #3194 @jsjoeio -- feat(testing): add tests for registerServiceWorker #3200 @jsjoeio -- refactor(testing): fix flaky terminal test #3230 @jsjoeio -- chore: ignore 15.x @types/node updates #3244 @jsjoeio -- chore(build): compile vscode+extensions in parallel #3250 @oxy -- fix(deps): remove eslint-plugin-jest-playwright #3260 @jsjoeio -- fix(testing): reduce flakiness of terminal.test.ts and use 1 worker for e2e tests #3263 @jsjoeio -- feat(testing): add isConnected check #3271 @jsjoeio -- feat(testing): add test for src/node/constants.ts #3290 @jsjoeio -- feat: test static route #3297 @code-asher -- refactor(ci): split audit from prebuild #3298 @oxy -- chore(lib/vscode): cleanup/update build deps #3314 @oxy -- fix(build): download correct cloud-agent for arch #3331 @oxy -- fix: xmldom and underscore #3332 @oxy +- Update dependencies with CVEs (#3223). ## Previous versions -This was added with `3.10.0`, which means any previous versions are not documented in the changelog. +This was added with `3.10.0`, which means any previous versions are not +documented in the changelog. -To see those, please visit the [Releases page](https://github.com/cdr/code-server/releases). +To see those, please visit the [Releases page](https://github.com/coder/code-server/releases). diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/ci/Caddyfile b/ci/Caddyfile new file mode 100644 index 000000000000..52e4da6da281 --- /dev/null +++ b/ci/Caddyfile @@ -0,0 +1,15 @@ +{ + admin localhost:4444 +} +:8000 { + @portLocalhost path_regexp port ^/([0-9]+)\/ide + handle @portLocalhost { + uri strip_prefix {re.port.1}/ide + reverse_proxy localhost:{re.port.1} + } + + handle { + respond "Bad hostname" 400 + } + +} diff --git a/ci/README.md b/ci/README.md deleted file mode 100644 index f4c418e5dda5..000000000000 --- a/ci/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# ci - -This directory contains scripts used for code-server's continuous integration infrastructure. - -Some of these scripts contain more detailed documentation and options -in header comments. - -Any file or directory in this subdirectory should be documented here. - -- [./ci/lib.sh](./lib.sh) - - Contains code duplicated across these scripts. - -## dev - -This directory contains scripts used for the development of code-server. - -- [./ci/dev/image](./dev/image) - - See [./docs/CONTRIBUTING.md](../docs/CONTRIBUTING.md) for docs on the development container. -- [./ci/dev/fmt.sh](./dev/fmt.sh) (`yarn fmt`) - - Runs formatters. -- [./ci/dev/lint.sh](./dev/lint.sh) (`yarn lint`) - - Runs linters. -- [./ci/dev/test-unit.sh](./dev/test-unit.sh) (`yarn test:unit`) - - Runs unit tests. -- [./ci/dev/test-e2e.sh](./dev/test-e2e.sh) (`yarn test:e2e`) - - Runs end-to-end tests. -- [./ci/dev/ci.sh](./dev/ci.sh) (`yarn ci`) - - Runs `yarn fmt`, `yarn lint` and `yarn test`. -- [./ci/dev/watch.ts](./dev/watch.ts) (`yarn watch`) - - Starts a process to build and launch code-server and restart on any code changes. - - Example usage in [./docs/CONTRIBUTING.md](../docs/CONTRIBUTING.md). -- [./ci/dev/gen_icons.sh](./ci/dev/gen_icons.sh) (`yarn icons`) - - Generates the various icons from a single `.svg` favicon in - `src/browser/media/favicon.svg`. - - Requires [imagemagick](https://imagemagick.org/index.php) - -## build - -This directory contains the scripts used to build and release code-server. -You can disable minification by setting `MINIFY=`. - -- [./ci/build/build-code-server.sh](./build/build-code-server.sh) (`yarn build`) - - Builds code-server into `./out` and bundles the frontend into `./dist`. -- [./ci/build/build-vscode.sh](./build/build-vscode.sh) (`yarn build:vscode`) - - Builds vscode into `./lib/vscode/out-vscode`. -- [./ci/build/build-release.sh](./build/build-release.sh) (`yarn release`) - - Bundles the output of the above two scripts into a single node module at `./release`. -- [./ci/build/build-standalone-release.sh](./build/build-standalone-release.sh) (`yarn release:standalone`) - - Requires a node module already built into `./release` with the above script. - - Will build a standalone release with node and node_modules bundled into `./release-standalone`. -- [./ci/build/clean.sh](./build/clean.sh) (`yarn clean`) - - Removes all build artifacts. - - Useful to do a clean build. -- [./ci/build/code-server.sh](./build/code-server.sh) - - Copied into standalone releases to run code-server with the bundled node binary. -- [./ci/build/test-standalone-release.sh](./build/test-standalone-release.sh) (`yarn test:standalone-release`) - - Ensures code-server in the `./release-standalone` directory works by installing an extension. -- [./ci/build/build-packages.sh](./build/build-packages.sh) (`yarn package`) - - Packages `./release-standalone` into a `.tar.gz` archive in `./release-packages`. - - If on linux, [nfpm](https://github.com/goreleaser/nfpm) is used to generate `.deb` and `.rpm`. -- [./ci/build/nfpm.yaml](./build/nfpm.yaml) - - Used to configure [nfpm](https://github.com/goreleaser/nfpm) to generate `.deb` and `.rpm`. -- [./ci/build/code-server-nfpm.sh](./build/code-server-nfpm.sh) - - Entrypoint script for code-server for `.deb` and `.rpm`. -- [./ci/build/code-server.service](./build/code-server.service) - - systemd user service packaged into the `.deb` and `.rpm`. -- [./ci/build/release-github-draft.sh](./build/release-github-draft.sh) (`yarn release:github-draft`) - - Uses [gh](https://github.com/cli/cli) to create a draft release with a template description. -- [./ci/build/release-github-assets.sh](./build/release-github-assets.sh) (`yarn release:github-assets`) - - Downloads the release-package artifacts for the current commit from CI. - - Uses [gh](https://github.com/cli/cli) to upload the artifacts to the release - specified in `package.json`. -- [./ci/build/npm-postinstall.sh](./build/npm-postinstall.sh) - - Post install script for the npm package. - - Bundled by`yarn release`. - -## release-image - -This directory contains the release docker container image. - -- [./ci/steps/build-docker-buildx-push.sh](./ci/steps/docker-buildx-push.sh) - - Builds the release containers with tags `codercom/code-server-$ARCH:$VERSION` for amd64 and arm64 with `docker buildx` and pushes them. - - Assumes debian releases are ready in `./release-packages`. - -## images - -This directory contains the images for CI. - -## steps - -This directory contains the scripts used in CI. -Helps avoid clobbering the CI configuration. - -- [./steps/fmt.sh](./steps/fmt.sh) - - Runs `yarn fmt`. -- [./steps/lint.sh](./steps/lint.sh) - - Runs `yarn lint`. -- [./steps/test-unit.sh](./steps/test-unit.sh) - - Runs `yarn test:unit`. -- [./steps/test-e2e.sh](./steps/test-e2e.sh) - - Runs `yarn test:e2e`. -- [./steps/release.sh](./steps/release.sh) - - Runs the release process. - - Generates the npm package at `./release`. -- [./steps/release-packages.sh](./steps/release-packages.sh) - - Takes the output of the previous script and generates a standalone release and - release packages into `./release-packages`. -- [./steps/publish-npm.sh](./steps/publish-npm.sh) - - Grabs the `npm-package` release artifact for the current commit and publishes it on npm. -- [./steps/docker-buildx-push.sh](./steps/docker-buildx-push.sh) - - Builds the docker image and then pushes it. -- [./steps/push-docker-manifest.sh](./steps/push-docker-manifest.sh) - - Loads all images in `./release-images` and then builds and pushes a multi architecture - docker manifest for the amd64 and arm64 images to `codercom/code-server:$VERSION` and - `codercom/code-server:latest`. diff --git a/ci/build/build-code-server.sh b/ci/build/build-code-server.sh index c17725393435..df86f2d97a18 100755 --- a/ci/build/build-code-server.sh +++ b/ci/build/build-code-server.sh @@ -14,24 +14,6 @@ main() { sed -i.bak "1s;^;#!/usr/bin/env node\n;" out/node/entry.js && rm out/node/entry.js.bak chmod +x out/node/entry.js fi - - if ! [ -f ./lib/coder-cloud-agent ]; then - echo "Downloading the cloud agent..." - - # for arch; we do not use OS from lib.sh and get our own. - # lib.sh normalizes macos to darwin - but cloud-agent's binaries do not - source ./ci/lib.sh - OS="$(uname | tr '[:upper:]' '[:lower:]')" - - set +e - curl -fsSL "https://github.com/cdr/cloud-agent/releases/latest/download/cloud-agent-$OS-$ARCH" -o ./lib/coder-cloud-agent - chmod +x ./lib/coder-cloud-agent - set -e - fi - - yarn browserify out/browser/register.js -o out/browser/register.browserified.js - yarn browserify out/browser/pages/login.js -o out/browser/pages/login.browserified.js - yarn browserify out/browser/pages/vscode.js -o out/browser/pages/vscode.browserified.js } main "$@" diff --git a/ci/build/build-packages.sh b/ci/build/build-packages.sh index 8da6aec38332..35b27a558edc 100755 --- a/ci/build/build-packages.sh +++ b/ci/build/build-packages.sh @@ -1,19 +1,18 @@ #!/usr/bin/env bash set -euo pipefail -# Packages code-server for the current OS and architecture into ./release-packages. -# This script assumes that a standalone release is built already into ./release-standalone +# Given a release found in $RELEASE_PATH, generate a deb, rpm, and tarball each +# named after $ARCH (derived from uname -m but can be overridden for +# cross-compilation) and $OS (derived from uname and cannot be overridden) and +# place them in ./release-packages and ./release-gcp. main() { cd "$(dirname "${0}")/../.." source ./ci/lib.sh source ./ci/build/build-lib.sh - # Allow us to override architecture - # we use this for our Linux ARM64 cross compile builds - if [ "$#" -eq 1 ] && [ "$1" ]; then - ARCH=$1 - fi + VERSION=$(jq -r .version "$RELEASE_PATH/package.json") + export VERSION # for nfpm to use mkdir -p release-packages @@ -27,9 +26,9 @@ main() { release_archive() { local release_name="code-server-$VERSION-$OS-$ARCH" if [[ $OS == "linux" ]]; then - tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone + tar -czf "release-packages/$release_name.tar.gz" --owner=0 --group=0 --transform "s/^$RELEASE_PATH/$release_name/" "$RELEASE_PATH" else - tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone + tar -czf "release-packages/$release_name.tar.gz" -s "/^$RELEASE_PATH/$release_name/" "$RELEASE_PATH" fi echo "done (release-packages/$release_name)" @@ -50,15 +49,13 @@ release_nfpm() { export NFPM_ARCH - PKG_FORMAT="deb" - NFPM_ARCH="$(get_nfpm_arch $PKG_FORMAT "$ARCH")" + NFPM_ARCH="$(get_nfpm_arch deb "$ARCH")" nfpm_config="$(envsubst < ./ci/build/nfpm.yaml)" echo "Building deb" echo "$nfpm_config" | head --lines=4 nfpm pkg -f <(echo "$nfpm_config") --target "release-packages/code-server_${VERSION}_${NFPM_ARCH}.deb" - PKG_FORMAT="rpm" - NFPM_ARCH="$(get_nfpm_arch $PKG_FORMAT "$ARCH")" + NFPM_ARCH="$(get_nfpm_arch rpm "$ARCH")" nfpm_config="$(envsubst < ./ci/build/nfpm.yaml)" echo "Building rpm" echo "$nfpm_config" | head --lines=4 diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index ba3193513e8e..6dbc2784acbc 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -1,13 +1,19 @@ #!/usr/bin/env bash set -euo pipefail -# This script requires vscode to be built with matching MINIFY. +# Once both code-server and VS Code have been built, use this script to copy +# them into a single directory (./release), prepare the package.json and +# product.json, and add shrinkwraps. This results in a generic NPM package that +# we can publish to NPM. -# MINIFY controls whether minified vscode is bundled. +# MINIFY controls whether minified VS Code is bundled. It must match the value +# used when VS Code was built. MINIFY="${MINIFY-true}" -# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install -# to run first. +# node_modules are not copied by default. Set KEEP_MODULES=1 to copy them. +# Note these modules will be for the platform that built them, making the result +# no longer generic (it can still be published though as the modules will be +# ignored when pushing). KEEP_MODULES="${KEEP_MODULES-0}" main() { @@ -15,8 +21,10 @@ main() { source ./ci/lib.sh - VSCODE_SRC_PATH="vendor/modules/code-oss-dev" - VSCODE_OUT_PATH="$RELEASE_PATH/vendor/modules/code-oss-dev" + VSCODE_SRC_PATH="lib/vscode" + VSCODE_OUT_PATH="$RELEASE_PATH/lib/vscode" + + create_shrinkwraps mkdir -p "$RELEASE_PATH" @@ -24,8 +32,25 @@ main() { bundle_vscode rsync ./docs/README.md "$RELEASE_PATH" - rsync LICENSE.txt "$RELEASE_PATH" - rsync ./vendor/modules/code-oss-dev/ThirdPartyNotices.txt "$RELEASE_PATH" + rsync LICENSE "$RELEASE_PATH" + rsync ./lib/vscode/ThirdPartyNotices.txt "$RELEASE_PATH" + + if [ "$KEEP_MODULES" = 1 ]; then + # Copy the code-server launcher. + mkdir -p "$RELEASE_PATH/bin" + rsync ./ci/build/code-server.sh "$RELEASE_PATH/bin/code-server" + chmod 755 "$RELEASE_PATH/bin/code-server" + + # Delete the extra bin scripts. + rm "$RELEASE_PATH/lib/vscode/bin/remote-cli/code-darwin.sh" + rm "$RELEASE_PATH/lib/vscode/bin/remote-cli/code-linux.sh" + rm "$RELEASE_PATH/lib/vscode/bin/helpers/browser-darwin.sh" + rm "$RELEASE_PATH/lib/vscode/bin/helpers/browser-linux.sh" + if [ "$OS" != windows ] ; then + rm "$RELEASE_PATH/lib/vscode/bin/remote-cli/code.cmd" + rm "$RELEASE_PATH/lib/vscode/bin/helpers/browser.cmd" + fi + fi } bundle_code_server() { @@ -39,14 +64,11 @@ bundle_code_server() { rsync src/browser/pages/*.css "$RELEASE_PATH/src/browser/pages" rsync src/browser/robots.txt "$RELEASE_PATH/src/browser" - # Add typings for plugins - mkdir -p "$RELEASE_PATH/typings" - rsync typings/pluginapi.d.ts "$RELEASE_PATH/typings" - # Adds the commit to package.json - jq --slurp '.[0] * .[1]' package.json <( + jq --slurp '(.[0] | del(.scripts,.jest,.devDependencies)) * .[1]' package.json <( cat << EOF { + "version": "$(jq -r .codeServerVersion "./lib/vscode-reh-web-$VSCODE_TARGET/product.json")", "commit": "$(git rev-parse HEAD)", "scripts": { "postinstall": "sh ./postinstall.sh" @@ -54,54 +76,101 @@ bundle_code_server() { } EOF ) > "$RELEASE_PATH/package.json" - rsync yarn.lock "$RELEASE_PATH" - rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh" + mv npm-shrinkwrap.json "$RELEASE_PATH" if [ "$KEEP_MODULES" = 1 ]; then - rsync node_modules/ "$RELEASE_PATH/node_modules" - mkdir -p "$RELEASE_PATH/lib" - rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib" + local rsync_opts=(-a) + if [[ ${DEBUG-} = 1 ]]; then + rsync_opts+=(-vh) + fi + # If we build from source, exclude the prebuilds. + if [[ ${npm_config_build_from_source-} = true ]]; then + rsync_opts+=(--exclude /argon2/prebuilds) + fi + rsync "${rsync_opts[@]}" node_modules/ "$RELEASE_PATH/node_modules" + # Remove dev dependencies. + pushd "$RELEASE_PATH" + npm prune --production + popd fi + + rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh" } bundle_vscode() { mkdir -p "$VSCODE_OUT_PATH" - rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH" - rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out" - - rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions" - if [ "$KEEP_MODULES" = 0 ]; then - rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules" - else - rsync "$VSCODE_SRC_PATH/node_modules/" "$VSCODE_OUT_PATH/node_modules" + + local rsync_opts=(-a) + if [[ ${DEBUG-} = 1 ]]; then + rsync_opts+=(-vh) fi - rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions" - rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions" - rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions" - mkdir -p "$VSCODE_OUT_PATH/resources/"{linux,web} - rsync "$VSCODE_SRC_PATH/resources/linux/code.png" "$VSCODE_OUT_PATH/resources/linux/code.png" - rsync "$VSCODE_SRC_PATH/resources/web/callback.html" "$VSCODE_OUT_PATH/resources/web/callback.html" + # Some extensions have a .gitignore which excludes their built source from the + # npm package so exclude any .gitignore files. + rsync_opts+=(--exclude .gitignore) - # Add the commit and date and enable telemetry. This just makes telemetry - # available; telemetry can still be disabled by flag or setting. - jq --slurp '.[0] * .[1]' "$VSCODE_SRC_PATH/product.json" <( - cat << EOF - { - "enableTelemetry": true, - "commit": "$(git rev-parse HEAD)", - "date": $(jq -n 'now | todate') - } -EOF - ) > "$VSCODE_OUT_PATH/product.json" + # Exclude Node since we want to place it in a directory above. + rsync_opts+=(--exclude /node) + + # Exclude Node modules. Note that these will already only include production + # dependencies, so if we do keep them there is no need to do any + # post-processing to remove dev dependencies. + if [[ $KEEP_MODULES = 0 ]]; then + rsync_opts+=(--exclude node_modules) + fi + + rsync "${rsync_opts[@]}" "./lib/vscode-reh-web-$VSCODE_TARGET/" "$VSCODE_OUT_PATH" - # We remove the scripts field so that later on we can run - # yarn to fetch node_modules if necessary without build scripts running. - # We cannot use --no-scripts because we still want dependent package scripts to run. - jq 'del(.scripts)' < "$VSCODE_SRC_PATH/package.json" > "$VSCODE_OUT_PATH/package.json" + # Copy the Node binary. + if [[ $KEEP_MODULES = 1 ]]; then + cp "./lib/vscode-reh-web-$VSCODE_TARGET/node" "$RELEASE_PATH/lib" + fi + + # Merge the package.json for the web/remote server so we can include + # dependencies, since we want to ship this via NPM. + # Also override the name to prevent vulnerability scanners from + # misidentifying this package as VS Code (see #7071). + jq --slurp '.[0] * .[1] | .name = "code-oss"' \ + "$VSCODE_SRC_PATH/remote/package.json" \ + "$VSCODE_OUT_PATH/package.json" > "$VSCODE_OUT_PATH/package.json.merged" + mv "$VSCODE_OUT_PATH/package.json.merged" "$VSCODE_OUT_PATH/package.json" + cp "$VSCODE_SRC_PATH/remote/npm-shrinkwrap.json" "$VSCODE_OUT_PATH/npm-shrinkwrap.json" + + # Include global extension dependencies as well. + rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions/package.json" + cp "$VSCODE_SRC_PATH/extensions/npm-shrinkwrap.json" "$VSCODE_OUT_PATH/extensions/npm-shrinkwrap.json" + rsync "$VSCODE_SRC_PATH/extensions/postinstall.mjs" "$VSCODE_OUT_PATH/extensions/postinstall.mjs" +} + +create_shrinkwraps() { + # package-lock.json files (used to ensure deterministic versions of + # dependencies) are not packaged when publishing to the NPM registry. + # + # To ensure deterministic dependency versions (even when code-server is + # installed with NPM), we create an npm-shrinkwrap.json file from the + # currently installed node_modules. This ensures the versions used from + # development (that the package-lock.json guarantees) are also the ones + # installed by end-users. These will include devDependencies, but those will + # be ignored when installing globally (for code-server), and because we use + # --omit=dev (for VS Code). + + # We first generate the shrinkwrap file for code-server itself - which is the + # current directory. + cp package-lock.json package-lock.json.temp + npm shrinkwrap + mv package-lock.json.temp package-lock.json + + # Then the shrinkwrap files for the bundled VS Code. + pushd "$VSCODE_SRC_PATH/remote/" + cp package-lock.json package-lock.json.temp + npm shrinkwrap + mv package-lock.json.temp package-lock.json + popd - pushd "$VSCODE_OUT_PATH" - symlink_asar + pushd "$VSCODE_SRC_PATH/extensions/" + cp package-lock.json package-lock.json.temp + npm shrinkwrap + mv package-lock.json.temp package-lock.json popd } diff --git a/ci/build/build-standalone-release.sh b/ci/build/build-standalone-release.sh deleted file mode 100755 index 481110b47b39..000000000000 --- a/ci/build/build-standalone-release.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# This is due to an upstream issue with RHEL7/CentOS 7 comptability with node-argon2 -# See: https://github.com/cdr/code-server/pull/3422#pullrequestreview-677765057 -export npm_config_build_from_source=true - -main() { - cd "$(dirname "${0}")/../.." - - source ./ci/lib.sh - - rsync "$RELEASE_PATH/" "$RELEASE_PATH-standalone" - RELEASE_PATH+=-standalone - - # We cannot find the path to node from $PATH because yarn shims a script to ensure - # we use the same version it's using so we instead run a script with yarn that - # will print the path to node. - local node_path - node_path="$(yarn -s node <<< 'console.info(process.execPath)')" - - mkdir -p "$RELEASE_PATH/bin" - mkdir -p "$RELEASE_PATH/lib" - rsync ./ci/build/code-server.sh "$RELEASE_PATH/bin/code-server" - rsync "$node_path" "$RELEASE_PATH/lib/node" - - ln -s "./bin/code-server" "$RELEASE_PATH/code-server" - ln -s "./lib/node" "$RELEASE_PATH/node" - - cd "$RELEASE_PATH" - yarn --production --frozen-lockfile - - # HACK: the version of Typescript vscode 1.57 uses in extensions/ - # leaves a few stray symlinks. Clean them up so nfpm does not fail. - # Remove this line when its no longer needed. - - rm -fr "$RELEASE_PATH/vendor/modules/code-oss-dev/extensions/node_modules/.bin" -} - -main "$@" diff --git a/ci/build/build-vscode.sh b/ci/build/build-vscode.sh index ca35d4f3b064..871a801aa30f 100755 --- a/ci/build/build-vscode.sh +++ b/ci/build/build-vscode.sh @@ -1,21 +1,159 @@ #!/usr/bin/env bash set -euo pipefail -# Builds vscode into vendor/modules/code-oss-dev/out-vscode. +# Builds vscode into lib/vscode/out-vscode. # MINIFY controls whether a minified version of vscode is built. MINIFY=${MINIFY-true} +fix-bin-script() { + local script="lib/vscode-reh-web-$VSCODE_TARGET/bin/$1" + sed -i.bak "s/@@VERSION@@/$(vscode_version)/g" "$script" + sed -i.bak "s/@@COMMIT@@/$BUILD_SOURCEVERSION/g" "$script" + sed -i.bak "s/@@APPNAME@@/code-server/g" "$script" + + # Fix Node path on Darwin and Linux. + # We do not want expansion here; this text should make it to the file as-is. + # shellcheck disable=SC2016 + sed -i.bak 's/^ROOT=\(.*\)$/VSROOT=\1\nROOT="$(dirname "$(dirname "$VSROOT")")"/g' "$script" + sed -i.bak 's/ROOT\/out/VSROOT\/out/g' "$script" + # We do not want expansion here; this text should make it to the file as-is. + # shellcheck disable=SC2016 + sed -i.bak 's/$ROOT\/node/${NODE_EXEC_PATH:-$ROOT\/lib\/node}/g' "$script" + + # Fix Node path on Windows. + sed -i.bak 's/^set ROOT_DIR=\(.*\)$/set ROOT_DIR=%~dp0..\\..\\..\\..\r\nset VSROOT_DIR=\1/g' "$script" + sed -i.bak 's/%ROOT_DIR%\\out/%VSROOT_DIR%\\out/g' "$script" + + chmod +x "$script" + rm "$script.bak" +} + +copy-bin-script() { + cp "lib/vscode/resources/server/bin/$1" "lib/vscode-reh-web-$VSCODE_TARGET/bin/$1" + fix-bin-script "$1" +} + main() { cd "$(dirname "${0}")/../.." - cd vendor/modules/code-oss-dev + source ./ci/lib.sh + + # Set the commit Code will embed into the product.json. We need to do this + # since Code tries to get the commit from the `.git` directory which will fail + # as it is a submodule. + # + # Also, we use code-server's commit rather than VS Code's otherwise it would + # not update when only our patch files change, and that will cause caching + # issues where the browser keeps using outdated code. + export BUILD_SOURCEVERSION + BUILD_SOURCEVERSION=$(git rev-parse HEAD) + + pushd lib/vscode - yarn gulp compile-build compile-extensions-build compile-extension-media - yarn gulp optimize --gulpfile ./coder.js - if [[ $MINIFY ]]; then - yarn gulp minify --gulpfile ./coder.js + if [[ ! ${VERSION-} ]]; then + echo "VERSION not set. Please set before running this script:" + echo "VERSION='0.0.0' npm run build:vscode" + exit 1 fi + + # Add the date, our name, links, enable telemetry (this just makes telemetry + # available; telemetry can still be disabled by flag or setting), and + # configure trusted extensions (since some, like github.copilot-chat, never + # ask to be trusted and this is the only way to get auth working). + # + # This needs to be done before building as Code will read this file and embed + # it into the client-side code. + git checkout product.json # Reset in case the script exited early. + cp product.json product.original.json # Since jq has no inline edit. + jq --slurp '.[0] * .[1]' product.original.json <( + cat << EOF + { + "enableTelemetry": true, + "quality": "stable", + "codeServerVersion": "$VERSION", + "nameShort": "code-server", + "nameLong": "code-server", + "applicationName": "code-server", + "dataFolderName": ".code-server", + "win32MutexName": "codeserver", + "licenseUrl": "https://github.com/coder/code-server/blob/main/LICENSE", + "win32DirName": "code-server", + "win32NameVersion": "code-server", + "win32AppUserModelId": "coder.code-server", + "win32ShellNameShort": "c&ode-server", + "darwinBundleIdentifier": "com.coder.code.server", + "linuxIconName": "com.coder.code.server", + "reportIssueUrl": "https://github.com/coder/code-server/issues/new", + "documentationUrl": "https://go.microsoft.com/fwlink/?LinkID=533484#vscode", + "keyboardShortcutsUrlMac": "https://go.microsoft.com/fwlink/?linkid=832143", + "keyboardShortcutsUrlLinux": "https://go.microsoft.com/fwlink/?linkid=832144", + "keyboardShortcutsUrlWin": "https://go.microsoft.com/fwlink/?linkid=832145", + "introductoryVideosUrl": "https://go.microsoft.com/fwlink/?linkid=832146", + "tipsAndTricksUrl": "https://go.microsoft.com/fwlink/?linkid=852118", + "newsletterSignupUrl": "https://www.research.net/r/vsc-newsletter", + "linkProtectionTrustedDomains": [ + "https://open-vsx.org" + ], + "trustedExtensionAuthAccess": [ + "vscode.git", "vscode.github", + "github.vscode-pull-request-github", + "github.copilot", "github.copilot-chat" + ], + "aiConfig": { + "ariaKey": "code-server" + } + } +EOF + ) > product.json + + + VSCODE_QUALITY=stable npm run gulp compile-copilot-extension-full-build + + npm run gulp core-ci + npm run gulp "vscode-reh-web-$VSCODE_TARGET${MINIFY:+-min}-ci" + + # Reset so if you develop after building you will not be stuck with the wrong + # commit (the dev client will use `oss-dev` but the dev server will still use + # product.json which will have `stable-$commit`). + git checkout product.json + + popd + + pushd "lib/vscode-reh-web-$VSCODE_TARGET" + # Make sure Code took the version we set in the environment variable. Not + # having a version will break display languages. + if ! jq -e .commit product.json; then + echo "'commit' is missing from product.json" + exit 1 + fi + popd + + # Set vars and fix paths. + case $OS in + windows) + fix-bin-script remote-cli/code.cmd + fix-bin-script helpers/browser.cmd + ;; + *) + fix-bin-script remote-cli/code-server + fix-bin-script helpers/browser.sh + ;; + esac + + # Include bin scripts for other platforms so we can use the right one in the + # NPM post-install. + + # These provide a `code-server` command in the integrated terminal to open + # files in the current instance. + copy-bin-script remote-cli/code-darwin.sh + copy-bin-script remote-cli/code-linux.sh + copy-bin-script remote-cli/code.cmd + + # These provide a way for terminal applications to open browser windows. + copy-bin-script helpers/browser-darwin.sh + copy-bin-script helpers/browser-linux.sh + copy-bin-script helpers/browser.cmd } main "$@" diff --git a/ci/build/code-server.sh b/ci/build/code-server.sh index cb71c182775a..801835ea843b 100755 --- a/ci/build/code-server.sh +++ b/ci/build/code-server.sh @@ -5,20 +5,12 @@ set -eu # Runs code-server with the bundled node binary. _realpath() { - # See https://github.com/cdr/code-server/issues/1537 on why no realpath or readlink -f. + # See https://github.com/coder/code-server/issues/1537 on why no realpath or readlink -f. script="$1" cd "$(dirname "$script")" while [ -L "$(basename "$script")" ]; do - if [ -L "./node" ] && [ -L "./code-server" ] \ - && [ -f "package.json" ] \ - && cat package.json | grep -q '^ "name": "code-server",$'; then - echo "***** Please use the script in bin/code-server instead!" >&2 - echo "***** This script will soon be removed!" >&2 - echo "***** See the release notes at https://github.com/cdr/code-server/releases/tag/v3.4.0" >&2 - fi - script="$(readlink "$(basename "$script")")" cd "$(dirname "$script")" done diff --git a/ci/build/nfpm.yaml b/ci/build/nfpm.yaml index 93142d198879..4b0ee371a2eb 100644 --- a/ci/build/nfpm.yaml +++ b/ci/build/nfpm.yaml @@ -4,11 +4,11 @@ platform: "linux" version: "v${VERSION}" section: "devel" priority: "optional" -maintainer: "Anmol Sethi " +maintainer: "Joe Previte " description: | Run VS Code in the browser. vendor: "Coder" -homepage: "https://github.com/cdr/code-server" +homepage: "https://github.com/coder/code-server" license: "MIT" contents: @@ -21,5 +21,5 @@ contents: - src: ./ci/build/code-server-user.service dst: /usr/lib/systemd/user/code-server.service - - src: ./release-standalone/* - dst: /usr/lib/code-server/ + - src: ./release/* + dst: /usr/lib/code-server diff --git a/ci/build/npm-postinstall.sh b/ci/build/npm-postinstall.sh index 76c558d1f823..b4ff69e66ebc 100755 --- a/ci/build/npm-postinstall.sh +++ b/ci/build/npm-postinstall.sh @@ -1,26 +1,58 @@ #!/usr/bin/env sh set -eu -# Copied from arch() in ci/lib.sh. -detect_arch() { - case "$(uname -m)" in - aarch64) - echo arm64 - ;; - x86_64 | amd64) - echo amd64 - ;; - *) - # This will cause the download to fail, but is intentional - uname -m - ;; +# Copied from ../lib.sh except we do not rename Darwin and we do not need to +# detect Alpine. +os() { + osname=$(uname | tr '[:upper:]' '[:lower:]') + case $osname in + cygwin* | mingw*) osname="windows" ;; esac + echo "$osname" +} + +# Create a symlink at $2 pointing to $1 on any platform. Anything that +# currently exists at $2 will be deleted. +symlink() { + source="$1" + dest="$2" + rm -rf "$dest" + case $OS in + windows) mklink /J "$dest" "$source" ;; + *) ln -s "$source" "$dest" ;; + esac +} + +# Make a symlink at bin/$1/$3 pointing to the platform-specific version of the +# script in $2. The extension of the link will be .cmd for Windows otherwise it +# will be whatever is in $4 (or no extension if $4 is not set). +symlink_bin_script() { + oldpwd="$(pwd)" + cd "bin/$1" + source="$2" + dest="$3" + ext="${4-}" + case $OS in + windows) symlink "$source.cmd" "$dest.cmd" ;; + darwin | macos) symlink "$source-darwin.sh" "$dest$ext" ;; + *) symlink "$source-linux.sh" "$dest$ext" ;; + esac + cd "$oldpwd" +} + +command_exists() { + if [ ! "$1" ]; then return 1; fi + command -v "$@" > /dev/null +} + +is_root() { + if command_exists id && [ "$(id -u)" = 0 ]; then + return 0 + fi + return 1 } -ARCH="${NPM_CONFIG_ARCH:-$(detect_arch)}" -# This is due to an upstream issue with RHEL7/CentOS 7 comptability with node-argon2 -# See: https://github.com/cdr/code-server/pull/3422#pullrequestreview-677765057 -export npm_config_build_from_source=true +OS="$(os)" main() { # Grabs the major version of node from $npm_config_user_agent which looks like @@ -33,8 +65,8 @@ main() { echo "USE AT YOUR OWN RISK!" fi - if [ "$major_node_version" -ne "${FORCE_NODE_VERSION:-14}" ]; then - echo "ERROR: code-server currently requires node v14." + if [ "$major_node_version" -ne "${FORCE_NODE_VERSION:-24}" ]; then + echo "ERROR: code-server currently requires node v24." if [ -n "$FORCE_NODE_VERSION" ]; then echo "However, you have overrided the version check to use v$FORCE_NODE_VERSION." fi @@ -44,28 +76,24 @@ main() { exit 1 fi - case "${npm_config_user_agent-}" in npm*) - # We are running under npm. - if [ "${npm_config_unsafe_perm-}" != "true" ]; then - echo "Please pass --unsafe-perm to npm to install code-server" - echo "Otherwise the postinstall script does not have permissions to run" - echo "See https://docs.npmjs.com/misc/config#unsafe-perm" - echo "See https://stackoverflow.com/questions/49084929/npm-sudo-global-installation-unsafe-perm" - exit 1 - fi - ;; - esac - - OS="$(uname | tr '[:upper:]' '[:lower:]')" - if curl -fsSL "https://github.com/cdr/cloud-agent/releases/latest/download/cloud-agent-$OS-$ARCH" -o ./lib/coder-cloud-agent; then - chmod +x ./lib/coder-cloud-agent - else - echo "Failed to download cloud agent; --link will not work" + # Under npm, if we are running as root, we need --unsafe-perm otherwise + # post-install scripts will not have sufficient permissions to do their thing. + if is_root; then + case "${npm_config_user_agent-}" in npm*) + if [ "${npm_config_unsafe_perm-}" != "true" ]; then + echo "Please pass --unsafe-perm to npm to install code-server" + echo "Otherwise post-install scripts will not have permissions to run" + echo "See https://docs.npmjs.com/misc/config#unsafe-perm" + echo "See https://stackoverflow.com/questions/49084929/npm-sudo-global-installation-unsafe-perm" + exit 1 + fi + ;; + esac fi - if ! vscode_yarn; then + if ! vscode_install; then echo "You may not have the required dependencies to build the native modules." - echo "Please see https://github.com/cdr/code-server/blob/master/docs/npm.md" + echo "Please see https://github.com/coder/code-server/blob/main/docs/npm.md" exit 1 fi @@ -76,33 +104,45 @@ main() { fi } -# This is a copy of symlink_asar in ../lib.sh. Look there for details. -symlink_asar() { - rm -rf node_modules.asar - if [ "${WINDIR-}" ]; then - mklink /J node_modules.asar node_modules - else - ln -s node_modules node_modules.asar - fi +install_with_yarn_or_npm() { + echo "User agent: ${npm_config_user_agent-none}" + # For development we enforce npm, but for installing the package as an + # end-user we want to keep using whatever package manager is in use. + case "${npm_config_user_agent-}" in + npm*) + if ! npm install --unsafe-perm --omit=dev; then + return 1 + fi + ;; + yarn*) + if ! yarn --production --frozen-lockfile --no-default-rc; then + return 1 + fi + ;; + *) + echo "Could not determine which package manager is being used to install code-server" + exit 1 + ;; + esac + return 0 } -vscode_yarn() { - echo 'Installing vendor dependencies...' - cd vendor/modules/code-oss-dev - yarn --production --frozen-lockfile +vscode_install() { + echo 'Installing Code dependencies...' + cd lib/vscode + if ! install_with_yarn_or_npm; then + return 1 + fi - symlink_asar + symlink_bin_script remote-cli code code-server + symlink_bin_script helpers browser browser .sh cd extensions - yarn --production --frozen-lockfile - - for ext in */; do - ext="${ext%/}" - echo "extensions/$ext: installing dependencies" - cd "$ext" - yarn --production --frozen-lockfile - cd "$OLDPWD" - done + if ! install_with_yarn_or_npm; then + return 1 + fi + + return 0 } main "$@" diff --git a/ci/build/release-github-assets.sh b/ci/build/release-github-assets.sh deleted file mode 100755 index 43083e1373d2..000000000000 --- a/ci/build/release-github-assets.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Downloads the release artifacts from CI for the current -# commit and then uploads them to the release with the version -# in package.json. -# You will need $GITHUB_TOKEN set. - -main() { - cd "$(dirname "$0")/../.." - source ./ci/lib.sh - - download_artifact release-packages ./release-packages - local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.deb,.rpm}) - - EDITOR=true gh release upload "v$VERSION" "${assets[@]}" -} - -main "$@" diff --git a/ci/build/release-github-draft.sh b/ci/build/release-github-draft.sh deleted file mode 100755 index 69073e4dc11f..000000000000 --- a/ci/build/release-github-draft.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Creates a draft release with the template for the version in package.json - -main() { - cd "$(dirname "$0")/../.." - source ./ci/lib.sh - - gh release create "v$VERSION" \ - --notes-file - \ - --target "$(git rev-parse HEAD)" \ - --draft << EOF -v$VERSION - -VS Code v$(vscode_version) - -Upgrading is as easy as installing the new version over the old one. code-server -maintains all user data in \`~/.local/share/code-server\` so that it is preserved in between -installations. - -## New Features - -⭐ Summarize new features here with references to issues - - - item - -## Bug Fixes - -⭐ Summarize bug fixes here with references to issues - - - item - -## Documentation - -⭐ Summarize doc changes here with references to issues - - - item - -## Development - -⭐ Summarize development/testing changes here with references to issues - - - item - -Cheers! 🍻 -EOF -} - -main "$@" diff --git a/ci/build/release-prep.sh b/ci/build/release-prep.sh deleted file mode 100755 index 91189fb53268..000000000000 --- a/ci/build/release-prep.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# Description: This is a script to make the release process easier -# Run it with `yarn release:prep` and it will do the following: -# 1. Check that you have gh installed and that you're signed in -# 2. Update the version of code-server (package.json, docs, etc.) -# 3. Update the code coverage badge in the README -# 4. Open a draft PR using the release_template.md and view in browser -# If you want to perform a dry run of this script run DRY_RUN=1 yarn release:prep - -set -euo pipefail - -main() { - if [ "${DRY_RUN-}" = 1 ]; then - echo "Performing a dry run..." - CMD="echo" - else - CMD='' - fi - - cd "$(dirname "$0")/../.." - - # Check that gh is installed - if ! command -v gh &> /dev/null; then - echo "gh could not be found." - echo "We use this with the release-github-draft.sh and release-github-assets.sh scripts." - echo -e "See docs here: https://github.com/cli/cli#installation" - exit - fi - - # Check that they have jq installed - if ! command -v jq &> /dev/null; then - echo "jq could not be found." - echo "We use this to parse the package.json and grab the current version of code-server." - echo -e "See docs here: https://stedolan.github.io/jq/download/" - exit - fi - - # Check that they have rg installed - if ! command -v rg &> /dev/null; then - echo "rg could not be found." - echo "We use this when updating files across the codebase." - echo -e "See docs here: https://github.com/BurntSushi/ripgrep#installation" - exit - fi - - # Check that they have node installed - if ! command -v node &> /dev/null; then - echo "node could not be found." - echo "That's surprising..." - echo "We use it in this script for getting the package.json version" - echo -e "See docs here: https://nodejs.org/en/download/" - exit - fi - - # Check that gh is authenticated - if ! gh auth status -h github.com &> /dev/null; then - echo "gh isn't authenticated to github.com." - echo "This is needed for our scripts that use gh." - echo -e "See docs regarding authentication: https://cli.github.com/manual/gh_auth_login" - exit - fi - - # Note: we need to set upstream as well or the gh pr create step will fail - # See: https://github.com/cli/cli/issues/575 - CURRENT_BRANCH=$(git branch | grep '\*' | cut -d' ' -f2-) - if [[ -z $(git config "branch.${CURRENT_BRANCH}.remote") ]]; then - echo "Doesn't look like you've pushed this branch to remote" - # Note: we need to set upstream as well or the gh pr create step will fail - # See: https://github.com/cli/cli/issues/575 - echo "Please set the upstream and then run the script" - exit 1 - fi - - # credit to jakwuh for this solution - # https://gist.github.com/DarrenN/8c6a5b969481725a4413#gistcomment-1971123 - CODE_SERVER_CURRENT_VERSION=$(node -pe "require('./package.json').version") - # Ask which version we should update to - # In the future, we'll automate this and determine the latest version automatically - echo "Current version: ${CODE_SERVER_CURRENT_VERSION}" - # The $'\n' adds a line break. See: https://stackoverflow.com/a/39581815/3015595 - read -r -p "What version of code-server do you want to update to?"$'\n' CODE_SERVER_VERSION_TO_UPDATE - - echo -e "Great! We'll prep a PR for updating to $CODE_SERVER_VERSION_TO_UPDATE\n" - $CMD rg -g '!yarn.lock' -g '!*.svg' -g '!CHANGELOG.md' --files-with-matches --fixed-strings "${CODE_SERVER_CURRENT_VERSION}" | $CMD xargs sd "$CODE_SERVER_CURRENT_VERSION" "$CODE_SERVER_VERSION_TO_UPDATE" - - $CMD git commit -am "chore(release): bump version to $CODE_SERVER_VERSION_TO_UPDATE" - - # This runs from the root so that's why we use this path vs. ../../ - RELEASE_TEMPLATE_STRING=$(cat ./.github/PULL_REQUEST_TEMPLATE/release_template.md) - - echo -e "\nOpening a draft PR on GitHub" - # To read about these flags, visit the docs: https://cli.github.com/manual/gh_pr_create - $CMD gh pr create --base main --title "release: $CODE_SERVER_VERSION_TO_UPDATE" --body "$RELEASE_TEMPLATE_STRING" --reviewer @cdr/code-server-reviewers --repo cdr/code-server --draft --assignee "@me" - - # Open PR in browser - $CMD gh pr view --web -} - -main "$@" diff --git a/ci/build/test-standalone-release.sh b/ci/build/test-standalone-release.sh deleted file mode 100755 index 73961dd44d93..000000000000 --- a/ci/build/test-standalone-release.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Make sure a code-server release works. You can pass in the path otherwise it -# will use release-standalone in the current directory. -# -# This is to make sure we don't have Node version errors or any other -# compilation-related errors. -main() { - cd "$(dirname "${0}")/../.." - - local EXTENSIONS_DIR - EXTENSIONS_DIR="$(mktemp -d)" - - local path=${1:-./release-standalone/bin/code-server} - - echo "Testing standalone release in $path." - - # NOTE: using a basic theme extension because it doesn't update often and is more reliable for testing - "$path" --extensions-dir "$EXTENSIONS_DIR" --install-extension wesbos.theme-cobalt2 - local installed_extensions - installed_extensions="$("$path" --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)" - # We use grep as wesbos.theme-cobalt2 may have dependency extensions that change. - if ! echo "$installed_extensions" | grep -q "wesbos.theme-cobalt2"; then - echo "Unexpected output from listing extensions:" - echo "$installed_extensions" - exit 1 - fi - - echo "Standalone release works correctly." -} - -main "$@" diff --git a/ci/build/update-repo.sh b/ci/build/update-repo.sh new file mode 100755 index 000000000000..0f1924fa91a4 --- /dev/null +++ b/ci/build/update-repo.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Given versions $1 and $2 figure out the first component that is different +# (major, minor, patch). +function find_version_diff() { + # shellcheck disable=SC2206 + local a=( ${1//./ } ) + # shellcheck disable=SC2206 + local b=( ${2//./ } ) + + if [[ ${a[0]} != "${b[0]}" ]] ; then + echo major + elif [[ ${a[1]} != "${b[1]}" ]] ; then + echo minor + else + echo patch + fi +} + +# Bump $1 by the bump type (major, minor, patch) in $2. +function bump_version() { + # shellcheck disable=SC2206 + local a=( ${1//./ } ) + case $2 in + major) + ((a[0]++)) + a[1]=0 + a[2]=0 + ;; + minor) + ((a[1]++)) + a[2]=0 + ;; + *) + ((a[2]++)) + ;; + esac + echo "${a[0]}.${a[1]}.${a[2]}" +} + +function update_helm() { + local chart_version + chart_version=$(yq .version ci/helm-chart/Chart.yaml) + local app_version + app_version=$(yq .appVersion ci/helm-chart/Chart.yaml) + local image_version + image_version=$(yq .image.tag ci/helm-chart/values.yaml) + + local bump_type + bump_type=$(find_version_diff "$app_version" "$version") + local chart_version_bump + chart_version_bump=$(bump_version "$chart_version" "$bump_type") + + # Use sed to replace because yq will reformat. + echo "Bumping version from $chart_version to $chart_version_bump..." + sed -i.bak "s/^version: $chart_version\$/version: $chart_version_bump/" ci/helm-chart/Chart.yaml + + echo "Bumping app version from $app_version to $version..." + sed -i.bak "s/^appVersion: .\+\$/appVersion: $version/" ci/helm-chart/Chart.yaml + + echo "Bumping image version from $image_version to $version..." + sed -i.bak "s/^ tag: .\+\$/ tag: '$version'/" ci/helm-chart/values.yaml +} + +function update_changelog() { + local date + date=$(printf '%(%Y-%m-%d)T\n' -1) + local link="https://github.com/coder/code-server/releases/tag/v$version" + sed -i.bak "s|## Unreleased|## Unreleased\n\n## [$version]($link) - $date|" CHANGELOG.md +} + +function main() { + cd "$(dirname "${0}")/../.." + + source ./ci/lib.sh + + local version=${VERSION:-$(git describe --tags)} + version="${version#v}" + + declare -a steps + + steps+=( + "Update Helm chart" "update_helm" + "Update changelog" "update_changelog" + ) + + # Even if a step failed, still output the last checkmark. + run-steps "${steps[@]}" || true + + # This step is always manual. + echo "- [ ] https://github.com/coder/code-server-aur/pulls" >> .cache/checklist +} + +main "$@" diff --git a/ci/build/update-vscode.sh b/ci/build/update-vscode.sh new file mode 100755 index 000000000000..b3b4857d2eb4 --- /dev/null +++ b/ci/build/update-vscode.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +function unapply_patches() { + local -i exit_code=0 + quiet quilt pop -af 2>&1 || exit_code=$? + case $exit_code in + # Sucessfully unapplied. + 0) ;; + # No more patches to unapply. + 2) ;; + # Some error. + *) return $exit_code ;; + esac +} + +function apply_patches() { + local -i exit_code=0 + quiet quilt push -a 2>&1 || exit_code=$? + case $exit_code in + # Sucessfully applied. + 0) ;; + # No more patches to apply. + 2) ;; + # Some error. + *) return $exit_code ;; + esac +} + +function update_vscode() { + pushd lib/vscode + if ! git checkout 2>&1 "$target_vscode_version" ; then + echo "$target_vscode_version does not exist locally, fetching..." + git fetch --all --prune --tags + echo "Checking out $target_vscode_version again..." + git checkout "$target_vscode_version" + fi + popd +} + +function refresh_patches() { + local -i exit_code=0 + while quiet quilt push 2>&1 ; ! (( exit_code=$? )) ; do + quilt refresh 2>&1 + done + case $exit_code in + # No more patches to apply. + 2) ;; + # Some error. + *) return $exit_code ;; + esac +} + +function update_node() { + local node_version + node_version=$(cat .node-version) + local target_node_version + target_node_version=$(grep target lib/vscode/remote/.npmrc | awk -F= '{print $2}' | tr -d '"') + if [[ $node_version == "$target_node_version" ]] ; then + echo "Already set to $target_node_version" + else + echo "Updating from $node_version to $target_node_version..." + echo "$target_node_version" > .node-version + fi +} + +function get-webview-script-hash() { + local html + html=$(<"$1") + local start_tag='" + html=${html##*"$start_tag"} + html=${html%%"$end_tag"*} + echo -n "$html" | openssl sha256 -binary | openssl base64 +} + +function delete_csp() { + quilt delete csp-hashes.diff || true +} + +function update_csp() { + apply_patches + + local files=( + ./lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index.html + ./lib/vscode/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html + ) + + quilt new csp-hashes.diff + + local file + for file in "${files[@]}" ; do + quilt add "$file" + + local hash + hash=$(get-webview-script-hash "$file") + echo "Calculated hash as $hash" + # Use octothorpe as a delimiter since the hash may contain a slash. + sed -i.bak "s#'sha256-[^']\+'#'sha256-$hash'#" "$file" + done + + quilt refresh +} + +function add_changelog() { + local file=CHANGELOG.md + if grep --quiet "Code $target_vscode_version" "$file" ; then + echo "Changelog for $target_vscode_version already exists" + else + # TODO: This is not exactly robust. In particular, it needs to handle if + # there is already a "changed" section. + sed -i.bak "s/## Unreleased/## Unreleased\n\nCode v$target_vscode_version\n\n### Changed\n\n- Update to Code $target_vscode_version/" "$file" + fi +} + +function main() { + cd "$(dirname "${0}")/../.." + + source ./ci/lib.sh + + declare -a steps + + # Hashes are always regenerated to avoid having to resolve conflicts.. + steps+=("Revert CSP hashes" "delete_csp") + + # If version is not set, assume we are already at the target version and the + # user is just trying to resolve conflics. + local target_vscode_version + if [[ ${VERSION-} ]] ; then + # Removing patches only needs to be done locally; in CI we start from a + # fresh clone each time. + if [[ ! ${CI-} ]] ; then + steps+=("Unapplying patches" "unapply_patches") + fi + target_vscode_version="${VERSION#v}" + steps+=( + "Update Code to $target_vscode_version" "update_vscode" + "Refresh Code patches" "refresh_patches" + ) + else + target_vscode_version="$(git -C lib/vscode describe --tags --exact-match)" + echo "Detected Code version $target_vscode_version" + fi + + steps+=( + "Update Node version" "update_node" + "Regenerate CSP hashes" "update_csp" + "Add changelog note" "add_changelog" + ) + + # Even if a step failed, still output the last checkmark. + run-steps "${steps[@]}" || true + + # This step is always manual. + echo "- [ ] Verify changelog" >> .cache/checklist +} + +main "$@" diff --git a/ci/dev/audit.sh b/ci/dev/audit.sh deleted file mode 100755 index 0142eac2a906..000000000000 --- a/ci/dev/audit.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/../.." - - # Prevents integration with moderate or higher vulnerabilities - # Docs: https://github.com/IBM/audit-ci#options - yarn audit-ci --moderate -} - -main "$@" diff --git a/ci/dev/ci.sh b/ci/dev/ci.sh deleted file mode 100755 index daf4ecc69414..000000000000 --- a/ci/dev/ci.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/../.." - - yarn fmt - yarn lint - yarn _audit - yarn test:unit -} - -main "$@" diff --git a/ci/dev/fmt.sh b/ci/dev/doctoc.sh similarity index 68% rename from ci/dev/fmt.sh rename to ci/dev/doctoc.sh index d5d7ffeed9c7..39930f21e0b5 100755 --- a/ci/dev/fmt.sh +++ b/ci/dev/doctoc.sh @@ -4,24 +4,6 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - local prettierExts - prettierExts=( - "*.js" - "*.ts" - "*.tsx" - "*.html" - "*.json" - "*.css" - "*.md" - "*.toml" - "*.yaml" - "*.yml" - "*.sh" - ) - prettier --write --loglevel=warn $( - git ls-files "${prettierExts[@]}" | grep -v "lib/vscode" | grep -v "vendor/modules/code-oss-dev" | grep -v 'helm-chart' - ) - doctoc --title '# FAQ' docs/FAQ.md > /dev/null doctoc --title '# Setup Guide' docs/guide.md > /dev/null doctoc --title '# Install' docs/install.md > /dev/null @@ -32,12 +14,11 @@ main() { doctoc --title '# iPad' docs/ipad.md > /dev/null doctoc --title '# Termux' docs/termux.md > /dev/null - # TODO: replace with a method that generates fewer false positives. if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then echo "Files need generation or are formatted incorrectly:" git -c color.ui=always status | grep --color=no '\[31m' echo "Please run the following locally:" - echo " yarn fmt" + echo " npm run doctoc" exit 1 fi } diff --git a/ci/dev/gen_icons.sh b/ci/dev/gen_icons.sh index 9d27486dcc57..39d509bc4f4b 100755 --- a/ci/dev/gen_icons.sh +++ b/ci/dev/gen_icons.sh @@ -1,44 +1,50 @@ #!/bin/sh set -eu +# Generate icons from a single favicon.svg. favicon.svg should have no fill +# colors set. main() { cd src/browser/media - # We need .ico for backwards compatibility. - # The other two are the only icon sizes required by Chrome and - # we use them for stuff like apple-touch-icon as well. - # https://web.dev/add-manifest/ + # We need .ico for backwards compatibility. The other two are the only icon + # sizes required by Chrome and we use them for stuff like apple-touch-icon as + # well. https://web.dev/add-manifest/ # # This should be enough and we can always add more if there are problems. - + # + # -quiet to avoid https://github.com/ImageMagick/ImageMagick/issues/884 # -background defaults to white but we want it transparent. + # -density somehow makes the image both sharper and smaller in file size. + # # https://imagemagick.org/script/command-line-options.php#background - convert -quiet -background transparent -resize 256x256 favicon.svg favicon.ico - # We do not generate the pwa-icon from the favicon as they are slightly different - # designs and sizes. - # See favicon.afdesign and #2401 for details on the differences. - convert -quiet -background transparent -resize 192x192 pwa-icon.png pwa-icon-192.png - convert -quiet -background transparent -resize 512x512 pwa-icon.png pwa-icon-512.png + convert -quiet -background transparent \ + -resize 256x256 -density 256x256 \ + favicon.svg favicon.ico - # We use -quiet above to avoid https://github.com/ImageMagick/ImageMagick/issues/884 + # Generate PWA icons. There should be enough padding to support masking. + convert -quiet -border 60x60 -bordercolor white -background white \ + -resize 192x192 -density 192x192 \ + favicon.svg pwa-icon-maskable-192.png + convert -quiet -border 160x160 -bordercolor white -background white \ + -resize 512x512 -density 512x512 \ + favicon.svg pwa-icon-maskable-512.png - # The following adds dark mode support for the favicon as favicon-dark-support.svg - # There is no similar capability for pwas or .ico so we can only add support to the svg. - favicon_dark_style="" - # See https://stackoverflow.com/a/22901380/4283659 - # This escapes all newlines so that sed will accept them. - favicon_dark_style="$(printf "%s\n" "$favicon_dark_style" | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/\\n/g')" - sed "$( - cat -n << EOF -s% favicon-dark-support.svg + # Generate non-maskable PWA icons. + magick pwa-icon-maskable-192.png \ + \( +clone -threshold 101% -fill white -draw "roundRectangle 0,0 %[fx:int(w)],%[fx:int(h)] 50,50" \) \ + -channel-fx "| gray=>alpha" \ + pwa-icon-192.png + magick pwa-icon-maskable-512.png \ + \( +clone -threshold 101% -fill white -draw "roundRectangle 0,0 %[fx:int(w)],%[fx:int(h)] 100,100" \) \ + -channel-fx "| gray=>alpha" \ + pwa-icon-512.png + + # The following adds dark mode support for the favicon as + # favicon-dark-support.svg There is no similar capability for pwas or .ico so + # we can only add support to the svg. + favicon_dark_style="" + cp favicon.svg favicon-dark-support.svg + sed "s% favicon-dark-support.svg } main "$@" diff --git a/ci/dev/lint-scripts.sh b/ci/dev/lint-scripts.sh new file mode 100755 index 000000000000..742030affc62 --- /dev/null +++ b/ci/dev/lint-scripts.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +main() { + cd "$(dirname "$0")/../.." + shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files '*.sh' | grep -v 'lib/vscode') +} + +main "$@" diff --git a/ci/dev/lint.sh b/ci/dev/lint.sh deleted file mode 100755 index 34b2e8f78fd9..000000000000 --- a/ci/dev/lint.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/../.." - - eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js" | grep -v "vendor/modules/code-oss-dev" | grep -v "lib/vscode") - stylelint $(git ls-files "*.css" | grep -v "vendor/modules/code-oss-dev" | grep -v "lib/vscode") - tsc --noEmit --skipLibCheck - shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh" | grep -v "vendor/modules/code-oss-dev" | grep -v "lib/vscode") - if command -v helm && helm kubeval --help > /dev/null; then - helm kubeval ci/helm-chart - fi - - cd "$OLDPWD" -} - -main "$@" diff --git a/ci/dev/postinstall.sh b/ci/dev/postinstall.sh index 026ef5f3a225..63aad7bcf985 100755 --- a/ci/dev/postinstall.sh +++ b/ci/dev/postinstall.sh @@ -1,37 +1,38 @@ #!/usr/bin/env bash set -euo pipefail -main() { - cd "$(dirname "$0")/../.." - - echo "Installing code-server test dependencies..." - - cd test - yarn install - cd .. - - cd vendor - echo "Installing vendor dependencies..." - - # * We install in 'modules' instead of 'node_modules' because VS Code's extensions - # use a webpack config which cannot differentiate between its own node_modules - # and itself being in a directory with the same name. - # - # * We ignore scripts because NPM/Yarn's default behavior is to assume that - # devDependencies are not needed, and that even git repo based packages are - # assumed to be compiled. Because the default behavior for VS Code's `postinstall` - # assumes we're also compiled, this needs to be ignored. - - local args=(install --modules-folder modules --ignore-scripts) - +# Install dependencies in $1. +install-deps() { + local args=() if [[ ${CI-} ]]; then - args+=("--frozen-lockfile") + args+=(ci) + else + args+=(install) fi + # If there is no package.json then npm will look upward and end up installing + # from the root resulting in an infinite loop (this can happen if you have not + # checked out the submodule yet for example). + if [[ ! -f "$1/package.json" ]]; then + echo "$1/package.json is missing; did you run git submodule update --init?" + exit 1 + fi + pushd "$1" + echo "Installing dependencies for $PWD" + npm "${args[@]}" + popd +} - yarn "${args[@]}" - - # Finally, run the vendor `postinstall` - yarn run postinstall +main() { + cd "$(dirname "$0")/../.." + source ./ci/lib.sh + + install-deps test + install-deps test/e2e/extensions/test-extension + # We don't need these when running the integration tests + # so you can pass SKIP_SUBMODULE_DEPS + if [[ ! ${SKIP_SUBMODULE_DEPS-} ]]; then + install-deps lib/vscode + fi } main "$@" diff --git a/ci/dev/preinstall.js b/ci/dev/preinstall.js new file mode 100644 index 000000000000..9c4240d89696 --- /dev/null +++ b/ci/dev/preinstall.js @@ -0,0 +1,3 @@ +if (process.env.npm_execpath.includes("yarn")) { + throw new Error("`yarn` is no longer supported; please use `npm install` instead") +} diff --git a/ci/dev/test-e2e.sh b/ci/dev/test-e2e.sh index f42deb837552..fa748823974e 100755 --- a/ci/dev/test-e2e.sh +++ b/ci/dev/test-e2e.sh @@ -1,11 +1,23 @@ #!/usr/bin/env bash set -euo pipefail +help() { + echo >&2 " You can build with 'npm run watch' or you can build a release" + echo >&2 " For example: 'npm run build && npm run build:vscode && KEEP_MODULES=1 npm run release'" + echo >&2 " Then 'CODE_SERVER_TEST_ENTRY=./release npm run test:e2e'" + echo >&2 " You can manually run that release with 'node ./release'" +} + main() { cd "$(dirname "$0")/../.." source ./ci/lib.sh + pushd test/e2e/extensions/test-extension + echo "Building test extension" + npm run build + popd + local dir="$PWD" if [[ ! ${CODE_SERVER_TEST_ENTRY-} ]]; then echo "Set CODE_SERVER_TEST_ENTRY to test another build of code-server" @@ -21,18 +33,18 @@ main() { # wrong (native modules version issues, incomplete build, etc). if [[ ! -d $dir/out ]]; then echo >&2 "No code-server build detected" - echo >&2 "You can build it with 'yarn build' or 'yarn watch'" + help exit 1 fi - if [[ ! -d $dir/vendor/modules/code-oss-dev/out ]]; then + if [[ ! -d $dir/lib/vscode/out ]]; then echo >&2 "No VS Code build detected" - echo >&2 "You can build it with 'yarn build:vscode' or 'yarn watch'" + help exit 1 fi cd test - yarn playwright test "$@" + ./node_modules/.bin/playwright test "$@" } main "$@" diff --git a/ci/dev/test-integration.sh b/ci/dev/test-integration.sh new file mode 100755 index 000000000000..36d87de8ad6d --- /dev/null +++ b/ci/dev/test-integration.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +help() { + echo >&2 " You can build the release with 'KEEP_MODULES=1 npm run release'" + echo >&2 " Or you can pass in a custom path." + echo >&2 " CODE_SERVER_PATH='/var/tmp/coder/code-server/bin/code-server' npm run test:integration" +} + +# Make sure a code-server release works. You can pass in the path otherwise it +# will look for $RELEASE_PATH in the current directory. +# +# This is to make sure we don't have Node version errors or any other +# compilation-related errors. +main() { + cd "$(dirname "$0")/../.." + + source ./ci/lib.sh + + local path="$RELEASE_PATH/bin/code-server" + if [[ ! ${CODE_SERVER_PATH-} ]]; then + echo "Set CODE_SERVER_PATH to test another build of code-server" + else + path="$CODE_SERVER_PATH" + fi + + echo "Running tests with code-server binary: '$path'" + + if [[ ! -f $path ]]; then + echo >&2 "No code-server build detected" + echo >&2 "Looked in $path" + help + exit 1 + fi + + CODE_SERVER_PATH="$path" ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration" --testPathIgnorePatterns "./test/integration/fixtures" +} + +main "$@" diff --git a/ci/dev/test-native.sh b/ci/dev/test-native.sh new file mode 100755 index 000000000000..8e91f9a63e3b --- /dev/null +++ b/ci/dev/test-native.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +help() { + echo >&2 " You can build the release with 'KEEP_MODULES=1 npm run release'" + echo >&2 " Or you can pass in a custom path." + echo >&2 " CODE_SERVER_PATH='/var/tmp/coder/code-server/bin/code-server' npm run test:integration" +} + +# Make sure a code-server release works. You can pass in the path otherwise it +# will look for $RELEASE_PATH in the current directory. +# +# This is to make sure we don't have Node version errors or any other +# compilation-related errors. +main() { + cd "$(dirname "$0")/../.." + + source ./ci/lib.sh + + local path="$RELEASE_PATH/bin/code-server" + if [[ ! ${CODE_SERVER_PATH-} ]]; then + echo "Set CODE_SERVER_PATH to test another build of code-server" + else + path="$CODE_SERVER_PATH" + fi + + echo "Running tests with code-server binary: '$path'" + + if [[ ! -f $path ]]; then + echo >&2 "No code-server build detected" + echo >&2 "Looked in $path" + help + exit 1 + fi + + CODE_SERVER_PATH="$path" ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration/help.test.ts" +} + +main "$@" diff --git a/ci/dev/test-unit.sh b/ci/dev/test-unit.sh index 65fa94001e39..15fd2030ea59 100755 --- a/ci/dev/test-unit.sh +++ b/ci/dev/test-unit.sh @@ -3,13 +3,13 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - cd test/unit/node/test-plugin - make -s out/index.js + + source ./ci/lib.sh + # We must keep jest in a sub-directory. See ../../test/package.json for more # information. We must also run it from the root otherwise coverage will not # include our source files. - cd "$OLDPWD" - CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" + ./test/node_modules/.bin/jest "$@" --testRegex "./test/unit/.*ts" } main "$@" diff --git a/ci/dev/watch.ts b/ci/dev/watch.ts index a0c116ec28ed..e48489ce69dc 100644 --- a/ci/dev/watch.ts +++ b/ci/dev/watch.ts @@ -1,157 +1,143 @@ -import browserify from "browserify" -import * as cp from "child_process" -import * as fs from "fs" +import { spawn, ChildProcess } from "child_process" import * as path from "path" -import { onLine } from "../../src/node/util" - -async function main(): Promise { - try { - const watcher = new Watcher() - await watcher.watch() - } catch (error) { - console.error(error.message) - process.exit(1) - } +import { onLine, OnLineCallback } from "../../src/node/util" + +interface DevelopmentCompilers { + [key: string]: ChildProcess | undefined + vscode: ChildProcess + vscodeWebExtensions: ChildProcess + codeServer: ChildProcess + plugins: ChildProcess | undefined } class Watcher { - private readonly rootPath = path.resolve(__dirname, "../..") - private readonly vscodeSourcePath = path.join(this.rootPath, "vendor/modules/code-oss-dev") + private rootPath = path.resolve(process.cwd()) + private readonly paths = { + /** Path to uncompiled VS Code source. */ + vscodeDir: path.join(this.rootPath, "lib/vscode"), + pluginDir: process.env.PLUGIN_DIR, + } + + //#region Web Server + + /** Development web server. */ + private webServer: ChildProcess | undefined - private static log(message: string, skipNewline = false): void { - process.stdout.write(message) - if (!skipNewline) { - process.stdout.write("\n") + private reloadWebServer = (): void => { + if (this.webServer) { + this.webServer.kill() } + + // Pass CLI args, save for `node` and the initial script name. + const args = process.argv.slice(2) + this.webServer = spawn("node", [path.join(this.rootPath, "out/node/entry.js"), ...args]) + onLine(this.webServer, (line) => console.log("[code-server]", line)) + const { pid } = this.webServer + + this.webServer.on("exit", () => console.log("[code-server]", `Web process ${pid} exited`)) + + console.log("\n[code-server]", `Spawned web server process ${pid}`) } - public async watch(): Promise { - let server: cp.ChildProcess | undefined - const restartServer = (): void => { - if (server) { - server.kill() - } - const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(2)) - console.log(`[server] spawned process ${s.pid}`) - s.on("exit", () => console.log(`[server] process ${s.pid} exited`)) - server = s + //#endregion + + //#region Compilers + + private readonly compilers: DevelopmentCompilers = { + codeServer: spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath }), + vscode: spawn("npm", ["run", "watch"], { cwd: this.paths.vscodeDir }), + vscodeWebExtensions: spawn("npm", ["run", "watch-web"], { cwd: this.paths.vscodeDir }), + plugins: this.paths.pluginDir + ? spawn("npm", ["run", "build", "--watch"], { cwd: this.paths.pluginDir }) + : undefined, + } + + public async initialize(): Promise { + for (const event of ["SIGINT", "SIGTERM"]) { + process.on(event, () => this.dispose(0)) } - const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath }) - const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath }) - const plugin = process.env.PLUGIN_DIR - ? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR }) - : undefined - - const cleanup = (code?: number | null): void => { - Watcher.log("killing vs code watcher") - vscode.removeAllListeners() - vscode.kill() - - Watcher.log("killing tsc") - tsc.removeAllListeners() - tsc.kill() - - if (plugin) { - Watcher.log("killing plugin") - plugin.removeAllListeners() - plugin.kill() - } + for (const [processName, devProcess] of Object.entries(this.compilers)) { + if (!devProcess) continue - if (server) { - Watcher.log("killing server") - server.removeAllListeners() - server.kill() + devProcess.on("exit", (code) => { + console.log(`[${processName}]`, "Terminated unexpectedly") + this.dispose(code) + }) + + if (devProcess.stderr) { + devProcess.stderr.on("data", (d: string | Uint8Array) => process.stderr.write(d)) } + } + + onLine(this.compilers.vscode, this.parseVSCodeLine) + onLine(this.compilers.codeServer, this.parseCodeServerLine) - Watcher.log("killing watch") - process.exit(code || 0) + if (this.compilers.plugins) { + onLine(this.compilers.plugins, this.parsePluginLine) } + } - process.on("SIGINT", () => cleanup()) - process.on("SIGTERM", () => cleanup()) - - vscode.on("exit", (code) => { - Watcher.log("vs code watcher terminated unexpectedly") - cleanup(code) - }) - tsc.on("exit", (code) => { - Watcher.log("tsc terminated unexpectedly") - cleanup(code) - }) - if (plugin) { - plugin.on("exit", (code) => { - Watcher.log("plugin terminated unexpectedly") - cleanup(code) - }) + //#endregion + + //#region Line Parsers + + private parseVSCodeLine: OnLineCallback = (strippedLine, originalLine) => { + if (!strippedLine.length) return + + console.log("[Code OSS]", originalLine) + + if (strippedLine.includes("Finished compilation with")) { + console.log("[Code OSS] ✨ Finished compiling! ✨", "(Refresh your web browser ♻️)") + this.reloadWebServer() } + } - vscode.stderr.on("data", (d) => process.stderr.write(d)) - tsc.stderr.on("data", (d) => process.stderr.write(d)) - if (plugin) { - plugin.stderr.on("data", (d) => process.stderr.write(d)) + private parseCodeServerLine: OnLineCallback = (strippedLine, originalLine) => { + if (!strippedLine.length) return + + console.log("[Compiler][code-server]", originalLine) + + if (strippedLine.includes("Watching for file changes")) { + console.log("[Compiler][code-server]", "Finished compiling!", "(Refresh your web browser ♻️)") + this.reloadWebServer() } + } - const browserFiles = [ - path.join(this.rootPath, "out/browser/register.js"), - path.join(this.rootPath, "out/browser/pages/login.js"), - path.join(this.rootPath, "out/browser/pages/vscode.js"), - ] - - let startingVscode = false - let startedVscode = false - onLine(vscode, (line, original) => { - console.log("[vscode]", original) - // Wait for watch-client since "Finished compilation" will appear multiple - // times before the client starts building. - if (!startingVscode && line.includes("Starting watch-client")) { - startingVscode = true - } else if (startingVscode && line.includes("Finished compilation")) { - if (startedVscode) { - restartServer() - } - startedVscode = true - } - }) + private parsePluginLine: OnLineCallback = (strippedLine, originalLine) => { + if (!strippedLine.length) return - onLine(tsc, (line, original) => { - // tsc outputs blank lines; skip them. - if (line !== "") { - console.log("[tsc]", original) - } - if (line.includes("Watching for file changes")) { - bundleBrowserCode(browserFiles) - restartServer() - } - }) - - if (plugin) { - onLine(plugin, (line, original) => { - // tsc outputs blank lines; skip them. - if (line !== "") { - console.log("[plugin]", original) - } - if (line.includes("Watching for file changes")) { - restartServer() - } - }) + console.log("[Compiler][Plugin]", originalLine) + + if (strippedLine.includes("Watching for file changes...")) { + this.reloadWebServer() } } + + //#endregion + + //#region Utilities + + private dispose(code: number | null): void { + for (const [processName, devProcess] of Object.entries(this.compilers)) { + console.log(`[${processName}]`, "Killing...\n") + devProcess?.removeAllListeners() + devProcess?.kill() + } + process.exit(typeof code === "number" ? code : 0) + } + + //#endregion } -function bundleBrowserCode(inputFiles: string[]) { - console.log(`[browser] bundling...`) - inputFiles.forEach(async (path: string) => { - const outputPath = path.replace(".js", ".browserified.js") - browserify() - .add(path) - .bundle() - .on("error", function (error: Error) { - console.error(error.toString()) - }) - .pipe(fs.createWriteStream(outputPath)) - }) - console.log(`[browser] done bundling`) +async function main(): Promise { + try { + const watcher = new Watcher() + await watcher.initialize() + } catch (error: any) { + console.error(error.message) + process.exit(1) + } } main() diff --git a/ci/helm-chart/Chart.yaml b/ci/helm-chart/Chart.yaml index e8efd9d1c3de..99c540e33719 100644 --- a/ci/helm-chart/Chart.yaml +++ b/ci/helm-chart/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: code-server -description: A Helm chart for cdr/code-server +description: A Helm chart for coder/code-server # A chart can be either an 'application' or a 'library' chart. # @@ -15,9 +15,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.3 +version: 3.48.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 3.12.0 +appVersion: 4.133.0 diff --git a/ci/helm-chart/templates/NOTES.txt b/ci/helm-chart/templates/NOTES.txt index 17c25f646dc2..45c9aed3881d 100644 --- a/ci/helm-chart/templates/NOTES.txt +++ b/ci/helm-chart/templates/NOTES.txt @@ -15,9 +15,8 @@ export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "code-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "code-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" - kubectl port-forward $POD_NAME 8080:80 + kubectl port-forward --namespace {{ .Release.Namespace }} service/{{ include "code-server.fullname" . }} 8080:http {{- end }} Administrator credentials: diff --git a/ci/helm-chart/templates/deployment.yaml b/ci/helm-chart/templates/deployment.yaml index 201e3e6cb8c6..18bece028fc6 100644 --- a/ci/helm-chart/templates/deployment.yaml +++ b/ci/helm-chart/templates/deployment.yaml @@ -3,33 +3,41 @@ kind: Deployment metadata: name: {{ include "code-server.fullname" . }} labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - helm.sh/chart: {{ include "code-server.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- include "code-server.labels" . | nindent 4 }} + {{- if .Values.annotations }} + annotations: {{- toYaml .Values.annotations | nindent 4 }} + {{- end }} spec: - replicas: 1 + {{- if ne .Values.replicaCount nil }} + replicas: {{ .Values.replicaCount }} + {{- end }} strategy: type: Recreate selector: matchLabels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} + {{- include "code-server.selectorLabels" . | nindent 6 }} template: metadata: labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} + {{- include "code-server.selectorLabels" . | nindent 8 }} + {{- if .Values.podAnnotations }} + annotations: {{- toYaml .Values.podAnnotations | nindent 8 }} + {{- end }} spec: + imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} {{- if .Values.hostnameOverride }} hostname: {{ .Values.hostnameOverride }} {{- end }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} + {{- end }} {{- if .Values.securityContext.enabled }} securityContext: fsGroup: {{ .Values.securityContext.fsGroup }} {{- end }} - {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} + {{- if or (and .Values.volumePermissions.enabled .Values.persistence.enabled) .Values.extraInitContainers }} initContainers: + {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} - name: init-chmod-data image: busybox:latest imagePullPolicy: IfNotPresent @@ -43,6 +51,7 @@ spec: volumeMounts: - name: data mountPath: /home/coder + {{- end }} {{- if .Values.extraInitContainers }} {{ tpl .Values.extraInitContainers . | indent 6}} {{- end }} @@ -58,6 +67,17 @@ spec: securityContext: runAsUser: {{ .Values.securityContext.runAsUser }} {{- end }} + {{- if .Values.lifecycle.enabled }} + lifecycle: + {{- if .Values.lifecycle.postStart }} + postStart: + {{ toYaml .Values.lifecycle.postStart | nindent 14 }} + {{- end }} + {{- if .Values.lifecycle.preStop }} + preStop: + {{ toYaml .Values.lifecycle.preStop | nindent 14 }} + {{- end }} + {{- end }} env: {{- if .Values.extraVars }} {{ toYaml .Values.extraVars | indent 10 }} @@ -87,6 +107,7 @@ spec: {{- range .Values.extraSecretMounts }} - name: {{ .name }} mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} readOnly: {{ .readOnly }} {{- end }} {{- range .Values.extraVolumeMounts }} @@ -99,14 +120,23 @@ spec: - name: http containerPort: 8080 protocol: TCP + {{- range .Values.extraPorts }} + - name: {{ .name }} + containerPort: {{ .port }} + protocol: {{ .protocol }} + {{- end }} + {{- if ne .Values.livenessProbe.enabled false }} livenessProbe: httpGet: - path: / + path: /healthz port: http + {{- end }} + {{- if ne .Values.readinessProbe.enabled false }} readinessProbe: httpGet: - path: / + path: /healthz port: http + {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} @@ -115,7 +145,7 @@ spec: {{- end }} {{- with .Values.affinity }} affinity: - {{- toYaml . | nindent 8 }} + {{- tpl . $ | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: @@ -142,14 +172,23 @@ spec: secretName: {{ .secretName }} defaultMode: {{ .defaultMode }} {{- end }} + {{- range .Values.extraConfigmapMounts }} + - name: {{ .name }} + configMap: + name: {{ .configMap }} + defaultMode: {{ .defaultMode }} + {{- end }} {{- range .Values.extraVolumeMounts }} - name: {{ .name }} {{- if .existingClaim }} persistentVolumeClaim: claimName: {{ .existingClaim }} - {{- else }} + {{- else if .hostPath }} hostPath: path: {{ .hostPath }} type: Directory + {{- else }} + emptyDir: + {{- toYaml .emptyDir | nindent 10 }} {{- end }} {{- end }} diff --git a/ci/helm-chart/templates/ingress.yaml b/ci/helm-chart/templates/ingress.yaml index d0a552cdfd99..1da432074b29 100644 --- a/ci/helm-chart/templates/ingress.yaml +++ b/ci/helm-chart/templates/ingress.yaml @@ -18,6 +18,9 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: + {{- if .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName }} + {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} diff --git a/ci/helm-chart/templates/pvc.yaml b/ci/helm-chart/templates/pvc.yaml index 2f1c87405886..206b834e930d 100644 --- a/ci/helm-chart/templates/pvc.yaml +++ b/ci/helm-chart/templates/pvc.yaml @@ -9,10 +9,7 @@ metadata: {{ toYaml . | indent 4 }} {{- end }} labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - helm.sh/chart: {{ include "code-server.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- include "code-server.labels" . | nindent 4 }} spec: accessModes: - {{ .Values.persistence.accessMode | quote }} diff --git a/ci/helm-chart/templates/secrets.yaml b/ci/helm-chart/templates/secrets.yaml index 6c600417a516..93e75800dffd 100644 --- a/ci/helm-chart/templates/secrets.yaml +++ b/ci/helm-chart/templates/secrets.yaml @@ -1,3 +1,4 @@ +{{- if not .Values.existingSecret }} apiVersion: v1 kind: Secret metadata: @@ -5,14 +6,12 @@ metadata: annotations: "helm.sh/hook": "pre-install" labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - helm.sh/chart: {{ include "code-server.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- include "code-server.labels" . | nindent 4 }} type: Opaque data: - {{ if .Values.password }} + {{- if .Values.password }} password: "{{ .Values.password | b64enc }}" - {{ else }} + {{- else }} password: "{{ randAlphaNum 24 | b64enc }}" - {{ end }} + {{- end }} +{{- end }} diff --git a/ci/helm-chart/templates/service.yaml b/ci/helm-chart/templates/service.yaml index 038b6cd0d23f..1b58af0b1acd 100644 --- a/ci/helm-chart/templates/service.yaml +++ b/ci/helm-chart/templates/service.yaml @@ -3,10 +3,7 @@ kind: Service metadata: name: {{ include "code-server.fullname" . }} labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - helm.sh/chart: {{ include "code-server.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- include "code-server.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: @@ -14,6 +11,12 @@ spec: targetPort: http protocol: TCP name: http + {{- range .Values.extraPorts }} + - port: {{ .port }} + targetPort: {{ .port }} + protocol: {{ .protocol }} + name: {{ .name }} + {{- end }} selector: app.kubernetes.io/name: {{ include "code-server.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/ci/helm-chart/templates/serviceaccount.yaml b/ci/helm-chart/templates/serviceaccount.yaml index df9e1e37562b..2fa308fec172 100644 --- a/ci/helm-chart/templates/serviceaccount.yaml +++ b/ci/helm-chart/templates/serviceaccount.yaml @@ -3,9 +3,6 @@ apiVersion: v1 kind: ServiceAccount metadata: labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - helm.sh/chart: {{ include "code-server.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- include "code-server.labels" . | nindent 4 }} name: {{ template "code-server.serviceAccountName" . }} {{- end -}} diff --git a/ci/helm-chart/templates/tests/test-connection.yaml b/ci/helm-chart/templates/tests/test-connection.yaml index 2e67f56ec64c..dd81f8904e06 100644 --- a/ci/helm-chart/templates/tests/test-connection.yaml +++ b/ci/helm-chart/templates/tests/test-connection.yaml @@ -3,16 +3,13 @@ kind: Pod metadata: name: "{{ include "code-server.fullname" . }}-test-connection" labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - helm.sh/chart: {{ include "code-server.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- include "code-server.labels" . | nindent 4 }} annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: containers: - name: wget image: busybox command: ['wget'] - args: ['{{ include "code-server.fullname" . }}:{{ .Values.service.port }}'] + args: ['{{ include "code-server.fullname" . }}:{{ .Values.service.port }}/healthz'] restartPolicy: Never diff --git a/ci/helm-chart/values.yaml b/ci/helm-chart/values.yaml index 36a0457ec25f..9d07b928e53b 100644 --- a/ci/helm-chart/values.yaml +++ b/ci/helm-chart/values.yaml @@ -6,14 +6,22 @@ replicaCount: 1 image: repository: codercom/code-server - tag: '3.12.0' + tag: '4.133.0' pullPolicy: Always +# Specifies one or more secrets to be used when pulling images from a +# private container repository +# https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry imagePullSecrets: [] +# - name: registry-creds + nameOverride: "" fullnameOverride: "" hostnameOverride: "" +# The existing secret to use for code-server authentication in the frontend. the password is stored in the secret under the key `password` +# existingSecret: "" + serviceAccount: # Specifies whether a service account should be created create: true @@ -23,18 +31,19 @@ serviceAccount: # If not set and create is true, a name is generated using the fullname template name: "" +# Specifies annotations for deployment +annotations: {} + +# code-server.labels -- The Deployment labels. See: +# https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +labels: {} + podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 +priorityClassName: "" service: type: ClusterIP @@ -43,13 +52,12 @@ service: ingress: enabled: false #annotations: - # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" #hosts: # - host: code-server.example.loc # paths: # - / - + ingressClassName: "" #tls: # - secretName: code-server # hosts: @@ -73,7 +81,10 @@ extraArgs: [] # Optional additional environment variables extraVars: [] # - name: DISABLE_TELEMETRY -# value: true +# value: "true" +# if dind is desired: +# - name: DOCKER_HOST +# value: "tcp://localhost:2376" ## ## Init containers parameters: @@ -104,6 +115,12 @@ resources: {} # cpu: 100m # memory: 1000Mi +livenessProbe: + enabled: true + +readinessProbe: + enabled: true + nodeSelector: {} tolerations: [] @@ -127,33 +144,73 @@ persistence: # existingClaim: "" # hostPath: /data -serviceAccount: - create: true - name: +lifecycle: + enabled: false + # postStart: + # exec: + # command: + # - /bin/bash + # - -c + # - curl -s -L SOME_SCRIPT | bash + + # for dind, the following may be helpful + # postStart: + # exec: + # command: + # - /bin/sh + # - -c + # - | + # sudo apt-get update \ + # && sudo apt-get install -y docker.io ## Enable an Specify container in extraContainers. ## This is meant to allow adding code-server dependencies, like docker-dind. extraContainers: | -#- name: docker-dind -# image: docker:19.03-dind -# imagePullPolicy: IfNotPresent -# resources: -# requests: -# cpu: 250m -# memory: 256M -# securityContext: -# privileged: true -# procMount: Default -# env: -# - name: DOCKER_TLS_CERTDIR -# value: "" -# - name: DOCKER_DRIVER -# value: "overlay2" +# If docker-dind is used, DOCKER_HOST env is mandatory to set in "extraVars" +# - name: docker-dind +# image: docker:28.3.2-dind +# imagePullPolicy: IfNotPresent +# resources: +# requests: +# cpu: 1 +# ephemeral-storage: "50Gi" +# memory: 10Gi +# securityContext: +# privileged: true +# procMount: Default +# env: +# - name: DOCKER_TLS_CERTDIR +# value: "" # disable TLS setup +# command: +# - dockerd +# - --host=unix:///var/run/docker.sock +# - --host=tcp://0.0.0.0:2376 + + +extraInitContainers: | +# - name: customization +# image: {{ .Values.image.repository }}:{{ .Values.image.tag }} +# imagePullPolicy: IfNotPresent +# env: +# - name: SERVICE_URL +# value: https://open-vsx.org/vscode/gallery +# - name: ITEM_URL +# value: https://open-vsx.org/vscode/item +# command: +# - sh +# - -c +# - | +# code-server --install-extension ms-python.python +# code-server --install-extension golang.Go +# volumeMounts: +# - name: data +# mountPath: /home/coder ## Additional code-server secret mounts extraSecretMounts: [] # - name: secret-files # mountPath: /etc/secrets + # subPath: private.key # (optional) # secretName: code-server-secret-files # readOnly: true @@ -164,6 +221,7 @@ extraVolumeMounts: [] # readOnly: true # existingClaim: volume-claim # hostPath: "" + # emptyDir: {} extraConfigmapMounts: [] # - name: certs-configmap @@ -171,3 +229,8 @@ extraConfigmapMounts: [] # subPath: certificates.crt # (optional) # configMap: certs-configmap # readOnly: true + +extraPorts: [] + # - name: minecraft + # port: 25565 + # protocol: tcp diff --git a/ci/lib.sh b/ci/lib.sh index 0e357986153f..7c1f0d9e5914 100755 --- a/ci/lib.sh +++ b/ci/lib.sh @@ -9,116 +9,116 @@ popd() { builtin popd > /dev/null } -pkg_json_version() { - jq -r .version package.json -} - vscode_version() { - jq -r .version vendor/modules/code-oss-dev/package.json + jq -r .version lib/vscode/package.json } os() { - local os - os=$(uname | tr '[:upper:]' '[:lower:]') - if [[ $os == "linux" ]]; then - # Alpine's ldd doesn't have a version flag but if you use an invalid flag - # (like --version) it outputs the version to stderr and exits with 1. - local ldd_output - ldd_output=$(ldd --version 2>&1 || true) - if echo "$ldd_output" | grep -iq musl; then - os="alpine" - fi - elif [[ $os == "darwin" ]]; then - os="macos" - fi - echo "$os" + osname=$(uname | tr '[:upper:]' '[:lower:]') + case $osname in + linux) + # Alpine's ldd doesn't have a version flag but if you use an invalid flag + # (like --version) it outputs the version to stderr and exits with 1. + # TODO: Better to check /etc/os-release; see ../install.sh. + ldd_output=$(ldd --version 2>&1 || true) + if echo "$ldd_output" | grep -iq musl; then + osname="alpine" + fi + ;; + darwin) osname="macos" ;; + cygwin* | mingw*) osname="windows" ;; + esac + echo "$osname" } arch() { cpu="$(uname -m)" case "$cpu" in - aarch64) - echo arm64 - ;; - x86_64 | amd64) - echo amd64 - ;; - *) - echo "$cpu" - ;; + aarch64) cpu=arm64 ;; + x86_64) cpu=amd64 ;; esac + echo "$cpu" } -# Grabs the most recent ci.yaml github workflow run that was triggered from the -# pull request of the release branch for this version (regardless of whether -# that run succeeded or failed). The release branch name must be in semver -# format with a v prepended. -# This will contain the artifacts we want. -# https://developer.github.com/v3/actions/workflow-runs/#list-workflow-runs -get_artifacts_url() { - local artifacts_url - local version_branch="v$VERSION" - local workflow_runs_url="repos/:owner/:repo/actions/workflows/ci.yaml/runs?event=pull_request&branch=$version_branch" - artifacts_url=$(gh api "$workflow_runs_url" | jq -r ".workflow_runs[] | select(.head_branch == \"$version_branch\") | .artifacts_url" | head -n 1) - if [[ -z "$artifacts_url" ]]; then - echo >&2 "ERROR: artifacts_url came back empty" - echo >&2 "We looked for a successful run triggered by a pull_request with for code-server version: $VERSION and a branch named $version_branch" - echo >&2 "URL used for gh API call: $workflow_runs_url" - exit 1 - fi - - echo "$artifacts_url" -} - -# Grabs the artifact's download url. -# https://developer.github.com/v3/actions/artifacts/#list-workflow-run-artifacts -get_artifact_url() { - local artifact_name="$1" - gh api "$(get_artifacts_url)" | jq -r ".artifacts[] | select(.name == \"$artifact_name\") | .archive_download_url" | head -n 1 +rsync() { + command rsync -a --del "$@" } -# Uses the above two functions to download a artifact into a directory. -download_artifact() { - local artifact_name="$1" - local dst="$2" +if [[ ! ${ARCH-} ]]; then + ARCH=$(arch) + export ARCH +fi - local tmp_file - tmp_file="$(mktemp)" +if [[ ! ${OS-} ]]; then + OS=$(os) + export OS +fi - gh api "$(get_artifact_url "$artifact_name")" > "$tmp_file" - unzip -q -o "$tmp_file" -d "$dst" - rm "$tmp_file" +# RELEASE_PATH is the destination directory for the release from the root. +# Defaults to release +if [[ ! ${RELEASE_PATH-} ]]; then + RELEASE_PATH="release" + export RELEASE_PATH +fi + +nodeOS() { + osname=$OS + case $osname in + macos) osname=darwin ;; + windows) osname=win32 ;; + esac + echo "$osname" } -rsync() { - command rsync -a --del "$@" +nodeArch() { + cpu=$ARCH + case $cpu in + amd64) cpu=x64 ;; + esac + echo "$cpu" } -VERSION="$(pkg_json_version)" -export VERSION -ARCH="$(arch)" -export ARCH -OS=$(os) -export OS +run-steps() { + local -i failed=0 + mkdir -p .cache + rm -f .cache/checklist + while (( $# )) ; do + local name=$1 ; shift + local fn=$1 ; shift + echo "$name..." + # Only run if an earlier step has not failed. + # For all failed steps, write out an empty checkbox. + if [[ $failed == 0 ]] ; then + if $fn | indent ; then + echo "- [X] $name" >> .cache/checklist + else + ((failed++)) + echo "- [ ] $name" >> .cache/checklist + echo "Failed" | indent + fi + else + echo "- [ ] $name" >> .cache/checklist + echo "Skipped" | indent + fi + done + if [[ $failed != 0 ]] ; then + return 1 + fi +} -# RELEASE_PATH is the destination directory for the release from the root. -# Defaults to release -RELEASE_PATH="${RELEASE_PATH-release}" +quiet() { + "$@" >/dev/null +} -# VS Code bundles some modules into an asar which is an archive format that -# works like tar. It then seems to get unpacked into node_modules.asar. -# -# I don't know why they do this but all the dependencies they bundle already -# exist in node_modules so just symlink it. We have to do this since not only VS -# Code itself but also extensions will look specifically in this directory for -# files (like the ripgrep binary or the oniguruma wasm). -symlink_asar() { - rm -rf node_modules.asar - if [ "${WINDIR-}" ]; then - # mklink takes the link name first. - mklink /J node_modules.asar node_modules - else - # ln takes the link name second. - ln -s node_modules node_modules.asar - fi +indent() { + local count=2 + local space + space=$(printf "%${count}s") + sed "s/^/$space| /g" } + +# See gulpfile.reh.ts for available targets. +if [[ ! ${VSCODE_TARGET-} ]]; then + VSCODE_TARGET="$(nodeOS)-$(nodeArch)" + export VSCODE_TARGET +fi diff --git a/ci/release-image/Dockerfile b/ci/release-image/Dockerfile index cbd7f5d697bb..4c91e291e0e8 100644 --- a/ci/release-image/Dockerfile +++ b/ci/release-image/Dockerfile @@ -1,20 +1,29 @@ -FROM debian:11 +# syntax=docker/dockerfile:experimental + +ARG BASE=debian:13 +FROM scratch AS packages +COPY release-packages/code-server*.deb /tmp/ + +FROM $BASE RUN apt-get update \ - && apt-get install -y \ + && apt-get install -y \ curl \ dumb-init \ - zsh \ + git \ + git-lfs \ htop \ locales \ - man \ + lsb-release \ + man-db \ nano \ - git \ - procps \ openssh-client \ + procps \ sudo \ - vim.tiny \ - lsb-release \ + vim-tiny \ + wget \ + zsh \ + && git lfs install \ && rm -rf /var/lib/apt/lists/* # https://wiki.debian.org/Locale#Manually @@ -22,19 +31,25 @@ RUN sed -i "s/# en_US.UTF-8/en_US.UTF-8/" /etc/locale.gen \ && locale-gen ENV LANG=en_US.UTF-8 -RUN adduser --gecos '' --disabled-password coder && \ - echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd +RUN if grep -q 1000 /etc/passwd; then \ + userdel -r "$(id -un 1000)"; \ + fi \ + && adduser --gecos '' --disabled-password coder \ + && echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd -RUN ARCH="$(dpkg --print-architecture)" && \ - curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.5/fixuid-0.5-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - && \ - chown root:root /usr/local/bin/fixuid && \ - chmod 4755 /usr/local/bin/fixuid && \ - mkdir -p /etc/fixuid && \ - printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml +RUN ARCH="$(dpkg --print-architecture)" \ + && curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - \ + && chown root:root /usr/local/bin/fixuid \ + && chmod 4755 /usr/local/bin/fixuid \ + && mkdir -p /etc/fixuid \ + && printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml -COPY release-packages/code-server*.deb /tmp/ COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh -RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb +RUN --mount=from=packages,src=/tmp,dst=/tmp/packages dpkg -i /tmp/packages/code-server*$(dpkg --print-architecture).deb + +# Allow users to have scripts run on container startup to prepare workspace. +# https://github.com/coder/code-server/issues/5177 +ENV ENTRYPOINTD=${HOME}/entrypoint.d EXPOSE 8080 # This way, if someone sets $DOCKER_USER, docker-exec will still work as diff --git a/ci/release-image/Dockerfile.fedora b/ci/release-image/Dockerfile.fedora new file mode 100644 index 000000000000..ec618530cb78 --- /dev/null +++ b/ci/release-image/Dockerfile.fedora @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:experimental + +ARG BASE=fedora:39 +FROM scratch AS packages +COPY release-packages/code-server*.rpm /tmp/ + +FROM $BASE + +RUN dnf update -y \ + && dnf install -y \ + curl \ + git \ + git-lfs \ + htop \ + nano \ + openssh-clients \ + procps \ + wget \ + zsh \ + dumb-init \ + glibc-langpack-en \ + && rm -rf /var/cache/dnf +RUN git lfs install + +ENV LANG=en_US.UTF-8 +RUN echo 'LANG="en_US.UTF-8"' > /etc/locale.conf + +RUN useradd -u 1000 coder && echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd + +RUN ARCH="$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g')" \ + && curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - \ + && chown root:root /usr/local/bin/fixuid \ + && chmod 4755 /usr/local/bin/fixuid \ + && mkdir -p /etc/fixuid \ + && printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml + +COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh +RUN --mount=from=packages,src=/tmp,dst=/tmp/packages rpm -i /tmp/packages/code-server*$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g').rpm + +# Allow users to have scripts run on container startup to prepare workspace. +# https://github.com/coder/code-server/issues/5177 +ENV ENTRYPOINTD=${HOME}/entrypoint.d + +EXPOSE 8080 +# This way, if someone sets $DOCKER_USER, docker-exec will still work as +# the uid will remain the same. note: only relevant if -u isn't passed to +# docker-run. +USER 1000 +ENV USER=coder +WORKDIR /home/coder +ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."] diff --git a/ci/release-image/Dockerfile.opensuse b/ci/release-image/Dockerfile.opensuse new file mode 100644 index 000000000000..f445d45c27b1 --- /dev/null +++ b/ci/release-image/Dockerfile.opensuse @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:experimental + +ARG BASE=opensuse/tumbleweed +FROM scratch AS packages +COPY release-packages/code-server*.rpm /tmp/ + +FROM $BASE + +RUN zypper dup -y \ + && zypper in -y \ + curl \ + git \ + git-lfs \ + htop \ + nano \ + openssh-clients \ + procps \ + wget \ + zsh \ + sudo \ + catatonit \ + && rm -rf /var/cache/zypp /var/cache/zypper +RUN git lfs install + +ENV LANG=en_US.UTF-8 +RUN echo 'LANG="en_US.UTF-8"' > /etc/locale.conf + +RUN useradd -u 1000 coder && echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd + +RUN ARCH="$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g')" \ + && curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - \ + && chown root:root /usr/local/bin/fixuid \ + && chmod 4755 /usr/local/bin/fixuid \ + && mkdir -p /etc/fixuid \ + && printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml + +COPY ci/release-image/entrypoint-catatonit.sh /usr/bin/entrypoint-catatonit.sh +RUN --mount=from=packages,src=/tmp,dst=/tmp/packages rpm -i /tmp/packages/code-server*$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g').rpm + +# Allow users to have scripts run on container startup to prepare workspace. +# https://github.com/coder/code-server/issues/5177 +ENV ENTRYPOINTD=${HOME}/entrypoint.d + +EXPOSE 8080 +# This way, if someone sets $DOCKER_USER, docker-exec will still work as +# the uid will remain the same. note: only relevant if -u isn't passed to +# docker-run. +USER 1000 +ENV USER=coder +WORKDIR /home/coder +ENTRYPOINT ["/usr/bin/entrypoint-catatonit.sh", "--bind-addr", "0.0.0.0:8080", "."] diff --git a/ci/release-image/docker-bake.hcl b/ci/release-image/docker-bake.hcl index cb55da69c1fd..ecb0c313daed 100644 --- a/ci/release-image/docker-bake.hcl +++ b/ci/release-image/docker-bake.hcl @@ -6,15 +6,125 @@ variable "VERSION" { default = "latest" } +variable "DOCKER_REGISTRY" { + default = "docker.io/codercom/code-server" +} + +variable "GITHUB_REGISTRY" { + default = "ghcr.io/coder/code-server" +} + group "default" { - targets = ["code-server"] + targets = [ + "code-server-debian-13", + "code-server-debian-12", + "code-server-ubuntu-focal", + "code-server-ubuntu-noble", + "code-server-ubuntu-resolute", + "code-server-fedora-39", + "code-server-opensuse-tumbleweed", + ] +} + +function "prepend_hyphen_if_not_null" { + params = [tag] + result = notequal("","${tag}") ? "-${tag}" : "${tag}" +} + +# use empty tag (tag="") to generate default tags +function "gen_tags" { + params = [registry, tag] + result = notequal("","${registry}") ? [ + notequal("", "${tag}") ? "${registry}:${tag}" : "${registry}:latest", + notequal("latest",VERSION) ? "${registry}:${VERSION}${prepend_hyphen_if_not_null(tag)}" : "", + ] : [] } -target "code-server" { +# helper function to generate tags for docker registry and github registry. +# set (DOCKER|GITHUB)_REGISTRY="" to disable corresponding registry +function "gen_tags_for_docker_and_ghcr" { + params = [tag] + result = concat( + gen_tags("${DOCKER_REGISTRY}", "${tag}"), + gen_tags("${GITHUB_REGISTRY}", "${tag}"), + ) +} + +target "code-server-debian-13" { dockerfile = "ci/release-image/Dockerfile" - tags = [ - "docker.io/codercom/code-server:latest", - notequal("latest",VERSION) ? "docker.io/codercom/code-server:${VERSION}" : "", - ] + tags = concat( + gen_tags_for_docker_and_ghcr(""), + gen_tags_for_docker_and_ghcr("debian"), + gen_tags_for_docker_and_ghcr("trixie"), + ) + platforms = ["linux/amd64", "linux/arm64"] +} + +target "code-server-debian-12" { + dockerfile = "ci/release-image/Dockerfile" + tags = concat( + gen_tags_for_docker_and_ghcr("bookworm"), + ) + args = { + BASE = "debian:12" + } + platforms = ["linux/amd64", "linux/arm64"] +} + +target "code-server-ubuntu-focal" { + dockerfile = "ci/release-image/Dockerfile" + tags = concat( + gen_tags_for_docker_and_ghcr("focal"), + ) + args = { + BASE = "ubuntu:focal" + } + platforms = ["linux/amd64", "linux/arm64"] +} + +target "code-server-ubuntu-noble" { + dockerfile = "ci/release-image/Dockerfile" + tags = concat( + gen_tags_for_docker_and_ghcr("noble"), + gen_tags_for_docker_and_ghcr("ubuntu"), + ) + args = { + BASE = "ubuntu:noble" + } + platforms = ["linux/amd64", "linux/arm64"] +} + +target "code-server-ubuntu-resolute" { + dockerfile = "ci/release-image/Dockerfile" + tags = concat( + gen_tags_for_docker_and_ghcr("resolute"), + ) + args = { + BASE = "ubuntu:resolute" + } + platforms = ["linux/amd64", "linux/arm64"] +} + +target "code-server-fedora-39" { + dockerfile = "ci/release-image/Dockerfile.fedora" + tags = concat( + gen_tags_for_docker_and_ghcr("fedora"), + gen_tags_for_docker_and_ghcr("39"), + ) + args = { + BASE = "fedora:39" + } + platforms = ["linux/amd64", "linux/arm64"] +} + +target "code-server-opensuse-tumbleweed" { + dockerfile = "ci/release-image/Dockerfile.opensuse" + tags = concat( + gen_tags_for_docker_and_ghcr("opensuse"), + gen_tags_for_docker_and_ghcr("tumbleweed"), + ) + args = { + BASE = "opensuse/tumbleweed" + } platforms = ["linux/amd64", "linux/arm64"] } diff --git a/ci/release-image/entrypoint-catatonit.sh b/ci/release-image/entrypoint-catatonit.sh new file mode 100755 index 000000000000..d22acc6d237b --- /dev/null +++ b/ci/release-image/entrypoint-catatonit.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu + +# We do this first to ensure sudo works below when renaming the user. +# Otherwise the current container UID may not exist in the passwd database. +eval "$(fixuid -q)" + +if [ "${DOCKER_USER-}" ]; then + USER="$DOCKER_USER" + if [ "$DOCKER_USER" != "$(whoami)" ]; then + echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null + # Unfortunately we cannot change $HOME as we cannot move any bind mounts + # nor can we bind mount $HOME into a new home as that requires a privileged container. + sudo usermod --login "$DOCKER_USER" coder + sudo groupmod -n "$DOCKER_USER" coder + + sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd + fi +fi + +# Allow users to have scripts run on container startup to prepare workspace. +# https://github.com/coder/code-server/issues/5177 +if [ -d "${ENTRYPOINTD}" ]; then + find "${ENTRYPOINTD}" -type f -executable -print -exec {} \; +fi + +exec catatonit -- /usr/bin/code-server "$@" diff --git a/ci/release-image/entrypoint.sh b/ci/release-image/entrypoint.sh index 770232d7e30f..efe2f39d9bd9 100755 --- a/ci/release-image/entrypoint.sh +++ b/ci/release-image/entrypoint.sh @@ -7,7 +7,7 @@ eval "$(fixuid -q)" if [ "${DOCKER_USER-}" ]; then USER="$DOCKER_USER" - if [ "$DOCKER_USER" != "$(whoami)" ]; then + if [ -z "$(id -u "$DOCKER_USER" 2>/dev/null)" ]; then echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null # Unfortunately we cannot change $HOME as we cannot move any bind mounts # nor can we bind mount $HOME into a new home as that requires a privileged container. @@ -18,4 +18,10 @@ if [ "${DOCKER_USER-}" ]; then fi fi -dumb-init /usr/bin/code-server "$@" +# Allow users to have scripts run on container startup to prepare workspace. +# https://github.com/coder/code-server/issues/5177 +if [ -d "${ENTRYPOINTD}" ]; then + find "${ENTRYPOINTD}" -type f -executable -print -exec {} \; +fi + +exec dumb-init /usr/bin/code-server "$@" diff --git a/ci/steps/brew-bump.sh b/ci/steps/brew-bump.sh deleted file mode 100755 index f3f9be7c8250..000000000000 --- a/ci/steps/brew-bump.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/../.." - # Only sourcing this so we get access to $VERSION - source ./ci/lib.sh - - # NOTE: we need to make sure cdrci/homebrew-core - # is up-to-date - # otherwise, brew bump-formula-pr will use an - # outdated base - echo "Cloning cdrci/homebrew-core" - git clone https://github.com/cdrci/homebrew-core.git - - echo "Changing into homebrew-core directory" - cd homebrew-core && pwd - - echo "Adding Homebrew/homebrew-core as $(upstream)" - git remote add upstream https://github.com/Homebrew/homebrew-core.git - - echo "Fetching upstream Homebrew/hombrew-core commits" - git fetch upstream - - echo "Merging in latest Homebrew/homebrew-core changes" - git merge upstream/master - - echo "Pushing changes to cdrci/homebrew-core fork on GitHub" - # Source: https://serverfault.com/a/912788 - # shellcheck disable=SC2016,SC2028 - echo '#!/bin/sh\nexec echo "$HOMEBREW_GITHUB_API_TOKEN"' > "$HOME"/.git-askpass.sh - # Ensure it's executable since we just created it - chmod +x "$HOME/.git-askpass.sh" - # GIT_ASKPASS lets us use the password when pushing without revealing it in the process list - # See: https://serverfault.com/a/912788 - GIT_ASKPASS="$HOME/.git-askpass.sh" git push https://cdr-oss@github.com/cdr-oss/homebrew-core.git --all - - # Find the docs for bump-formula-pr here - # https://github.com/Homebrew/brew/blob/master/Library/Homebrew/dev-cmd/bump-formula-pr.rb#L18 - local output - if ! output=$(brew bump-formula-pr --version="${VERSION}" code-server --no-browse --no-audit 2>&1); then - if [[ $output == *"Duplicate PRs should not be opened"* ]]; then - echo "$VERSION is already submitted" - else - echo "$output" - exit 1 - fi - fi - - # Clean up and remove homebrew-core - cd .. - rm -rf homebrew-core -} - -main "$@" diff --git a/ci/steps/docker-buildx-push.sh b/ci/steps/docker-buildx-push.sh index 79b205580291..6314063ffa54 100755 --- a/ci/steps/docker-buildx-push.sh +++ b/ci/steps/docker-buildx-push.sh @@ -1,36 +1,14 @@ #!/usr/bin/env bash set -euo pipefail -# See if this version already exists on Docker Hub. -function version_exists() { - local output - output=$(curl --silent "https://index.docker.io/v1/repositories/codercom/code-server/tags/$VERSION") - if [[ $output == "Tag not found" ]]; then - return 1 - else - return 0 - fi -} - main() { cd "$(dirname "$0")/../.." + # NOTE@jsjoeio - this script assumes VERSION exists as an + # environment variable. - # ci/lib.sh sets VERSION and provides download_artifact here - source ./ci/lib.sh - - if version_exists; then - echo "$VERSION is already pushed" - return - fi - - # Download the release-packages artifact - download_artifact release-packages ./release-packages - - # Login to Docker - if [[ ${CI-} ]]; then - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - fi - + # NOTE@jsjoeio - this script assumes that you've downloaded + # the release-packages artifact to ./release-packages before + # running this docker buildx step docker buildx bake -f ci/release-image/docker-bake.hcl --push } diff --git a/ci/steps/publish-npm.sh b/ci/steps/publish-npm.sh deleted file mode 100755 index 0b3d8bd02c3a..000000000000 --- a/ci/steps/publish-npm.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/../.." - source ./ci/lib.sh - - # npm view won't exit with non-zero so we have to check the output. - local hasVersion - hasVersion=$(npm view "code-server@$VERSION" version) - if [[ $hasVersion == "$VERSION" ]]; then - echo "$VERSION is already published" - return - fi - - if [[ ${CI-} ]]; then - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc - fi - - download_artifact npm-package ./release-npm-package - # https://github.com/actions/upload-artifact/issues/38 - tar -xzf release-npm-package/package.tar.gz - - # Ignore symlink when publishing npm package - # See: https://github.com/cdr/code-server/pull/3935 - echo "node_modules.asar" > release/.npmignore - yarn publish --non-interactive release -} - -main "$@" diff --git a/ci/steps/steps-lib.sh b/ci/steps/steps-lib.sh new file mode 100755 index 000000000000..e71378e27f6c --- /dev/null +++ b/ci/steps/steps-lib.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# This is a library which contains functions used inside ci/steps +# +# We separated it into it's own file so that we could easily unit test +# these functions and helpers + +# Checks whether and environment variable is set. +# Source: https://stackoverflow.com/a/62210688/3015595 +is_env_var_set() { + local name="${1:-}" + if test -n "${!name:-}"; then + return 0 + else + return 1 + fi +} + +# Checks whether a directory exists. +directory_exists() { + local dir="${1:-}" + if [[ -d "${dir:-}" ]]; then + return 0 + else + return 1 + fi +} + +# Checks whether a file exists. +file_exists() { + local file="${1:-}" + if test -f "${file:-}"; then + return 0 + else + return 1 + fi +} + +# Checks whether a file is executable. +is_executable() { + local file="${1:-}" + if [ -f "${file}" ] && [ -r "${file}" ] && [ -x "${file}" ]; then + return 0 + else + return 1 + fi +} diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md index fe14b3a64000..168eac481bb7 100644 --- a/docs/CODE_OF_CONDUCT.md +++ b/docs/CODE_OF_CONDUCT.md @@ -1,5 +1,18 @@ + +# Contributor Covenant Code of Conduct + +- [Contributor Covenant Code of Conduct](#contributor-covenant-code-of-conduct) + - [Our Pledge](#our-pledge) + - [Our Standards](#our-standards) + - [Our Responsibilities](#our-responsibilities) + - [Scope](#scope) + - [Enforcement](#enforcement) + - [Attribution](#attribution) + + + # Contributor Covenant Code of Conduct diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 684bc16f26f4..d857b20c3850 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,38 +1,42 @@ + # Contributing - [Requirements](#requirements) -- [Creating pull requests](#creating-pull-requests) - - [Commits and commit history](#commits-and-commit-history) + - [Linux-specific requirements](#linux-specific-requirements) - [Development workflow](#development-workflow) - - [Updates to VS Code](#updates-to-vs-code) + - [Version updates to Code](#version-updates-to-code) + - [Patching Code](#patching-code) - [Build](#build) - - [Test](#test) + - [Troubleshooting](#troubleshooting) + - [I see "Forbidden access" when I load code-server in the browser](#i-see-forbidden-access-when-i-load-code-server-in-the-browser) + - ["Can only have one anonymous define call per script"](#can-only-have-one-anonymous-define-call-per-script) + - [Help](#help) +- [Test](#test) - [Unit tests](#unit-tests) + - [Script tests](#script-tests) - [Integration tests](#integration-tests) - [End-to-end tests](#end-to-end-tests) - [Structure](#structure) - - [Modifications to VS Code](#modifications-to-vs-code) + - [Modifications to Code](#modifications-to-code) - [Currently Known Issues](#currently-known-issues) - -- [Detailed CI and build process docs](../ci) + ## Requirements The prerequisites for contributing to code-server are almost the same as those -for [VS -Code](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites). +for [VS Code](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites). Here is what is needed: -- `node` v14.x +- `node` v24.x - `git` v2.x or greater - [`git-lfs`](https://git-lfs.github.com) -- [`yarn`](https://classic.yarnpkg.com/en/) +- [`npm`](https://www.npmjs.com/) - Used to install JS packages and run scripts -- [`nfpm`](https://classic.yarnpkg.com/en/) +- [`nfpm`](https://nfpm.goreleaser.com/) - Used to build `.deb` and `.rpm` packages - [`jq`](https://stedolan.github.io/jq/) - Used to build code-server releases @@ -41,101 +45,156 @@ Here is what is needed: signature verification](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification) or follow [this tutorial](https://joeprevite.com/verify-commits-on-github) -- `build-essential` (Linux only - used by VS Code) - - Get this by running `apt-get install -y build-essential` +- `quilt` + - Used to manage patches to Code - `rsync` and `unzip` - Used for code-server releases - `bats` - Used to run script unit tests -## Creating pull requests - -Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) that -includes context for issues that you see. You can skip this if the proposed fix -is minor. - -In your pull requests (PR), link to the issue that the PR solves. - -Please ensure that the base of your PR is the **main** branch. - -### Commits and commit history +### Linux-specific requirements -We prefer a clean commit history. This means you should squash all fixups and -fixup-type commits before asking for a review (e.g., clean up, squash, then force -push). If you need help with this, feel free to leave a comment in your PR, and -we'll guide you. - -## Development workflow +If you're developing code-server on Linux, make sure you have installed or +install the following dependencies: ```shell -yarn -yarn watch -# Visit http://localhost:8080 once the build is completed. +sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3 ``` -`yarn watch` will live reload changes to the source. +These are required by Code. See [their Wiki](https://github.com/microsoft/vscode/wiki/How-to-Contribute#prerequisites) +for more information. -### Updates to VS Code +## Development workflow -1. Update the package tag listed in `vendor/package.json`: +1. `git clone https://github.com/coder/code-server.git` - Clone `code-server` +2. `git submodule update --init` - Clone `vscode` submodule +3. `quilt push -a` - Apply patches to the `vscode` submodule. +4. `npm install` - Install dependencies +5. `npm run watch` - Launch code-server localhost:8080. code-server will be live + reloaded when changes are made; the browser needs to be refreshed manually. + +When pulling down changes that include modifications to the patches you will +need to apply them with `quilt`. If you pull down changes that update the +`vscode` submodule you will need to run `git submodule update --init` and +re-apply the patches. + +When you make a change that affects people deploying the marketplace please +update the changelog as part of your PR. + +Note that building code-server takes a very, very long time, and loading it in +the browser in development mode also takes a very, very long time. + +Display language (Spanish, etc) support only works in a full build; it will not +work in development mode. + +Generally we prefer that PRs be squashed into `main` but you can rebase or merge +if it is important to keep the individual commits (make sure to clean up the +commits first if you are doing this). + +### Version updates to Code + +PRs will be automatically created with updates to VS Code. If a patch cannot be +automatically resolved, it will be necessary to clone the branch, resolve the +conflicts manually, and finish the update. To do this: + +1. Apply as many patches as possible `quilt push -a`. +2. Once you hit a conflict, force apply with `quilt push -f`, manually add back + the rejected code, then run `quilt refresh`. +3. Once all patches have been resolved, run `./ci/build/update.sh` to finish the + update process. +4. Commit all changes, push them up to the branch, and update the checklist in + the PR description. + +Once the PR is ready, manually verify that the unreleased changelog section +contains all the changes going into this version before merging. + +### Patching Code + +1. You can go through the patch stack with `quilt push` and `quilt pop`. +2. Create a new patch (`quilt new {name}.diff`) or use an existing patch. +3. Add the file(s) you are patching (`quilt add [-P patch] {file}`). A file + **must** be added before you make changes to it. +4. Make your changes. Patches do not need to be independent of each other but + each patch must result in a working code-server without any broken in-between + states otherwise they are difficult to test and modify. +5. Add your changes to the patch (`quilt refresh`) +6. Add a comment in the patch about the reason for the patch and how to + reproduce the behavior it fixes or adds. Every patch should have an e2e test + as well. -```json -{ - "devDependencies": { - "vscode": "cdr/vscode#X.XX.X-code-server" - } -} -``` +### Build -2. From the code-server **project root**, run `yarn install`. - Then, test code-server locally to make sure everything works. -1. Check the Node.js version that's used by Electron (which is shipped with VS - Code. If necessary, update your version of Node.js to match. -1. Open a PR +You can build a full production release as follows: -> Watch for updates to -> `vendor/modules/code-oss-dev/src/vs/code/browser/workbench/workbench.html`. You may need to -> make changes to `src/browser/pages/vscode.html`. +```shell +git submodule update --init +quilt push -a +npm install +npm run build +VERSION=0.0.0 npm run build:vscode +KEEP_MODULES=1 npm run release +``` -### Build +You can omit `KEEP_MODULES` if you intend to use this in a platform-agnostic way +(like for publishing to NPM), but since the VS Code build process does +post-processing deletion of the modules, it is recommended to keep the modules +when possible, since if you install them later you will have more than is +required. `KEEP_MODULES` will also bundle Node and the code-server entry script. -You can build as follows: +Run your build: ```shell -yarn build -yarn build:vscode -yarn release +./release/bin/code-server ``` -Run your build: +Or if you omitted `KEEP_MODULES`: ```shell cd release -yarn --production -# Runs the built JavaScript with Node. +npm install --omit=dev node . ``` -Build the release packages (make sure that you run `yarn release` first): +Then, to package the release: ```shell -yarn release:standalone -yarn test:standalone-release -yarn package +npm run package ``` > On Linux, the currently running distro will become the minimum supported -> version. In our GitHub Actions CI, we use CentOS 7 for maximum compatibility. +> version. In our GitHub Actions CI, we use CentOS 8 for maximum compatibility. > If you need your builds to support older distros, run the build commands > inside a Docker container with all the build requirements installed. -### Test +### Troubleshooting + +#### I see "Forbidden access" when I load code-server in the browser + +This means your patches didn't apply correctly. We have a patch to remove the +auth from vanilla Code because we use our own. -There are three kinds of tests in code-server: +Try popping off the patches with `quilt pop -a` and reapplying with `quilt push +-a`. + +#### "Can only have one anonymous define call per script" + +Code might be trying to use a dev or prod HTML in the wrong context. You can try +re-running code-server and setting `VSCODE_DEV=1`. + +### Help + +If you get stuck or need help, you can always start a new GitHub Discussion +[here](https://github.com/coder/code-server/discussions). One of the maintainers +will respond and help you out. + +## Test + +There are four kinds of tests in code-server: 1. Unit tests -2. Integration tests -3. End-to-end tests +2. Script tests +3. Integration tests +4. End-to-end tests ### Unit tests @@ -144,13 +203,24 @@ Our unit tests are written in TypeScript and run using These live under [test/unit](../test/unit). -We use unit tests for functions and things that can be tested in isolation. The file structure is modeled closely after `/src` so it's easy for people to know where test files should live. +We use unit tests for functions and things that can be tested in isolation. The +file structure is modeled closely after `/src` so it's easy for people to know +where test files should live. + +### Script tests + +Our script tests are written in bash and run using [bats](https://github.com/bats-core/bats-core). + +These tests live under `test/scripts`. + +We use these to test anything related to our scripts (most of which live under +`ci`). ### Integration tests -These are a work in progress. We build code-server and run a script called -[test-standalone-release.sh](../ci/build/test-standalone-release.sh), which -ensures that code-server's CLI is working. +These are a work in progress. We build code-server and run tests with `npm run +test:integration`, which ensures that code-server builds work on their +respective platforms. Our integration tests look at components that rely on one another. For example, testing the CLI requires us to build and package code-server. @@ -171,106 +241,48 @@ Take a look at `codeServer.test.ts` to see how you would use it (see We also have a model where you can create helpers to use within tests. See [models/CodeServer.ts](../test/e2e/models/CodeServer.ts) for an example. -Generally speaking, e2e means testing code-server while running in the browser -and interacting with it in a way that's similar to how a user would interact -with it. When running these tests with `yarn test:e2e`, you must have -code-server running locally. In CI, this is taken care of for you. - ## Structure -The `code-server` script serves as an HTTP API for login and starting a remote VS -Code process. +code-server essentially serves as an HTTP API for logging in and starting a +remote Code process. The CLI code is in [src/node](../src/node) and the HTTP routes are implemented in [src/node/routes](../src/node/routes). -Most of the meaty parts are in the VS Code portion of the codebase under -[vendor/modules/code-oss-dev](../vendor/modules/code-oss-dev), which we describe next. - -### Modifications to VS Code - -In v1 of code-server, we had a patch of VS Code that split the codebase into a -front-end and a server. The front-end consisted of the UI code, while the server -ran the extensions and exposed an API to the front-end for file access and all -UI needs. - -Over time, Microsoft added support to VS Code to run it on the web. They have -made the front-end open source, but not the server. As such, code-server v2 (and -later) uses the VS Code front-end and implements the server. We do this by using -a Git subtree to fork and modify VS Code. This code lives under -[vendor/modules/code-oss-dev](../vendor/modules/code-oss-dev). - -Some noteworthy changes in our version of VS Code include: - -- Adding our build file, [`vendor/modules/code-oss-dev/coder.js`](../vendor/modules/code-oss-dev/coder.js), which includes build steps specific to code-server -- Node.js version detection changes in [`build/lib/node.ts`](../vendor/modules/code-oss-dev/build/lib/node.ts) and [`build/lib/util.ts`](../vendor/modules/code-oss-dev/build/lib/util.ts) -- Allowing extra extension directories - - Added extra arguments to [`src/vs/platform/environment/common/argv.ts`](../vendor/modules/code-oss-dev/src/vs/platform/environment/common/argv.ts) and to [`src/vs/platform/environment/node/argv.ts`](../vendor/modules/code-oss-dev/src/vs/platform/environment/node/argv.ts) - - Added extra environment state to [`src/vs/platform/environment/common/environment.ts`](../vendor/modules/code-oss-dev/src/vs/platform/environment/common/environment.ts); - - Added extra getters to [`src/vs/platform/environment/common/environmentService.ts`](../vendor/modules/code-oss-dev/src/vs/platform/environment/common/environmentService.ts) - - Added extra scanning paths to [`src/vs/platform/extensionManagement/node/extensionsScanner.ts`](../vendor/modules/code-oss-dev/src/vs/platform/extensionManagement/node/extensionsScanner.ts) -- Additions/removals from [`package.json`](../vendor/modules/code-oss-dev/package.json): - - Removing `electron`, `keytar` and `native-keymap` to avoid pulling in desktop dependencies during build on Linux - - Removing `gulp-azure-storage` and `gulp-tar` (unsued in our build process, may pull in outdated dependencies) - - Adding `proxy-agent`, `proxy-from-env` (for proxying) and `rimraf` (used during build/install steps) -- Adding our branding/custom URLs/version: - - [`product.json`](../vendor/modules/code-oss-dev/product.json) - - [`src/vs/base/common/product.ts`](../vendor/modules/code-oss-dev/src/vs/base/common/product.ts) - - [`src/vs/workbench/browser/parts/dialogs/dialogHandler.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts) - - [`src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts) - - [`src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts) -- Removing azure/macOS signing related dependencies from [`build/package.json`](../vendor/modules/code-oss-dev/build/package.json) -- Modifying `.gitignore` to allow us to add files to `src/vs/server` and modifying `.eslintignore` to ignore lint on the shared files below (we use different formatter settings than VS Code). -- Sharing some files with our codebase via symlinks: - - [`src/vs/base/common/ipc.d.ts`](../vendor/modules/code-oss-dev/src/vs/base/common/ipc.d.ts) points to [`typings/ipc.d.ts`](../typings/ipc.d.ts) - - [`src/vs/base/common/util.ts`](../vendor/modules/code-oss-dev/src/vs/base/common/util.ts) points to [`src/common/util.ts`](../src/common/util.ts) - - [`src/vs/base/node/proxy_agent.ts`](../vendor/modules/code-oss-dev/src/vs/base/node/proxy_agent.ts) points to [`src/node/proxy_agent.ts`](../src/node/proxy_agent.ts) -- Allowing socket changes by adding `setSocket` in [`src/vs/base/parts/ipc/common/ipc.net.ts`](../vendor/modules/code-oss-dev/src/vs/base/parts/ipc/common/ipc.net.ts) - - We use this for connection persistence in our server-side code. -- Added our server-side Node.JS code to `src/vs/server`. - - This code includes the logic to spawn the various services (extension host, terminal, etc.) and some glue -- Added [`src/vs/workbench/browser/client.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/browser/client.ts) to hold some server customizations. - - Includes the functionality for the Log Out command and menu item - - Also, imported and called `initialize` from the main web file, [`src/vs/workbench/browser/web.main.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/browser/web.main.ts) -- Added a (hopefully temporary) hotfix to [`src/vs/workbench/common/resources.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/common/resources.ts) to get context menu actions working for the Git integration. -- Added connection type to WebSocket query parameters in [`src/vs/platform/remote/common/remoteAgentConnection.ts`](../vendor/modules/code-oss-dev/src/vs/platform/remote/common/remoteAgentConnection.ts) -- Added `CODE_SERVER*` variables to the sanitization list in [`src/vs/base/common/processes.ts`](../vendor/modules/code-oss-dev/src/vs/base/common/processes.ts) -- Fix localization support: - - Added file [`src/vs/workbench/services/localizations/browser/localizationsService.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/services/localizations/browser/localizationsService.ts). - - Modified file [`src/vs/base/common/platform.ts`](../vendor/modules/code-oss-dev/src/vs/base/common/platform.ts) - - Modified file [`src/vs/base/node/languagePacks.js`](../vendor/modules/code-oss-dev/src/vs/base/node/languagePacks.js) -- Added code to allow server to inject settings to [`src/vs/platform/product/common/product.ts`](../vendor/modules/code-oss-dev/src/vs/platform/product/common/product.ts) -- Extension fixes: - - Avoid disabling extensions by extensionKind in [`src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts) (Needed for vscode-icons) - - Remove broken symlinks in [`extensions/postinstall.js`](../vendor/modules/code-oss-dev/extensions/postinstall.js) - - Add tip about extension gallery in [`src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts) - - Use our own server for GitHub authentication in [`extensions/github-authentication/src/githubServer.ts`](../vendor/modules/code-oss-dev/extensions/github-authentication/src/githubServer.ts) - - Settings persistence on the server in [`src/vs/workbench/services/environment/browser/environmentService.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/services/environment/browser/environmentService.ts) - - Add extension install fallback in [`src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts) - - Add proxy-agent monkeypatch and keep extension host indefinitely running in [`src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts) - - Patch build system to avoid removing extension dependencies for `yarn global add` users in [`build/lib/extensions.ts`](../vendor/modules/code-oss-dev/build/lib/extensions.ts) - - Allow all extensions to use proposed APIs in [`src/vs/workbench/services/environment/browser/environmentService.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/services/environment/browser/environmentService.ts) - - Make storage writes async to allow extensions to wait for them to complete in [`src/vs/platform/storage/common/storage.ts`](../vendor/modules/code-oss-dev/src/vs/platform/storage/common/storage.ts) -- Specify webview path in [`src/vs/code/browser/workbench/workbench.ts`](../vendor/modules/code-oss-dev/src/vs/code/browser/workbench/workbench.ts) -- URL readability improvements for folder/workspace in [`src/vs/code/browser/workbench/workbench.ts`](../vendor/modules/code-oss-dev/src/vs/code/browser/workbench/workbench.ts) -- Socket/Authority-related fixes (for remote proxying etc.): - - [`src/vs/code/browser/workbench/workbench.ts`](../vendor/modules/code-oss-dev/src/vs/code/browser/workbench/workbench.ts) - - [`src/vs/platform/remote/browser/browserSocketFactory.ts`](../vendor/modules/code-oss-dev/src/vs/platform/remote/browser/browserSocketFactory.ts) - - [`src/vs/base/common/network.ts`](../vendor/modules/code-oss-dev/src/vs/base/common/network.ts) -- Added code to write out IPC path in [`src/vs/workbench/api/node/extHostCLIServer.ts`](../vendor/modules/code-oss-dev/src/vs/workbench/api/node/extHostCLIServer.ts) - -As the web portion of VS Code matures, we'll be able to shrink and possibly -eliminate our modifications. In the meantime, upgrading the VS Code version requires -us to ensure that our changes are still applied and work as intended. In the future, -we'd like to run VS Code unit tests against our builds to ensure that features -work as expected. +Most of the meaty parts are in the Code portion of the codebase under +[lib/vscode](../lib/vscode), which we describe next. + +### Modifications to Code + +Our modifications to Code can be found in the [patches](../patches) directory. +We pull in Code as a submodule pointing to an upstream release branch. + +In v1 of code-server, we had Code as a submodule and used a single massive patch +that split the codebase into a front-end and a server. The front-end consisted +of the UI code, while the server ran the extensions and exposed an API to the +front-end for file access and all UI needs. + +Over time, Microsoft added support to Code to run it on the web. They had made +the front-end open source, but not the server. As such, code-server v2 (and +later) uses the Code front-end and implements the server. We did this by using a +Git subtree to fork and modify Code. + +Microsoft eventually made the server open source and we were able to reduce our +changes significantly. Some time later we moved back to a submodule and patches +(managed by `quilt` this time instead of the mega-patch). + +As the web portion of Code continues to mature, we'll be able to shrink and +possibly eliminate our patches. In the meantime, upgrading the Code version +requires us to ensure that our changes are still applied correctly and work as +intended. In the future, we'd like to run Code unit tests against our builds to +ensure that features work as expected. > We have [extension docs](../ci/README.md) on the CI and build system. -If the functionality you're working on does NOT depend on code from VS Code, please +If the functionality you're working on does NOT depend on code from Code, please move it out and into code-server. ### Currently Known Issues -- Creating custom VS Code extensions and debugging them doesn't work +- Creating custom Code extensions and debugging them doesn't work - Extension profiling and tips are currently disabled diff --git a/docs/FAQ.md b/docs/FAQ.md index 8ade51ce7c89..656b45978fe3 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,3 +1,4 @@ + # FAQ @@ -13,30 +14,41 @@ - [How do I install an extension manually?](#how-do-i-install-an-extension-manually) - [How do I use my own extensions marketplace?](#how-do-i-use-my-own-extensions-marketplace) - [Where are extensions stored?](#where-are-extensions-stored) +- [Where is VS Code configuration stored?](#where-is-vs-code-configuration-stored) - [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration) - [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open) +- [Can I open a file at a specific line from a URL?](#can-i-open-a-file-at-a-specific-line-from-a-url) - [How do I access my Documents/Downloads/Desktop folders in code-server on macOS?](#how-do-i-access-my-documentsdownloadsdesktop-folders-in-code-server-on-macos) - [How do I direct server-side requests through a proxy?](#how-do-i-direct-server-side-requests-through-a-proxy) - [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server) - [What is the healthz endpoint?](#what-is-the-healthz-endpoint) - [What is the heartbeat file?](#what-is-the-heartbeat-file) +- [How do I change the reconnection grace time?](#how-do-i-change-the-reconnection-grace-time) - [How do I change the password?](#how-do-i-change-the-password) - [Can I store my password hashed?](#can-i-store-my-password-hashed) - [Is multi-tenancy possible?](#is-multi-tenancy-possible) - [Can I use Docker in a code-server container?](#can-i-use-docker-in-a-code-server-container) - [How do I disable telemetry?](#how-do-i-disable-telemetry) +- [What's the difference between code-server and Coder?](#whats-the-difference-between-code-server-and-coder) - [What's the difference between code-server and Theia?](#whats-the-difference-between-code-server-and-theia) -- [What's the difference between code-server and VS Code Codespaces?](#whats-the-difference-between-code-server-and-vs-code-codespaces) +- [What's the difference between code-server and OpenVSCode-Server?](#whats-the-difference-between-code-server-and-openvscode-server) +- [What's the difference between code-server and GitHub Codespaces?](#whats-the-difference-between-code-server-and-github-codespaces) +- [What's the difference between code-server and VS Code web?](#whats-the-difference-between-code-server-and-vs-code-web) - [Does code-server have any security login validation?](#does-code-server-have-any-security-login-validation) - [Are there community projects involving code-server?](#are-there-community-projects-involving-code-server) - [How do I change the port?](#how-do-i-change-the-port) +- [How do I hide the coder/coder promotion in Help: Getting Started?](#how-do-i-hide-the-codercoder-promotion-in-help-getting-started) +- [How do I disable the proxy?](#how-do-i-disable-the-proxy) +- [How do I disable file download?](#how-do-i-disable-file-download) +- [Why do web views not work?](#why-do-web-views-not-work) + ## Questions? Please file all questions and support requests at -. +. ## How should I expose code-server to the internet? @@ -82,6 +94,12 @@ app (PWA): 1. Start the editor 2. Click the **plus** icon in the URL toolbar to install the PWA +If you use Firefox, you can use the appropriate extension to install PWA. + +1. Go to the installation [website](https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/) of the add-on +2. Add the add-on to Firefox +3. Follow the os-specific instructions on how to install the runtime counterpart + For other browsers, you'll have to remap keybindings for shortcuts to work. ## Why can't code-server use Microsoft's extension marketplace? @@ -100,32 +118,28 @@ Service](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-St > Visual Studio Products and Services. Because of this, we can't offer any extensions on Microsoft's marketplace. -Instead, we've created a marketplace offering open-source extensions. The -marketplace works by scraping GitHub for VS Code extensions and building them. +Instead, we use the [Open-VSX extension gallery](https://open-vsx.org), which is also used by various other forks. +It isn't perfect, but its getting better by the day with more and more extensions. + +We also offer our own marketplace for open source extensions, but plan to +deprecate it at a future date and completely migrate to Open-VSX. These are the closed-source extensions that are presently unavailable: 1. [Live Share](https://visualstudio.microsoft.com/services/live-share). We may implement something similar (see - [#33](https://github.com/cdr/code-server/issues/33)) + [#33](https://github.com/coder/code-server/issues/33)) 1. [Remote Extensions (SSH, Containers, WSL)](https://github.com/microsoft/vscode-remote-release). We may implement these again at some point, see - ([#1315](https://github.com/cdr/code-server/issues/1315)). + ([#1315](https://github.com/coder/code-server/issues/1315)). For more about the closed source portions of VS Code, see [vscodium/vscodium](https://github.com/VSCodium/vscodium#why-does-this-exist). ## How can I request an extension that's missing from the marketplace? -We are in the process of transitioning to [Open VSX](https://open-vsx.org/). -Once we've [implemented Open -VSX](https://github.com/eclipse/openvsx/issues/249), we can finalize this -transition. As such, we are not currently accepting new extension requests. - -In the meantime, we suggest: - -- [Switching to Open VSX](#how-do-i-configure-the-marketplace-url) now -- Downloading and [installing the extension manually](#installing-an-extension-manually) +To add an extension to Open-VSX, please see [open-vsx/publish-extensions](https://github.com/open-vsx/publish-extensions). +We no longer plan to add new extensions to our legacy extension gallery. ## How do I install an extension? @@ -158,20 +172,19 @@ You can also download extensions using the command line. For instance, downloading from OpenVSX can be done like this: ```shell -SERVICE_URL=https://open-vsx.org/vscode/gallery ITEM_URL=https://open-vsx.org/vscode/item code-server --install-extension +code-server --install-extension ``` ## How do I use my own extensions marketplace? If you own a marketplace that implements the VS Code Extension Gallery API, you -can point code-server to it by setting `$SERVICE_URL` and `$ITEM_URL`. These correspond directly -to `serviceUrl` and `itemUrl` in VS Code's `product.json`. +can point code-server to it by setting `$EXTENSIONS_GALLERY`. +This corresponds directly with the `extensionsGallery` entry in in VS Code's `product.json`. -For example, to use [open-vsx.org](https://open-vsx.org), run: +For example: ```bash -export SERVICE_URL=https://open-vsx.org/vscode/gallery -export ITEM_URL=https://open-vsx.org/vscode/item +export EXTENSIONS_GALLERY='{"serviceUrl": "https://my-extensions/api"}' ``` Though you can technically use Microsoft's marketplace in this manner, we @@ -184,10 +197,20 @@ docs](https://github.com/VSCodium/vscodium/blob/master/DOCS.md#extensions--marke ## Where are extensions stored? -Extensions are store, by default, to `~/.local/share/code-server/extensions`. +Extensions are stored in `~/.local/share/code-server/extensions` by default. + +On Linux and macOS if you set the `XDG_DATA_HOME` environment variable, the +extensions directory will be `$XDG_DATA_HOME/code-server/extensions`. In +general, we try to follow the XDG directory spec. -If you set the `XDG_DATA_HOME` environment variable, the data directory will be -`$XDG_DATA_HOME/code-server/extensions`. In general, we try to follow the XDG directory spec. +## Where is VS Code configuration stored? + +VS Code configuration such as settings and keybindings are stored in +`~/.local/share/code-server` by default. + +On Linux and macOS if you set the `XDG_DATA_HOME` environment variable, the data +directory will be `$XDG_DATA_HOME/code-server`. In general, we try to follow the +XDG directory spec. ## How can I reuse my VS Code configuration? @@ -208,6 +231,39 @@ code-server tries the following in this order: 3. The workspace or directory passed via the command line 4. The last opened workspace or directory +## Can I open a file at a specific line from a URL? + +Yes. In addition to `workspace` and `folder`, code-server supports VS Code's +`payload` query parameter, which can open a specific file — optionally at a +line and column — once the workbench loads. + +`payload` is a URL-encoded JSON array of `[key, value]` string pairs. The +`openFile` key takes a `vscode-remote:///` URI, where +`` is the host you use to reach code-server. Add +`["gotoLineMode","true"]` to have a trailing `:line[:column]` suffix on the +path interpreted as a cursor position: + +```text +https://code.example.com/?folder=/home/coder/project&payload=[["gotoLineMode","true"],["openFile","vscode-remote://code.example.com/home/coder/project/src/app.py:10:5"]] +``` + +(with the `payload` value URL-encoded). Notes: + +- Paths must be absolute; there is no form relative to `folder`. +- This is upstream VS Code web behavior (the same mechanism vscode.dev uses), + so it works without any code-server-specific configuration. + +For example, to generate links from a shell: + +```sh +#!/bin/sh +# usage: code-link [line[:column]] +HOST=code.example.com +payload="[[\"gotoLineMode\",\"true\"],[\"openFile\",\"vscode-remote://$HOST$1${2:+:$2}\"]]" +printf 'https://%s/?folder=%s&payload=%s\n' "$HOST" "$(dirname "$1")" \ + "$(printf '%s' "$payload" | jq -sRr @uri)" +``` + ## How do I access my Documents/Downloads/Desktop folders in code-server on macOS? Newer versions of macOS require permission through a non-UNIX mechanism for @@ -226,7 +282,7 @@ You may have to give Node.js full disk access, since it doesn't implement any of Privacy** > **Privacy** > **Full Disk Access**. Then, click the 🔒 to unlock, click **+**, and select the Node.js binary you located in the previous step. -See [#2794](https://github.com/cdr/code-server/issues/2794) for additional context. +See [#2794](https://github.com/coder/code-server/issues/2794) for additional context. ## How do I direct server-side requests through a proxy? @@ -302,12 +358,18 @@ As long as there is an active browser connection, code-server touches `~/.local/share/code-server/heartbeat` once a minute. If you want to shutdown code-server if there hasn't been an active connection -after a predetermined amount of time, you can do so by checking continuously for -the last modified time on the heartbeat file. If it is older than X minutes (or -whatever amount of time you'd like), you can kill code-server. +after a predetermined amount of time, you can use the --idle-timeout-seconds flag +or set an `CODE_SERVER_IDLE_TIMEOUT_SECONDS` environment variable. + +## How do I change the reconnection grace time? + +Pass `--reconnection-grace-time ` to `code-server`, set +`CODE_SERVER_RECONNECTION_GRACE_TIME=`, or add +`reconnection-grace-time: ` to +`~/.config/code-server/config.yaml`. -Eventually, [#1636](https://github.com/cdr/code-server/issues/1636) will make -this process better. +The default is `10800` (3 hours). If a client stays disconnected longer than +this, it must reload the window. ## How do I change the password? @@ -337,6 +399,12 @@ hashed-password: "$argon2i$v=19$m=4096,t=3,p=1$wST5QhBgk2lu1ih4DMuxvg$LS1alrVdIW The `hashed-password` field takes precedence over `password`. +If you're using Docker Compose file, in order to make this work, you need to change all the single $ to $$. For example: + +```yaml +- HASHED_PASSWORD=$$argon2i$$v=19$$m=4096,t=3,p=1$$wST5QhBgk2lu1ih4DMuxvg$$LS1alrVdIWtvZHwnzCM1DUGg+5DTO3Dt1d5v9XtLws4 +``` + ## Is multi-tenancy possible? If you want to run multiple code-servers on shared infrastructure, we recommend @@ -357,12 +425,24 @@ mount into `/home/coder/myproject` from inside the `code-server` container. You need to make sure the Docker daemon's `/home/coder/myproject` is the same as the one mounted inside the `code-server` container, and the mount will work. +If you want Docker enabled when deploying on Kubernetes, look at the `values.yaml` +file for the 3 fields: `extraVars`, `lifecycle.postStart`, and `extraContainers`. + ## How do I disable telemetry? Use the `--disable-telemetry` flag to disable telemetry. > We use the data collected only to improve code-server. +## What's the difference between code-server and Coder? + +code-server and Coder are both applications that can be installed on any +machine. The main difference is who they serve. Out of the box, code-server is +simply VS Code in the browser while Coder is a tool for provisioning remote +development environments via Terraform. + +code-server was built for individuals while Coder was built for teams. In Coder, you create Workspaces which can have applications like code-server. If you're looking for a team solution, you should reach for [Coder](https://github.com/coder/coder). + ## What's the difference between code-server and Theia? At a high level, code-server is a patched fork of VS Code that runs in the @@ -377,18 +457,49 @@ for extensions. Theia doesn't allow you to reuse your existing VS Code config. -## What's the difference between code-server and VS Code Codespaces? +## What's the difference between code-server and OpenVSCode-Server? + +code-server and OpenVSCode-Server both allow you to access VS Code via a +browser. OpenVSCode-Server is a direct fork of VS Code with changes comitted +directly while code-server pulls VS Code in via a submodule and makes changes +via patch files. + +However, OpenVSCode-Server is scoped at only making VS Code available as-is in +the web browser. code-server contains additional changes to make the self-hosted +experience better (see the next section for details). -Both code-server and VS Code Codespaces allow you to access VS Code via a -browser. +## What's the difference between code-server and GitHub Codespaces? -VS Code Codespaces, however, is a closed-source, paid service offered by -Microsoft. While you can self-host environments with VS Code Codespaces, you -still need an Azure billing account, and you must access VS Code via the -Codespaces web dashboard instead of connecting directly to it. +Both code-server and GitHub Codespaces allow you to access VS Code via a +browser. GitHub Codespaces, however, is a closed-source, paid service offered by +GitHub and Microsoft. -On the other hand, code-server is free, open-source, and can be run on any -machine with few limitations. +On the other hand, code-server is self-hosted, free, open-source, and can be run +on any machine with few limitations. + +Specific changes include: + +- Password authentication +- The ability to host at sub-paths +- Self-contained web views that do not call out to Microsoft's servers +- The ability to use your own marketplace and collect your own telemetry +- Built-in proxy for accessing ports on the remote machine integrated into + VS Code's ports panel +- Settings are stored on disk like desktop VS Code, instead of in browser + storage (note that state is still stored in browser storage). +- Wrapper process that spawns VS Code on-demand and has a separate CLI +- Notification when updates are available +- [Some other things](https://github.com/coder/code-server/tree/main/patches) + +Some of these changes appear very unlikely to ever be adopted by Microsoft. +Some may make their way upstream, further closing the gap, but at the moment it +looks like there will always be some subtle differences. + +## What's the difference between code-server and VS Code web? + +VS Code web (which can be ran using `code serve-web`) has the same differences +as the Codespaces section above. VS Code web can be a better choice if you need +access to the official Microsoft marketplace. ## Does code-server have any security login validation? @@ -397,7 +508,7 @@ minute plus an additional twelve per hour. ## Are there community projects involving code-server? -Visit the [awesome-code-server](https://github.com/cdr/awesome-code-server) +Visit the [awesome-code-server](https://github.com/coder/awesome-code-server) repository to view community projects and guides with code-server! Feel free to add your own! @@ -407,3 +518,45 @@ There are two ways to change the port on which code-server runs: 1. with an environment variable e.g. `PORT=3000 code-server` 2. using the flag `--bind-addr` e.g. `code-server --bind-addr localhost:3000` + +## How do I hide the coder/coder promotion in Help: Getting Started? + +You can pass the flag `--disable-getting-started-override` to `code-server` or +you can set the environment variable `CS_DISABLE_GETTING_STARTED_OVERRIDE=1` or +`CS_DISABLE_GETTING_STARTED_OVERRIDE=true`. + +## How do I disable the proxy? + +You can pass the flag `--disable-proxy` to `code-server` or +you can set the environment variable `CS_DISABLE_PROXY=1` or +`CS_DISABLE_PROXY=true`. + +Note, this option currently only disables the proxy routes to forwarded ports, including +the domain and path proxy routes over HTTP and WebSocket; however, it does not +disable the automatic port forwarding in the VS Code workbench itself. In other words, +user will still see the Ports tab and notifications, but will not be able to actually +use access the ports. It is recommended to set `remote.autoForwardPorts` to `false` +when using the option. + +## How do I disable file download? + +You can pass the flag `--disable-file-downloads` to `code-server` + +## Why do web views not work? + +Web views rely on service workers, and service workers are only available in a +secure context, so most likely the answer is that you are using an insecure +context (for example an IP address). + +If this happens, in the browser log you will see something like: + +> Error loading webview: Error: Could not register service workers: SecurityError: Failed to register a ServiceWorker for scope with script: An SSL certificate error occurred when fetching the script.. + +To fix this, you must either: + +- Access over localhost/127.0.0.1 which is always considered secure. +- Use a domain with a real certificate (for example with Let's Encrypt). +- Use a trusted self-signed certificate with [mkcert](https://mkcert.dev) (or + create and trust a certificate manually). +- Disable security if your browser allows it. For example, in Chromium see + `chrome://flags/#unsafely-treat-insecure-origin-as-secure` diff --git a/docs/MAINTAINING.md b/docs/MAINTAINING.md index a131c73a5511..69263576ec2c 100644 --- a/docs/MAINTAINING.md +++ b/docs/MAINTAINING.md @@ -1,226 +1,110 @@ + # Maintaining -- [Team](#team) - - [Onboarding](#onboarding) - - [Offboarding](#offboarding) -- [Workflow](#workflow) - - [Milestones](#milestones) - - [Triage](#triage) - - [Project boards](#project-boards) -- [Versioning](#versioning) -- [Pull requests](#pull-requests) - - [Merge strategies](#merge-strategies) - - [Changelog](#changelog) -- [Releases](#releases) - - [Publishing a release](#publishing-a-release) +- [Releasing](#releasing) + - [Release Candidates](#release-candidates) - [AUR](#aur) - [Docker](#docker) - - [Homebrew](#homebrew) + - [nixpkgs](#nixpkgs) - [npm](#npm) +- [Testing](#testing) - [Documentation](#documentation) - [Troubleshooting](#troubleshooting) + + +We keep code-server up to date with VS Code releases (there are usually two or +three a month) but we are not generally actively developing code-server aside +from fixing regressions. + +Most of the work is keeping on top of issues and discussions. + +## Releasing + +1. Check that the changelog lists all the important changes. +2. Make sure the changelog entry lists the current version of VS Code. +3. Go to GitHub Actions > Draft release > Run workflow on the commit you want to + release. For the version we match VS Code's minor and patch version. The + patch number may become temporarily out of sync if we need to put out a + patch, but if we make our own minor change then we will not release it until + the next minor VS Code release. +4. CI will build an NPM package and platform-specific packages, and upload those + to a draft release. +5. Update the resulting draft release with the changelog contents. +6. Publish the draft release after validating it. +7. Update the changelog with the release date and bump the Helm chart version + once the Docker images have published. +8. Merge the PR submitted to coder/code-server-aur repo. + +#### Release Candidates + +We prefer to do release candidates so the community can test things before a +full-blown release. To do this follow the same steps as above but: + +1. Add a `-rc.` suffix to the version. +2. When you publish the release select "pre-release". CI will not automatically + publish pre-releases. +3. Do not update the chart version or merge in the changelog until the final + release. -This document is meant to serve current and future maintainers of code-server, -as well as share our workflow for maintaining the project. - -## Team - -Current maintainers: - -- @code-asher -- @TeffenEllis -- @jsjoeio - -Occassionally, other Coder employees may step in time to time to assist with code-server. - -### Onboarding - -To onboard a new maintainer to the project, please make sure to do the following: - -- [ ] Add to [cdr/code-server-reviewers](https://github.com/orgs/cdr/teams/code-server-reviewers) -- [ ] Add as Admin under [Repository Settings > Access](https://github.com/cdr/code-server/settings/access) -- [ ] Add to [npm Coder org](https://www.npmjs.com/org/coder) -- [ ] Add as [AUR maintainer](https://aur.archlinux.org/packages/code-server/) (talk to Colin) -- [ ] Introduce to community via Discussion (see [example](https://github.com/cdr/code-server/discussions/3955)) - -### Offboarding - -Very similar to Onboarding but Remove maintainer from all teams and revoke access. Please also do the following: - -- [ ] Write farewell post via Discussion (see [example](https://github.com/cdr/code-server/discussions/3933)) - -## Workflow - -The workflow used by code-server maintainers aims to be easy to understood by -the community and easy enough for new maintainers to jump in and start -contributing on day one. - -### Milestones - -We operate mainly using -[milestones](https://github.com/cdr/code-server/milestones). This was heavily -inspired by our friends over at [vscode](https://github.com/microsoft/vscode). - -Here are the milestones we use and how we use them: - -- "Backlog" -> Work not yet planned for a specific release. -- "On Deck" -> Work under consideration for upcoming milestones. -- "Backlog Candidates" -> Work that is not yet accepted for the backlog. We wait - for the community to weigh in. -- "<0.0.0>" -> Work to be done for a specific version. - -With this flow, any un-assigned issues are essentially in triage state. Once -triaged, issues are either "Backlog" or "Backlog Candidates". They will -eventually move to "On Deck" (or be closed). Lastly, they will end up on a -version milestone where they will be worked on. - -### Triage - -We use the following process for triaging GitHub issues: - -1. Create an issue -1. Add appropriate labels to the issue (including "needs-investigation" if we - should look into it further) -1. Add the issue to a milestone - 1. If it should be fixed soon, add to version milestone or "On Deck" - 2. If not urgent, add to "Backlog" - 3. Otherwise, add to "Backlog Candidate" for future consideration - -### Project boards - -We use project boards for projects or goals that span multiple milestones. - -Think of this as a place to put miscellaneous things (like testing, clean up -stuff, etc). As a maintainer, random tasks may come up here and there. The -project boards give you places to add temporary notes before opening a new -issue. Given that our release milestones function off of issues, we believe -tasks should have dedicated issues. - -Project boards also give us a way to separate the issue triage from -bigger-picture, long-term work. - -## Versioning - -`` - -The code-server project follows traditional [semantic -versioning](https://semver.org/), with the objective of minimizing major changes -that break backward compatibility. We increment the patch level for all -releases, except when the upstream Visual Studio Code project increments its -minor version or we change the plugin API in a backward-compatible manner. In -those cases, we increment the minor version rather than the patch level. - -## Pull requests - -Ideally, every PR should fix an issue. If it doesn't, make sure it's associated -with a version milestone. - -If a PR does fix an issue, don't add it to the version milestone. Otherwise, the -version milestone will have duplicate information: the issue and the PR fixing -the issue. - -### Merge strategies - -For most things, we recommend the **squash and merge** strategy. If you're -updating `lib/vscode`, we suggest using the **rebase and merge** strategy. There -may be times where **creating a merge commit** makes sense as well. Use your -best judgment. If you're unsure, you can always discuss in the PR with the team. - -### Changelog - -To save time when creating a new release for code-server, we keep a running -changelog at `CHANGELOG.md`. - -If either the author or reviewer of a PR believes the change should be mentioned -in the changelog, then it should be added. - -If there is not a **Next Version** when you modify `CHANGELOG.md`, please add it -using the template you see near the top of the changelog. - -When writing your changelog item, ask yourself: - -1. How do these changes affect code-server users? -2. What actions do they need to take (if any)? - -If you need inspiration, we suggest looking at the [Emacs -changelog](https://github.com/emacs-mirror/emacs/blob/master/etc/NEWS). - -## Releases - -With each release, we rotate the role of release manager to ensure every -maintainer goes through the process. This helps us keep documentation up-to-date -and encourages us to continually review and improve the flow. +#### AUR -If you're the current release manager, follow these steps: +We publish to AUR as a package +[here](https://aur.archlinux.org/packages/code-server/). This process is manual +and can be done by following the steps in [this +repo](https://github.com/coder/code-server-aur). -1. Create a [release issue](../.github/ISSUE_TEMPLATE/release.md) -1. Fill out checklist -1. Publish the release -1. After release is published, close release milestone +#### Docker -### Publishing a release +We publish code-server as a Docker image +[here](https://hub.docker.com/r/codercom/code-server), tagging it both with the +version and latest. -1. Create a release branch called `v0.0.0` but replace with new version -1. Run `yarn release:prep` and type in the new version (e.g., `3.8.1`) -1. GitHub Actions will generate the `npm-package`, `release-packages` and - `release-images` artifacts. You do not have to wait for this step to complete - before proceeding. -1. Run `yarn release:github-draft` to create a GitHub draft release from the - template with the updated version. -1. Summarize the major changes in the release notes and link to the relevant - issues. -1. Change the @ to target the version branch. Example: `v3.9.0 @ Target: v3.9.0` -1. Wait for the `npm-package`, `release-packages` and `release-images` artifacts - to build. -1. Run `yarn release:github-assets` to download the `release-packages` artifact. - They will upload them to the draft release. -1. Run some basic sanity tests on one of the released packages (pay special - attention to making sure the terminal works). -1. Publish the release and merge the PR. CI will automatically grab the - artifacts, publish the NPM package from `npm-package`, and publish the Docker - Hub image from `release-images`. -1. Update the AUR package. Instructions for updating the AUR package are at - [cdr/code-server-aur](https://github.com/cdr/code-server-aur). -1. Wait for the npm package to be published. +This is currently automated with the release process. -#### AUR +#### nixpkgs -We publish to AUR as a package [here](https://aur.archlinux.org/packages/code-server/). This process is manual and can be done by following the steps in [this repo](https://github.com/cdr/code-server-aur). +We publish code-server in nixpkgs but it must be updated manually. -#### Docker +#### npm -We publish code-server as a Docker image [here](https://registry.hub.docker.com/r/codercom/code-server), tagging it both with the version and latest. +We publish code-server as a npm package +[here](https://www.npmjs.com/package/code-server/v/latest). This is currently automated with the release process. -#### Homebrew - -We publish code-server on Homebrew [here](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb). +## Testing -This is currently automated with the release process (but may fail occassionally). If it does, run this locally: +Our testing structure is laid out under our [Contributing +docs](https://coder.com/docs/code-server/latest/CONTRIBUTING#test). -```shell -# Replace VERSION with version -brew bump-formula-pr --version="${VERSION}" code-server --no-browse --no-audit -``` +If you're ever looking to add more tests, here are a few ways to get started: -#### npm - -We publish code-server as a npm package [here](https://www.npmjs.com/package/code-server/v/latest). +- run `npm run test:unit` and look at the coverage chart. You'll see all the + uncovered lines. This is a good place to start. +- look at `test/scripts` to see which scripts are tested. We can always use more + tests there. +- look at `test/e2e`. We can always use more end-to-end tests. -This is currently automated with the release process. +Otherwise, talk to a current maintainer and ask which part of the codebase is +lacking most when it comes to tests. ## Documentation ### Troubleshooting -Our docs are hosted on [Vercel](https://vercel.com/). Vercel only shows logs in realtime, which means you need to have the logs open in one tab and reproduce your error in another tab. Since our logs are private to Coder the organization, you can only follow these steps if you're a Coder employee. Ask a maintainer for help if you need it. +Our docs are hosted on [Vercel](https://vercel.com/). Vercel only shows logs in +realtime, which means you need to have the logs open in one tab and reproduce +your error in another tab. Since our logs are private to Coder the organization, +you can only follow these steps if you're a Coder employee. Ask a maintainer for +help if you need it. -Taking a real scenario, let's say you wanted to troubleshoot [this docs change](https://github.com/cdr/code-server/pull/4042). Here is how you would do it: +Taking a real scenario, let's say you wanted to troubleshoot [this docs +change](https://github.com/coder/code-server/pull/4042). Here is how you would +do it: 1. Go to https://vercel.com/codercom/codercom 2. Click "View Function Logs" diff --git a/docs/README.md b/docs/README.md index da68fcc5b320..470095071afd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,12 @@ # code-server -[!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/cdr/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://cdr.co/join-community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq) [![codecov](https://codecov.io/gh/cdr/code-server/branch/main/graph/badge.svg?token=5iM9farjnC)](https://codecov.io/gh/cdr/code-server) [![See v3.12.0 docs](https://img.shields.io/static/v1?label=Docs&message=see%20v3.12.0%20&color=blue)](https://github.com/cdr/code-server/tree/v3.12.0/docs) +[!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/coder/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://coder.com/community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq) [![Discord](https://img.shields.io/discord/747933592273027093)](https://discord.com/invite/coder) [![codecov](https://codecov.io/gh/coder/code-server/branch/main/graph/badge.svg?token=5iM9farjnC)](https://codecov.io/gh/coder/code-server) [![See latest](https://img.shields.io/static/v1?label=Docs&message=see%20latest&color=blue)](https://coder.com/docs/code-server/latest) Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser. -![Screenshot](./assets/screenshot.png) +![Screenshot](./assets/screenshot-1.png) +![Screenshot](./assets/screenshot-2.png) ## Highlights @@ -16,23 +17,27 @@ access it in the browser. ## Requirements -See [requirements](requirements.md) for minimum specs, as well as instructions +See [requirements](https://coder.com/docs/code-server/latest/requirements) for minimum specs, as well as instructions on how to set up a Google VM on which you can install code-server. -**TL;DR:** Linux machine with WebSockets enabled, 1 GB RAM, and 2 CPUs +**TL;DR:** Linux machine with WebSockets enabled, 1 GB RAM, and 2 vCPUs ## Getting started -There are three ways to get started: +There are five ways to get started: 1. Using the [install - script](https://github.com/cdr/code-server/blob/main/install.sh), which + script](https://github.com/coder/code-server/blob/main/install.sh), which automates most of the process. The script uses the system package manager if possible. 2. Manually [installing code-server](https://coder.com/docs/code-server/latest/install) -3. Using our one-click buttons and guides to [deploy code-server to a cloud - provider](https://github.com/cdr/deploy-code-server) ⚡ +3. Deploy code-server to your team with [coder/coder](https://cdr.co/coder-github) +4. Using our one-click buttons and guides to [deploy code-server to a cloud + provider](https://github.com/coder/deploy-code-server) ⚡ +5. Using the [code-server feature for + devcontainers](https://github.com/coder/devcontainer-features/blob/main/src/code-server/README.md), + if you already use devcontainers in your project. If you use the install script, you can preview what occurs during the install process: @@ -50,13 +55,12 @@ curl -fsSL https://code-server.dev/install.sh | sh When done, the install script prints out instructions for running and starting code-server. +> **Note** +> To manage code-server for a team on your infrastructure, see: [coder/coder](https://cdr.co/coder-github) + We also have an in-depth [setup and configuration](https://coder.com/docs/code-server/latest/guide) guide. -## TLS and authentication (beta) - -To add TLS and authentication out of the box, use [code-server --link](https://coder.com/docs/code-server/latest/link). - ## Questions? See answers to [frequently asked @@ -72,7 +76,7 @@ details. Interested in [working at Coder](https://coder.com/careers)? Check out [our open positions](https://coder.com/careers#openings)! -## For Organizations +## For Teams -Want remote development for your organization or enterprise? Visit [our -website](https://coder.com) to learn more about Coder. +We develop [coder/coder](https://cdr.co/coder-github) to help teams to +adopt remote development. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index bb7fc71790e4..9ff33e365c4b 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -16,19 +16,17 @@ We use the following tools to help us stay on top of vulnerability mitigation. - [trivy](https://github.com/aquasecurity/trivy) - Comprehensive vulnerability scanner that runs on PRs into the default branch and scans both our container image and repository code (see - `trivy-scan-repo` and `trivy-scan-image` jobs in `ci.yaml`) -- [`audit-ci`](https://github.com/IBM/audit-ci) - - Audits npm and Yarn dependencies in CI (see `Audit for vulnerabilities` step - in `ci.yaml`) on PRs into the default branch and fails CI if moderate or - higher vulnerabilities (see the `audit.sh` script) are present. + `trivy-scan-repo` and `trivy-scan-image` jobs in `build.yaml`) +- `npm audit` + - Audits NPM dependencies. ## Supported Versions Coder sponsors the development and maintenance of the code-server project. We will fix security issues within 90 days of receiving a report and publish the fix in a subsequent release. The code-server project does not provide backports or patch releases for security issues at this time. -| Version | Supported | -| ----------------------------------------------------- | ------------------ | -| [Latest](https://github.com/cdr/code-server/releases) | :white_check_mark: | +| Version | Supported | +| ------------------------------------------------------- | ------------------ | +| [Latest](https://github.com/coder/code-server/releases) | :white_check_mark: | ## Reporting a Vulnerability diff --git a/docs/android.md b/docs/android.md new file mode 100644 index 000000000000..4541bc7b5fee --- /dev/null +++ b/docs/android.md @@ -0,0 +1,31 @@ +# Running code-server using UserLAnd + +1. Install UserLAnd from [Google Play](https://play.google.com/store/apps/details?id=tech.ula&hl=en_US&gl=US) +2. Install an Ubuntu VM +3. Start app +4. Install Node.js and `curl` using `sudo apt install nodejs npm curl -y` +5. Install `nvm`: + +```shell +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +``` + +6. Exit the terminal using `exit` and then reopen the terminal +7. Install and use Node.js 24: + +```shell +nvm install 24 +nvm use 24 +``` + +8. Install code-server globally on device with: `npm install --global code-server` +9. Run code-server with `code-server` +10. Access on localhost:8080 in your browser + +# Running code-server using Nix-on-Droid + +1. Install Nix-on-Droid from [F-Droid](https://f-droid.org/packages/com.termux.nix/) +2. Start app +3. Spawn a shell with code-server by running `nix-shell -p code-server` +4. Run code-server with `code-server` +5. Access on localhost:8080 in your browser diff --git a/docs/assets/images/icons/collab.svg b/docs/assets/images/icons/collab.svg new file mode 100644 index 000000000000..239666a993bf --- /dev/null +++ b/docs/assets/images/icons/collab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/images/icons/contributing.svg b/docs/assets/images/icons/contributing.svg new file mode 100644 index 000000000000..c814591e1c7f --- /dev/null +++ b/docs/assets/images/icons/contributing.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/images/icons/faq.svg b/docs/assets/images/icons/faq.svg new file mode 100644 index 000000000000..a3e196d298a9 --- /dev/null +++ b/docs/assets/images/icons/faq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/images/icons/home.svg b/docs/assets/images/icons/home.svg new file mode 100644 index 000000000000..0f7bee254cd3 --- /dev/null +++ b/docs/assets/images/icons/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/images/icons/requirements.svg b/docs/assets/images/icons/requirements.svg new file mode 100644 index 000000000000..c3888f90274f --- /dev/null +++ b/docs/assets/images/icons/requirements.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/images/icons/upgrade.svg b/docs/assets/images/icons/upgrade.svg new file mode 100644 index 000000000000..28c35752f201 --- /dev/null +++ b/docs/assets/images/icons/upgrade.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/images/icons/usage.svg b/docs/assets/images/icons/usage.svg new file mode 100644 index 000000000000..f38aa04813e3 --- /dev/null +++ b/docs/assets/images/icons/usage.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/assets/images/icons/wrench.svg b/docs/assets/images/icons/wrench.svg new file mode 100644 index 000000000000..acca9b7614a1 --- /dev/null +++ b/docs/assets/images/icons/wrench.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/screenshot-1.png b/docs/assets/screenshot-1.png new file mode 100644 index 000000000000..cacbc21bd771 Binary files /dev/null and b/docs/assets/screenshot-1.png differ diff --git a/docs/assets/screenshot-2.png b/docs/assets/screenshot-2.png new file mode 100644 index 000000000000..5861fac0b905 Binary files /dev/null and b/docs/assets/screenshot-2.png differ diff --git a/docs/assets/screenshot.png b/docs/assets/screenshot.png deleted file mode 100644 index a35720acdd2b..000000000000 Binary files a/docs/assets/screenshot.png and /dev/null differ diff --git a/docs/coder.md b/docs/coder.md new file mode 100644 index 000000000000..eff3423b5b7a --- /dev/null +++ b/docs/coder.md @@ -0,0 +1,48 @@ +# Coder + +To install and run code-server in a Coder workspace, we suggest using the `install.sh` +script in your template like so: + +```terraform +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + startup_script = < -# Setup Guide - - - [Expose code-server](#expose-code-server) - - [Port forwarding via SSH](#port-forwarding-via-ssh) - - [Using Let's Encrypt with Caddy](#using-lets-encrypt-with-caddy) - - [Using Let's Encrypt with NGINX](#using-lets-encrypt-with-nginx) - - [Using a self-signed certificate](#using-a-self-signed-certificate) - - [External authentication](#external-authentication) - - [HTTPS and self-signed certificates](#https-and-self-signed-certificates) - - [Accessing web services](#accessing-web-services) - - [Using a subdomain](#using-a-subdomain) - - [Using a subpath](#using-a-subpath) - - [Stripping `/proxy/` from the request path](#stripping-proxyport-from-the-request-path) - - [Proxying to create a React app](#proxying-to-create-a-react-app) - - [Proxying to a Vue app](#proxying-to-a-vue-app) -- [Setup Guide](#setup-guide) - - [Expose code-server](#expose-code-server-1) - - [Port forwarding via SSH](#port-forwarding-via-ssh-1) - - [Using Let's Encrypt with Caddy](#using-lets-encrypt-with-caddy-1) - - [Using Let's Encrypt with NGINX](#using-lets-encrypt-with-nginx-1) - - [Using a self-signed certificate](#using-a-self-signed-certificate-1) - - [External authentication](#external-authentication-1) - - [HTTPS and self-signed certificates](#https-and-self-signed-certificates-1) - - [Accessing web services](#accessing-web-services-1) - - [Using a subdomain](#using-a-subdomain-1) - - [Using a subpath](#using-a-subpath-1) - - [Stripping `/proxy/` from the request path](#stripping-proxyport-from-the-request-path-1) - - [Proxying to create a React app](#proxying-to-create-a-react-app-1) - - [Proxying to a Vue app](#proxying-to-a-vue-app-1) - - [SSH into code-server on VS Code](#ssh-into-code-server-on-vs-code) - - [Option 1: cloudflared tunnel](#option-1-cloudflared-tunnel) - - [Option 2: ngrok tunnel](#option-2-ngrok-tunnel) - - - -This article will walk you through exposing code-server securely once you've -completed the [installation process](install.md). - -## Expose code-server - -**Never** expose code-server directly to the internet without some form of -authentication and encryption, otherwise someone can take over your machine via -the terminal. - -By default, code-server uses password authentication. As such, you must copy the -password from code-server's config file to log in. To avoid exposing itself -unnecessarily, code-server listens on `localhost`; this practice is fine for -testing, but it doesn't work if you want to access code-server from a different -machine. - -> **Rate limits:** code-server rate limits password authentication attempts to -> two per minute plus an additional twelve per hour. - -There are several approaches to operating and exposing code-server securely: - -- Port forwarding via SSH -- Using Let's Encrypt with Caddy -- Using Let's Encrypt with NGINX -- Using a self-signed certificate - -### Port forwarding via SSH - -We highly recommend using [port forwarding via -SSH](https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding) to access -code-server. If you have an SSH server on your remote machine, this approach -doesn't required additional setup. - -The downside to SSH forwarding, however, is that you can't access code-server -when using machines without SSH clients (such as iPads). If this applies to you, -we recommend using another method, such as [Let's Encrypt](#let-encrypt) instead. - -> To work properly, your environment should have WebSockets enabled, which -> code-server uses to communicate between the browser and server. - -1. SSH into your instance and edit the code-server config file to disable - password authentication: - - ```console - # Replaces "auth: password" with "auth: none" in the code-server config. - sed -i.bak 's/auth: password/auth: none/' ~/.config/code-server/config.yaml - ``` - -2. Restart code-server: - - ```console - sudo systemctl restart code-server@$USER - ``` - -3. Forward local port `8080` to `127.0.0.1:8080` on the remote instance by running the following command on your local machine: - - ```console - # -N disables executing a remote shell - ssh -N -L 8080:127.0.0.1:8080 [user]@ - ``` - -4. At this point, you can access code-server by pointing your web browser to `http://127.0.0.1:8080`. - -5. If you'd like to make the port forwarding via SSH persistent, we recommend - using [mutagen](https://mutagen.io/documentation/introduction/installation) - to do so. Once you've installed mutagen, you can port forward as follows: - - ```console - # This is the same as the above SSH command, but it runs in the background - # continuously. Be sure to add `mutagen daemon start` to your ~/.bashrc to - # start the mutagen daemon when you open a shell. - - mutagen forward create --name=code-server tcp:127.0.0.1:8080 < instance-ip > :tcp:127.0.0.1:8080 - ``` - -6. Optional, but highly recommended: add the following to `~/.ssh/config` so - that you can detect bricked SSH connections: - - ```bash - Host * - ServerAliveInterval 5 - ExitOnForwardFailure yes - ``` - -> You can [forward your -> SSH](https://developer.github.com/v3/guides/using-ssh-agent-forwarding/) and -> [GPG agent](https://wiki.gnupg.org/AgentForwarding) to the instance to -> securely access GitHub and sign commits without having to copy your keys. - -### Using Let's Encrypt with Caddy - -Using [Let's Encrypt](https://letsencrypt.org) is an option if you want to -access code-server on an iPad or do not want to use SSH port forwarding. - -1. This option requires that the remote machine be exposed to the internet. Make sure that your instance allows HTTP/HTTP traffic. - -1. You'll need a domain name (if you don't have one, you can purchase one from - [Google Domains](https://domains.google.com) or the domain service of your - choice)). Once you have a domain name, add an A record to your domain that contains your - instance's IP address. - -1. Install [Caddy](https://caddyserver.com/docs/download#debian-ubuntu-raspbian): - -```console -sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/gpg/gpg.155B6D79CA56EA34.key' | sudo apt-key add - -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/setup/config.deb.txt?distro=debian&version=any-version' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list -sudo apt update -sudo apt install caddy -``` - -1. Replace `/etc/caddy/Caddyfile` using `sudo` so that the file looks like this: - - ```text - mydomain.com - - reverse_proxy 127.0.0.1:8080 - ``` - - If you want to serve code-server from a sub-path, you can do so as follows: - - ```text - mydomain.com/code/* { - uri strip_prefix /code - reverse_proxy 127.0.0.1:8080 - } - ``` - - Remember to replace `mydomain.com` with your domain name! - -1. Reload Caddy: - - ```console - sudo systemctl reload caddy - ``` - -At this point, you should be able to access code-server via -`https://mydomain.com`. - -### Using Let's Encrypt with NGINX - -1. This option requires that the remote machine be exposed to the internet. Make sure that your instance allows HTTP/HTTP traffic. - -1. You'll need a domain name (if you don't have one, you can purchase one from - [Google Domains](https://domains.google.com) or the domain service of your - choice)). Once you have a domain name, add an A record to your domain that contains your - instance's IP address. - -1. Install NGINX: - - ```bash - sudo apt update - sudo apt install -y nginx certbot python3-certbot-nginx - ``` - -1. Update `/etc/nginx/sites-available/code-server` using sudo with the following - configuration: - - ```text - server { - listen 80; - listen [::]:80; - server_name mydomain.com; - - location / { - proxy_pass http://localhost:8080/; - proxy_set_header Host $host; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection upgrade; - proxy_set_header Accept-Encoding gzip; - } - } - ``` - - Be sure to replace `mydomain.com` with your domain name! - -1. Enable the config: - - ```console - sudo ln -s ../sites-available/code-server /etc/nginx/sites-enabled/code-server - sudo certbot --non-interactive --redirect --agree-tos --nginx -d mydomain.com -m me@example.com - ``` - - Be sure to replace `me@example.com` with your actual email. - -At this point, you should be able to access code-server via -`https://mydomain.com`. - -### Using a self-signed certificate - -> Self signed certificates do not work with iPad; see [./ipad.md](./ipad.md) for -> more information. - -Before proceeding, we recommend familiarizing yourself with the [risks of -self-signing a certificate for -SSL](https://security.stackexchange.com/questions/8110). - -We recommend self-signed certificates as a last resort, since self-signed -certificates do not work with iPads and may cause unexpected issues with -code-server. You should only proceed with this option if: - -- You do not want to buy a domain or you cannot expose the remote machine to - the internet -- You do not want to use port forwarding via SSH - -To use a self-signed certificate: - -1. This option requires that the remote machine be exposed to the internet. Make - sure that your instance allows HTTP/HTTP traffic. - -1. SSH into your instance and edit your code-server config file to use a - randomly generated self-signed certificate: - - ```console - # Replaces "cert: false" with "cert: true" in the code-server config. - sed -i.bak 's/cert: false/cert: true/' ~/.config/code-server/config.yaml - # Replaces "bind-addr: 127.0.0.1:8080" with "bind-addr: 0.0.0.0:443" in the code-server config. - sed -i.bak 's/bind-addr: 127.0.0.1:8080/bind-addr: 0.0.0.0:443/' ~/.config/code-server/config.yaml - # Allows code-server to listen on port 443. - sudo setcap cap_net_bind_service=+ep /usr/lib/code-server/lib/node - ``` - -1. Restart code-server: - - ```console - sudo systemctl restart code-server@$USER - ``` - -At this point, you should be able to access code-server via -`https://`. - -If you'd like to avoid the warnings displayed by code-server when using a -self-signed certificate, you can use [mkcert](https://mkcert.dev) to create a -self-signed certificate that's trusted by your operating system, then pass the -certificate to code-server via the `cert` and `cert-key` config fields. - -## External authentication - -If you want to use external authentication mechanism (e.g., Sign in with -Google), you can do this with a reverse proxy such as: - -- [Pomerium](https://www.pomerium.io/guides/code-server.html) -- [oauth2_proxy](https://github.com/pusher/oauth2_proxy) -- [Cloudflare Access](https://teams.cloudflare.com/access) - -## HTTPS and self-signed certificates - -For HTTPS, you can use a self-signed certificate by: - -- Passing in `--cert` -- Passing in an existing certificate by providing the path to `--cert` and the - path to the key with `--cert-key` - -The self signed certificate will be generated to -`~/.local/share/code-server/self-signed.crt`. - -If you pass a certificate to code-server, it will respond to HTTPS requests and -redirect all HTTP requests to HTTPS. - -> You can use [Let's Encrypt](https://letsencrypt.org/) to get a TLS certificate -> for free. - -Note: if you set `proxy_set_header Host $host;` in your reverse proxy config, it will change the address displayed in the green section of code-server in the bottom left to show the correct address. - -## Accessing web services - -If you're working on web services and want to access it locally, code-server -can proxy to any port using either a subdomain or a subpath, allowing you to -securely access these services using code-server's built-in authentication. - -### Using a subdomain - -You will need a DNS entry that points to your server for each port you want to -access. You can either set up a wildcard DNS entry for `*.` if your -domain name registrar supports it, or you can create one for every port you want -to access (`3000.`, `8080.`, etc). - -You should also set up TLS certificates for these subdomains, either using a -wildcard certificate for `*.` or individual certificates for each port. - -To set your domain, start code-server with the `--proxy-domain` flag: - -```console -code-server --proxy-domain -``` - -Now you can browse to `.`. Note that this uses the host header, so -ensure your reverse proxy (if you're using one) forwards that information. - -### Using a subpath - -Simply browse to `/proxy//`. - -### Stripping `/proxy/` from the request path - -You may notice that the code-server proxy strips `/proxy/` from the -request path. - -HTTP servers should use relative URLs to avoid the need to be coupled to the -absolute path at which they are served. This means you must [use trailing -slashes on all paths with -subpaths](https://blog.cdivilly.com/2019/02/28/uri-trailing-slashes). - -This reasoning is why the default behavior is to strip `/proxy/` from the -base path. If your application uses relative URLs and does not assume the -absolute path at which it is being served, it will just work no matter what port -you decide to serve it off or if you put it in behind code-server or any other -proxy. - -However, some prefer the cleaner aesthetic of no trailing slashes. Omitting the -trailing slashes couples you to the base path, since you cannot use relative -redirects correctly anymore. If you're okay with this tradeoff, use `/absproxy` -instead and the path will be passed as is (e.g., `/absproxy/3000/my-app-path`). - -### Proxying to create a React app - -You must use `/absproxy/` with `create-react-app` (see -[#2565](https://github.com/cdr/code-server/issues/2565) and -[#2222](https://github.com/cdr/code-server/issues/2222) for more information). -You will need to inform `create-react-app` of the path at which you are serving -via `$PUBLIC_URL` and webpack via `$WDS_SOCKET_PATH`: - -```sh -PUBLIC_URL=/absproxy/3000 \ - WDS_SOCKET_PATH=$PUBLIC_URL/sockjs-node \ - BROWSER=none yarn start -``` - -You should then be able to visit `https://my-code-server-address.io/absproxy/3000` to see your app exposed through -code-server! - -> We highly recommend using the subdomain approach instead to avoid this class of issue. - -### Proxying to a Vue app - -Similar to the situation with React apps, you have to make a few modifications to proxy a Vue app. - -1. add `vue.config.js` -2. update the values to match this (you can use any free port): - -```js -module.exports = { - devServer: { - port: 3454, - sockPath: "sockjs-node", - }, - publicPath: "/absproxy/3454", -} -``` - -3. access app at `/absproxy/3454` e.g. `http://localhost:8080/absproxy/3454` - -Read more about `publicPath` in the [Vue.js docs](https://cli.vuejs.org/config/#publicpath) - - - - # Setup Guide - [Expose code-server](#expose-code-server) @@ -398,19 +8,26 @@ Read more about `publicPath` in the [Vue.js docs](https://cli.vuejs.org/config/# - [Using Let's Encrypt with Caddy](#using-lets-encrypt-with-caddy) - [Using Let's Encrypt with NGINX](#using-lets-encrypt-with-nginx) - [Using a self-signed certificate](#using-a-self-signed-certificate) + - [TLS 1.3 and Safari](#tls-13-and-safari) - [External authentication](#external-authentication) - [HTTPS and self-signed certificates](#https-and-self-signed-certificates) - [Accessing web services](#accessing-web-services) - [Using a subdomain](#using-a-subdomain) - [Using a subpath](#using-a-subpath) + - [Using your own proxy](#using-your-own-proxy) - [Stripping `/proxy/` from the request path](#stripping-proxyport-from-the-request-path) - [Proxying to create a React app](#proxying-to-create-a-react-app) - [Proxying to a Vue app](#proxying-to-a-vue-app) -- [SSH into code-server on VS Code](#ssh-into-code-server-on-vs-code) - - [Option 1: cloudflared tunnel](#option-1-cloudflared-tunnel) - - [Option 2: ngrok tunnel](#option-2-ngrok-tunnel) + - [Proxying to an Angular app](#proxying-to-an-angular-app) + - [Proxying to a Svelte app](#proxying-to-a-svelte-app) + - [Prefixing `/absproxy/` with a path](#prefixing-absproxyport-with-a-path) + - [Preflight requests](#preflight-requests) +- [Internationalization and customization](#internationalization-and-customization) + - [Available keys and placeholders](#available-keys-and-placeholders) + - [Legacy flag](#legacy-flag) + This article will walk you through exposing code-server securely once you've completed the [installation process](install.md). @@ -442,7 +59,7 @@ There are several approaches to operating and exposing code-server securely: We highly recommend using [port forwarding via SSH](https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding) to access code-server. If you have an SSH server on your remote machine, this approach -doesn't required additional setup. +doesn't require any additional setup at all. The downside to SSH forwarding, however, is that you can't access code-server when using machines without SSH clients (such as iPads). If this applies to you, @@ -478,11 +95,10 @@ we recommend using another method, such as [Let's Encrypt](#let-encrypt) instead using [mutagen](https://mutagen.io/documentation/introduction/installation) to do so. Once you've installed mutagen, you can port forward as follows: - ```console + ```shell # This is the same as the above SSH command, but it runs in the background # continuously. Be sure to add `mutagen daemon start` to your ~/.bashrc to # start the mutagen daemon when you open a shell. - mutagen forward create --name=code-server tcp:127.0.0.1:8080 < instance-ip > :tcp:127.0.0.1:8080 ``` @@ -505,29 +121,29 @@ we recommend using another method, such as [Let's Encrypt](#let-encrypt) instead Using [Let's Encrypt](https://letsencrypt.org) is an option if you want to access code-server on an iPad or do not want to use SSH port forwarding. -1. This option requires that the remote machine be exposed to the internet. Make sure that your instance allows HTTP/HTTP traffic. +1. This option requires that the remote machine be exposed to the internet. Make sure that your instance allows HTTP/HTTPS traffic. -1. You'll need a domain name (if you don't have one, you can purchase one from +2. You'll need a domain name (if you don't have one, you can purchase one from [Google Domains](https://domains.google.com) or the domain service of your - choice)). Once you have a domain name, add an A record to your domain that contains your + choice). Once you have a domain name, add an A record to your domain that contains your instance's IP address. -1. Install [Caddy](https://caddyserver.com/docs/download#debian-ubuntu-raspbian): +3. Install [Caddy](https://caddyserver.com/docs/download#debian-ubuntu-raspbian): -```console -sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/gpg/gpg.155B6D79CA56EA34.key' | sudo apt-key add - -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/setup/config.deb.txt?distro=debian&version=any-version' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list -sudo apt update -sudo apt install caddy -``` + ```console + sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list + sudo apt update + sudo apt install caddy + ``` -1. Replace `/etc/caddy/Caddyfile` using `sudo` so that the file looks like this: +4. Replace `/etc/caddy/Caddyfile` using `sudo` so that the file looks like this: ```text - mydomain.com - - reverse_proxy 127.0.0.1:8080 + mydomain.com { + reverse_proxy 127.0.0.1:8080 + } ``` If you want to serve code-server from a sub-path, you can do so as follows: @@ -536,12 +152,12 @@ sudo apt install caddy mydomain.com/code/* { uri strip_prefix /code reverse_proxy 127.0.0.1:8080 - } + } ``` Remember to replace `mydomain.com` with your domain name! -1. Reload Caddy: +5. Reload Caddy: ```console sudo systemctl reload caddy @@ -552,21 +168,22 @@ At this point, you should be able to access code-server via ### Using Let's Encrypt with NGINX -1. This option requires that the remote machine be exposed to the internet. Make sure that your instance allows HTTP/HTTP traffic. +1. This option requires that the remote machine be exposed to the internet. Make + sure that your instance allows HTTP/HTTPS traffic. -1. You'll need a domain name (if you don't have one, you can purchase one from +2. You'll need a domain name (if you don't have one, you can purchase one from [Google Domains](https://domains.google.com) or the domain service of your - choice)). Once you have a domain name, add an A record to your domain that contains your + choice). Once you have a domain name, add an A record to your domain that contains your instance's IP address. -1. Install NGINX: +3. Install NGINX: ```bash sudo apt update sudo apt install -y nginx certbot python3-certbot-nginx ``` -1. Update `/etc/nginx/sites-available/code-server` using sudo with the following +4. Update `/etc/nginx/sites-available/code-server` using sudo with the following configuration: ```text @@ -577,7 +194,7 @@ At this point, you should be able to access code-server via location / { proxy_pass http://localhost:8080/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Accept-Encoding gzip; @@ -587,13 +204,11 @@ At this point, you should be able to access code-server via Be sure to replace `mydomain.com` with your domain name! -1. Enable the config: - +5. Enable the config: ```console sudo ln -s ../sites-available/code-server /etc/nginx/sites-enabled/code-server sudo certbot --non-interactive --redirect --agree-tos --nginx -d mydomain.com -m me@example.com ``` - Be sure to replace `me@example.com` with your actual email. At this point, you should be able to access code-server via @@ -619,7 +234,7 @@ code-server. You should only proceed with this option if: To use a self-signed certificate: 1. This option requires that the remote machine be exposed to the internet. Make - sure that your instance allows HTTP/HTTP traffic. + sure that your instance allows HTTP/HTTPS traffic. 1. SSH into your instance and edit your code-server config file to use a randomly generated self-signed certificate: @@ -647,14 +262,21 @@ self-signed certificate, you can use [mkcert](https://mkcert.dev) to create a self-signed certificate that's trusted by your operating system, then pass the certificate to code-server via the `cert` and `cert-key` config fields. +### TLS 1.3 and Safari + +If you will be using Safari and your configuration does not allow anything less +than TLS 1.3 you will need to add support for TLS 1.2 since Safari does not +support TLS 1.3 for web sockets at the time of writing. If this is the case you +should see OSSStatus: 9836 in the browser console. + ## External authentication If you want to use external authentication mechanism (e.g., Sign in with Google), you can do this with a reverse proxy such as: -- [Pomerium](https://www.pomerium.io/guides/code-server.html) -- [oauth2_proxy](https://github.com/pusher/oauth2_proxy) -- [Cloudflare Access](https://teams.cloudflare.com/access) +- [Pomerium](https://www.pomerium.com/docs/guides/code-server.html) +- [oauth2-proxy](https://oauth2-proxy.github.io/oauth2-proxy/) +- [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) ## HTTPS and self-signed certificates @@ -673,11 +295,13 @@ redirect all HTTP requests to HTTPS. > You can use [Let's Encrypt](https://letsencrypt.org/) to get a TLS certificate > for free. -Note: if you set `proxy_set_header Host $host;` in your reverse proxy config, it will change the address displayed in the green section of code-server in the bottom left to show the correct address. +Note: if you set `proxy_set_header Host $host;` in your reverse proxy config, it +will change the address displayed in the green section of code-server in the +bottom left to show the correct address. ## Accessing web services -If you're working on web services and want to access it locally, code-server +If you're working on web services and want to access them locally, code-server can proxy to any port using either a subdomain or a subpath, allowing you to securely access these services using code-server's built-in authentication. @@ -697,12 +321,32 @@ To set your domain, start code-server with the `--proxy-domain` flag: code-server --proxy-domain ``` -Now you can browse to `.`. Note that this uses the host header, so -ensure your reverse proxy (if you're using one) forwards that information. +For instance, if you have code-server exposed on `domain.tld` and a Python +server running on port 8080 of the same machine code-server is running on, you +could run code-server with `--proxy-domain domain.tld` and access the Python +server via `8080.domain.tld`. + +Note that this uses the host header, so ensure your reverse proxy (if you're +using one) forwards that information. ### Using a subpath -Simply browse to `/proxy//`. +Simply browse to `/proxy//`. For instance, if you have code-server +exposed on `domain.tld` and a Python server running on port 8080 of the same +machine code-server is running on, you could access the Python server via +`domain.tld/proxy/8000`. + +### Using your own proxy + +You can make extensions and the ports panel use your own proxy by setting +`VSCODE_PROXY_URI`. For example if you set +`VSCODE_PROXY_URI=https://{{port}}.kyle.dev` when an application is detected +running on port 3000 of the same machine code-server is running on the ports +panel will create a link to https://3000.kyle.dev instead of pointing to the +built-in subpath-based proxy. + +Note: relative paths are also supported i.e. +`VSCODE_PROXY_URI=./proxy/{{port}}` ### Stripping `/proxy/` from the request path @@ -728,8 +372,8 @@ instead and the path will be passed as is (e.g., `/absproxy/3000/my-app-path`). ### Proxying to create a React app You must use `/absproxy/` with `create-react-app` (see -[#2565](https://github.com/cdr/code-server/issues/2565) and -[#2222](https://github.com/cdr/code-server/issues/2222) for more information). +[#2565](https://github.com/coder/code-server/issues/2565) and +[#2222](https://github.com/coder/code-server/issues/2222) for more information). You will need to inform `create-react-app` of the path at which you are serving via `$PUBLIC_URL` and webpack via `$WDS_SOCKET_PATH`: @@ -739,14 +383,16 @@ PUBLIC_URL=/absproxy/3000 \ BROWSER=none yarn start ``` -You should then be able to visit `https://my-code-server-address.io/absproxy/3000` to see your app exposed through -code-server! +You should then be able to visit +`https://my-code-server-address.io/absproxy/3000` to see your app exposed +through code-server. > We highly recommend using the subdomain approach instead to avoid this class of issue. ### Proxying to a Vue app -Similar to the situation with React apps, you have to make a few modifications to proxy a Vue app. +Similar to the situation with React apps, you have to make a few modifications +to proxy a Vue app. 1. add `vue.config.js` 2. update the values to match this (you can use any free port): @@ -765,92 +411,102 @@ module.exports = { Read more about `publicPath` in the [Vue.js docs](https://cli.vuejs.org/config/#publicpath) -## SSH into code-server on VS Code +### Proxying to an Angular app -[![SSH](https://img.shields.io/badge/SSH-363636?style=for-the-badge&logo=GNU+Bash&logoColor=ffffff)](https://ohmyz.sh/) [![Terminal](https://img.shields.io/badge/Terminal-2E2E2E?style=for-the-badge&logo=Windows+Terminal&logoColor=ffffff)](https://img.shields.io/badge/Terminal-2E2E2E?style=for-the-badge&logo=Windows+Terminal&logoColor=ffffff) [![Visual Studio Code](https://img.shields.io/badge/Visual_Studio_Code-007ACC?style=for-the-badge&logo=Visual+Studio+Code&logoColor=ffffff)](vscode:extension/ms-vscode-remote.remote-ssh) +In order to use code-server's built-in proxy with Angular, you need to make the +following changes in your app: -Follow these steps where code-server is running: +1. use `` in `src/index.html` +2. add `--serve-path /absproxy/4200` to `ng serve` in your `package.json` -1. Install `openssh-server`, `wget`, and `unzip`. +For additional context, see [this GitHub Discussion](https://github.com/coder/code-server/discussions/5439#discussioncomment-3371983). -```bash -# example for Debian and Ubuntu operating systems -sudo apt update -sudo apt install wget unzip openssh-server -``` +### Proxying to a Svelte app -2. Start the SSH server and set the password for your user, if you haven't already. If you use [deploy-code-server](https://github.com/cdr/deploy-code-server), +In order to use code-server's built-in proxy with Svelte, you need to make the +following changes in your app: -```bash -sudo service ssh start -sudo passwd {user} # replace user with your code-server user +1. Add `svelte.config.js` if you don't already have one +2. Update the values to match this (you can use any free port): + +```js +const config = { + kit: { + paths: { + base: "/absproxy/5173", + }, + }, +} ``` -### Option 1: cloudflared tunnel +3. Access app at `/absproxy/5173/` e.g. `http://localhost:8080/absproxy/5173/ -[![Cloudflared](https://img.shields.io/badge/Cloudflared-E4863B?style=for-the-badge&logo=cloudflare&logoColor=ffffff)](https://github.com/cloudflare/cloudflared) +For additional context, see [this Github Issue](https://github.com/sveltejs/kit/issues/2958) -1. Install [cloudflared](https://github.com/cloudflare/cloudflared#installing-cloudflared) on your local computer -2. Then go to `~/.ssh/config` and add the following: +### Prefixing `/absproxy/` with a path -```shell -Host *.trycloudflare.com -HostName %h -User root -Port 22 -ProxyCommand "cloudflared location" access ssh --hostname %h -``` +This is a case where you need to serve an application via `absproxy` as +explained above while serving code-server itself from a path other than the root +in your domain. + +For example: `http://my-code-server.com/user/123/workspace/my-app`. To achieve +this result: -3. Run `cloudflared tunnel --url ssh://localhost:22` on the remote server +1. Start code-server with the switch `--abs-proxy-base-path=/user/123/workspace` +2. Follow one of the instructions above for your framework. -4. Finally on VS Code or any IDE that supports SSH, run `ssh coder@https://your-link.trycloudflare.com` or `ssh coder@your-link.trycloudflare.com` +### Preflight requests -### Option 2: ngrok tunnel +By default, if you have auth enabled, code-server will authenticate all proxied +requests including preflight requests. This can cause issues because preflight +requests do not typically include credentials. To allow all preflight requests +through the proxy without authentication, use `--skip-auth-preflight`. -[![Ngrok](https://img.shields.io/badge/Ngrok-1F1E37?style=for-the-badge&logo=ngrok&logoColor=ffffff)](https://ngrok.com/) +## Internationalization and customization -1. Make a new account for ngrok [here](https://dashboard.ngrok.com/login) +code-server allows you to provide a JSON file to configure certain strings. This +can be used for both internationalization and customization. -2. Now, get the ngrok binary with `wget` and unzip it with `unzip`: +Create a JSON file with your custom strings: -```bash -wget "https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip" -unzip "ngrok-stable-linux-amd64.zip" +```json +{ + "WELCOME": "Welcome to {{app}}", + "LOGIN_TITLE": "{{app}} Access Portal", + "LOGIN_BELOW": "Please log in to continue", + "PASSWORD_PLACEHOLDER": "Enter Password" +} ``` -5. Then, go to [dashboard.ngrok.com](https://dashboard.ngrok.com) and go to the `Your Authtoken` section. -6. Copy the Authtoken shown there. -7. Now, go to the folder where you unzipped ngrok and store the Authtoken from the ngrok Dashboard. +Then reference the file: -```bash -./ngrok authtoken YOUR_AUTHTOKEN # replace YOUR_AUTHTOKEN with the ngrok authtoken. +```shell +code-server --i18n /path/to/custom-strings.json ``` -8. Now, forward port 22, which is the SSH port with this command: +Or this can be done in the config file: -```bash -./ngrok tcp 22 +```yaml +i18n: /path/to/custom-strings.json ``` -Now, you get a screen in the terminal like this: +You can combine this with the `--locale` flag to configure language support for +both code-server and VS Code in cases where code-server has no support but VS +Code does. If you are using this for internationalization, please consider +sending us a pull request to contribute it to `src/node/i18n/locales`. -```console -ngrok by @inconshreveable(Ctrl+C to quit) - -Session Status online -Account {Your name} (Plan: Free) -Version 2.3.40 -Region United States (us) -Web Interface http://127.0.0.1:4040 -Forwarding tcp://0.tcp.ngrok.io:19028 -> localhost:22 -``` +### Available keys and placeholders -In this case, copy the forwarded link `0.tcp.ngrok.io` and remember the port number `19028`. Type this on your local Visual Studio Code: +Refer to [../src/node/i18n/locales/en.json](../src/node/i18n/locales/en.json) +for a full list of the available keys for translations. Note that the only +placeholders supported for each key are the ones used in the default string. -```bash -ssh user@0.tcp.ngrok.io -p 19028 -``` +The `--app-name` flag controls the `{{app}}` placeholder in templates. If you +want to change the name, you can either: + +1. Set `--app-name` (potentially alongside `--i18n`) +2. Use `--i18n` and hardcode the name in your strings -The port redirects you to the default SSH port 22, and you can then successfully connect to code-server by entering the password you set for the user. +### Legacy flag -Note: the port and the url provided by ngrok will change each time you run it so modify as needed. +The `--welcome-text` flag is now deprecated. Use the `WELCOME` key instead. diff --git a/docs/helm.md b/docs/helm.md index 810cfa507d81..864a1940cbf4 100644 --- a/docs/helm.md +++ b/docs/helm.md @@ -1,8 +1,8 @@ # code-server Helm Chart -[![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square)](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) [![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)](https://img.shields.io/badge/Type-application-informational?style=flat-square) [![AppVersion: 3.12.0](https://img.shields.io/badge/AppVersion-3.12.0-informational?style=flat-square)](https://img.shields.io/badge/AppVersion-3.12.0-informational?style=flat-square) +[![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square)](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) [![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)](https://img.shields.io/badge/Type-application-informational?style=flat-square) [![AppVersion: 4.8.0](https://img.shields.io/badge/AppVersion-4.8.0-informational?style=flat-square)](https://img.shields.io/badge/AppVersion-4.8.0-informational?style=flat-square) -[code-server](https://github.com/cdr/code-server) code-server is VS Code running +[code-server](https://github.com/coder/code-server) code-server is VS Code running on a remote server, accessible through the browser. This chart is community maintained by [@Matthew-Beckett](https://github.com/Matthew-Beckett) and [@alexgorbatchev](https://github.com/alexgorbatchev) @@ -10,7 +10,7 @@ This chart is community maintained by [@Matthew-Beckett](https://github.com/Matt ## Quickstart ```console -$ git clone https://github.com/cdr/code-server +$ git clone https://github.com/coder/code-server $ cd code-server $ helm upgrade --install code-server ci/helm-chart ``` @@ -30,7 +30,7 @@ package manager. To install the chart with the release name `code-server`: ```console -$ git clone https://github.com/cdr/code-server +$ git clone https://github.com/coder/code-server $ cd code-server $ helm upgrade --install code-server ci/helm-chart ``` @@ -73,7 +73,7 @@ and their default values. | hostnameOverride | string | `""` | | image.pullPolicy | string | `"Always"` | | image.repository | string | `"codercom/code-server"` | -| image.tag | string | `"3.12.0"` | +| image.tag | string | `"4.8.0"` | | imagePullSecrets | list | `[]` | | ingress.enabled | bool | `false` | | nameOverride | string | `""` | @@ -105,7 +105,7 @@ $ helm upgrade --install code-server \ --set persistence.enabled=false ``` -The above command sets the the persistence storage to false. +The above command sets the persistence storage to false. Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, diff --git a/docs/install.md b/docs/install.md index e325fd6c43a5..edd256ba3072 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,14 +1,16 @@ + # Install - [install.sh](#installsh) - [Detection reference](#detection-reference) -- [yarn, npm](#yarn-npm) +- [npm](#npm) - [Standalone releases](#standalone-releases) - [Debian, Ubuntu](#debian-ubuntu) - [Fedora, CentOS, RHEL, SUSE](#fedora-centos-rhel-suse) - [Arch Linux](#arch-linux) +- [Artix Linux](#artix-linux) - [macOS](#macos) - [Docker](#docker) - [Helm](#helm) @@ -19,10 +21,11 @@ - [Uninstall](#uninstall) - [install.sh](#installsh-1) - [Homebrew](#homebrew) - - [yarn, npm](#yarn-npm-1) + - [npm](#npm-1) - [Debian, Ubuntu](#debian-ubuntu-1) + This document demonstrates how to install `code-server` on various distros and operating systems. @@ -30,7 +33,7 @@ operating systems. ## install.sh The easiest way to install code-server is to use our [install -script](../install.sh) for Linux, macOS and FreeBSD. The install script +script](https://github.com/coder/code-server/blob/main/install.sh) for Linux, macOS and FreeBSD. The install script [attempts to use the system package manager](#detection-reference) if possible. You can preview what occurs during the install process: @@ -56,6 +59,7 @@ following flags: - `--prefix=/usr/local`: install a standalone release archive system-wide. - `--version=X.X.X`: install version `X.X.X` instead of latest version. - `--help`: see usage docs. +- `--edge`: install the latest edge version (i.e. pre-release) When done, the install script prints out instructions for running and starting code-server. @@ -67,7 +71,7 @@ code-server. If you prefer to install code-server manually, despite the [detection references](#detection-reference) and `--dry-run` feature, then continue on for -information on how to do this. The [`install.sh`](../install.sh) script runs the +information on how to do this. The [`install.sh`](https://github.com/coder/code-server/blob/main/install.sh) script runs the _exact_ same commands presented in the rest of this document. ### Detection reference @@ -78,56 +82,51 @@ _exact_ same commands presented in the rest of this document. - For Arch Linux, code-server will install the AUR package. - For any unrecognized Linux operating system, code-server will install the latest standalone release into `~/.local`. - - Ensure that you add `~/.local/bin` to your `$PATH` to run code-server. - For macOS, code-server will install the Homebrew package (if you don't have Homebrew installed, code-server will install the latest standalone release into `~/.local`). - - Ensure that you add `~/.local/bin` to your `$PATH` to run code-server. -- For FreeBSD, code-server will install the [npm package](#yarn-npm) with `yarn` - or `npm`. +- For FreeBSD, code-server will install the [npm package](#npm) with `npm` - If you're installing code-server onto architecture with no releases, - code-server will install the [npm package](#yarn-npm) with `yarn` or `npm` + code-server will install the [npm package](#npm) with `npm` - We currently offer releases for amd64 and arm64. - - The [npm package](#yarn-npm) builds the native modules on post-install. + - The [npm package](#npm) builds the native modules on post-install. -## yarn, npm +## npm -We recommend installing with `yarn` or `npm` when: +We recommend installing with `npm` when: 1. You aren't using a machine with `amd64` or `arm64`. -1. You are installing code-server on Windows -1. You're on Linux with `glibc` < v2.17, `glibcxx` < v3.4.18 on `amd64`, `glibc` - < v2.23, or `glibcxx` < v3.4.21 on `arm64`. -1. You're running Alpine Linux or are using a non-glibc libc. See - [#1430](https://github.com/cdr/code-server/issues/1430#issuecomment-629883198) +2. You're on Linux with `glibc` < v2.28 or `glibcxx` < v3.4.21. +3. You're running Alpine Linux or are using a non-glibc libc. See + [#1430](https://github.com/coder/code-server/issues/1430#issuecomment-629883198) for more information. -Installing code-server with `yarn` or `npm` builds native modules on install. +Installing code-server with `npm` builds native modules on install. -This process requires C dependencies; see our guide on [installing with yarn and npm][./npm.md](./npm.md) for more information. +This process requires C dependencies; see our guide on [installing with npm](./npm.md) for more information. ## Standalone releases We publish self-contained `.tar.gz` archives for every release on -[GitHub](https://github.com/cdr/code-server/releases). The archives bundle the +[GitHub](https://github.com/coder/code-server/releases). The archives bundle the node binary and node modules. -We create the standalone releases using the [npm package](#yarn-npm), and we +We create the standalone releases using the [npm package](#npm), and we then create the remaining releases using the standalone version. -The only requirement to use the standalone release is `glibc` >= 2.17 and -`glibcxx` >= v3.4.18 on Linux (for macOS, there is no minimum system +The only requirement to use the standalone release is `glibc` >= 2.28 and +`glibcxx` >= v3.4.21 on Linux (for macOS, there is no minimum system requirement). To use a standalone release: 1. Download the latest release archive for your system from - [GitHub](https://github.com/cdr/code-server/releases). + [GitHub](https://github.com/coder/code-server/releases). 2. Unpack the release. 3. Run code-server by executing `./bin/code-server`. @@ -139,7 +138,7 @@ release on Linux: ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v$VERSION/code-server-$VERSION-linux-amd64.tar.gz \ +curl -fL https://github.com/coder/code-server/releases/download/v$VERSION/code-server-$VERSION-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz mv ~/.local/lib/code-server-$VERSION-linux-amd64 ~/.local/lib/code-server-$VERSION ln -s ~/.local/lib/code-server-$VERSION/bin/code-server ~/.local/bin/code-server @@ -151,11 +150,11 @@ code-server ## Debian, Ubuntu > The standalone arm64 .deb does not support Ubuntu 16.04 or earlier. Please -> upgrade or [build with yarn](#yarn-npm). +> upgrade or [build with npm](#npm). ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v$VERSION/code-server_$VERSION_amd64.deb -sudo dpkg -i code-server_$VERSION_amd64.deb +curl -fOL https://github.com/coder/code-server/releases/download/v$VERSION/code-server_${VERSION}_amd64.deb +sudo dpkg -i code-server_${VERSION}_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -163,10 +162,10 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE > The standalone arm64 .rpm does not support CentOS 7. Please upgrade or [build -> with yarn](#yarn-npm). +> with npm](#npm). ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v$VERSION/code-server-$VERSION-amd64.rpm +curl -fOL https://github.com/coder/code-server/releases/download/v$VERSION/code-server-$VERSION-amd64.rpm sudo rpm -i code-server-$VERSION-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml @@ -190,6 +189,72 @@ sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` +## Artix Linux + +```bash +# Install code-server from the AUR +git clone https://aur.archlinux.org/code-server.git +cd code-server +makepkg -si +``` + +Save the file as `code-server` in `/etc/init.d/` and make it executable with `chmod +x code-server`. Put your username in line 3. + +```bash +#!/sbin/openrc-run +name=$RC_SVCNAME +description="$name - VS Code on a remote server" +user="" # your username here +homedir="/home/$user" +command="$(which code-server)" +# Just because you can do this does not mean you should. Use ~/.config/code-server/config.yaml instead +#command_args="--extensions-dir $homedir/.local/share/$name/extensions --user-data-dir $homedir/.local/share/$name --disable-telemetry" +command_user="$user:$user" +pidfile="/run/$name/$name.pid" +command_background="yes" +extra_commands="report" + +depend() { + use logger dns + need net +} + +start_pre() { + checkpath --directory --owner $command_user --mode 0755 /run/$name /var/log/$name +} + +start() { + default_start + report +} + +stop() { + default_stop +} + +status() { + default_status + report +} + +report() { + # Report to the user + einfo "Reading configuration from ~/.config/code-server/config.yaml" +} +``` + +Start on boot with default runlevel + +``` +rc-update add code-server default +``` + +Start the service immediately + +``` +rc-service code-server start +``` + ## macOS ```bash @@ -211,6 +276,7 @@ brew services start code-server # outside the container. mkdir -p ~/.config docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$HOME/.local:/home/coder/.local" \ -v "$HOME/.config:/home/coder/.config" \ -v "$PWD:/home/coder/project" \ -u "$(id -u):$(id -g)" \ @@ -228,14 +294,14 @@ You can install code-server using the [Helm package manager](https://coder.com/d ## Windows -We currently [do not publish Windows releases](https://github.com/cdr/code-server/issues/1397). We recommend installing code-server onto Windows with [`yarn` or `npm`](#yarn-npm). - -> Note: You will also need to [build cdr/cloud-agent manually](https://github.com/cdr/cloud-agent/issues/17) if you would like to use `code-server --link` on Windows. +We currently [do not publish Windows +releases](https://github.com/coder/code-server/issues/1397). ## Raspberry Pi -We recommend installing code-server onto Raspberry Pi with [`yarn` or -`npm`](#yarn-npm). +We recommend installing code-server onto Raspberry Pi with [`npm`](#npm). + +If you see an error related to `node-gyp` during installation, See [#5174](https://github.com/coder/code-server/issues/5174) for more information. ## Termux @@ -245,7 +311,7 @@ information. ## Cloud providers We maintain [one-click apps and install scripts for cloud -providers](https://github.com/cdr/deploy-code-server) such as DigitalOcean, +providers](https://github.com/coder/deploy-code-server) such as DigitalOcean, Railway, Heroku, and Azure. ## Uninstall @@ -277,18 +343,12 @@ brew remove code-server brew uninstall code-server ``` -### yarn, npm +### npm To remove the code-server global module, run: ```shell -yarn global remove code-server -``` - -or - -```shell -npm uninstall -g code-server +npm uninstall --global code-server ``` ### Debian, Ubuntu diff --git a/docs/ios.md b/docs/ios.md new file mode 100644 index 000000000000..fc484e19b0dd --- /dev/null +++ b/docs/ios.md @@ -0,0 +1,9 @@ +# Using code-server on iOS with iSH + +1. Install iSH from the [App Store](https://apps.apple.com/us/app/ish-shell/id1436902243) +2. Install `curl` and `nano` with `apk add curl nano` +3. Configure iSH to use an earlier version of NodeJS with `nano /etc/apk/repositories` and edit `v3.14` to `v3.12` on both repository links. +4. Install `nodejs` and `npm` with `apk add nodejs npm` +5. Install code-server with `curl -fsSL https://code-server.dev/install.sh | sh` +6. Run code-server with `code-server` +7. Access on localhost:8080 in your browser diff --git a/docs/ipad.md b/docs/ipad.md index a66bf2e0b894..ef951cc4e6fe 100644 --- a/docs/ipad.md +++ b/docs/ipad.md @@ -1,3 +1,4 @@ + # iPad @@ -13,6 +14,7 @@ - [Sharing a self-signed certificate with an iPad](#sharing-a-self-signed-certificate-with-an-ipad) + Once you've installed code-server, you can access it from an iPad. @@ -104,25 +106,26 @@ and tricks helpful: process](#access-code-server-with-a-self-signed-certificate-on-an-ipad) - Keyboard issues: - The keyboard disappear sometimes - [#979](https://github.com/cdr/code-server/issues/979) + [#979](https://github.com/coder/code-server/issues/979) - Some expectations regarding shortcuts may not be met: - `cmd + n` opens new browser window instead of new file, and it's difficult to set alternative as a workaround - In general, expect to edit your keyboard shortcuts - There's no escape key by default on the Magic Keyboard, so most users set the globe key to be an escape key -- Trackpad scrolling does not work on iPadOS < 145 - ([#1455](https://github.com/cdr/code-server/issues/1455)) +- Trackpad scrolling does not work on iPadOS < 14.5 + ([#1455](https://github.com/coder/code-server/issues/1455)) - [WebKit fix](https://bugs.webkit.org/show_bug.cgi?id=210071#c13) -- Keyboard may lose focus in Safari / split view [#4182](https://github.com/cdr/code-server/issues/4182) -- Terminal text does not appear by default [#3824](https://github.com/cdr/code-server/issues/3824) +- Keyboard may lose focus in Safari / split view [#4182](https://github.com/coder/code-server/issues/4182) +- Terminal text does not appear by default [#3824](https://github.com/coder/code-server/issues/3824) +- Copy & paste in terminal does not work well with keyboard shortcuts [#3491](https://github.com/coder/code-server/issues/3491) - `ctrl+c` does not stop a long-running process in the browser - Tracking upstream issue here: [#114009](https://github.com/microsoft/vscode/issues/114009) - See [workaround](#ctrl-c-workaround) -Additionally, see [issues in the code-server repo that are tagged with the iPad -label](https://github.com/cdr/code-server/issues?q=is%3Aopen+is%3Aissue+label%3AiPad) +Additionally, see [issues in the code-server repo that are tagged with the `os-ios` +label](https://github.com/coder/code-server/issues?q=is%3Aopen+is%3Aissue+label%3Aos-ios) for more information. ### Workaround for issue with `ctrl+c` not stopping a running process in the terminal diff --git a/docs/link.md b/docs/link.md deleted file mode 100644 index 7b21244c5ded..000000000000 --- a/docs/link.md +++ /dev/null @@ -1,9 +0,0 @@ -# code-server --link - -Run code-server with the beta flag `--link` and you'll get TLS, authentication, and a dedicated URL -for accessing your IDE out of the box. - -```console -$ code-server --link -Proxying code-server, you can access your IDE at https://example.cdr.co -``` diff --git a/docs/manifest.json b/docs/manifest.json index e85eebf33871..4e82dcd7d713 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,27 +1,27 @@ { - "versions": ["v3.12.0"], + "versions": ["v4.8.0"], "routes": [ { "title": "Home", "description": "Learn how to install and run code-server.", "path": "./README.md", - "icon": "" + "icon_path": "assets/images/icons/home.svg" }, { "title": "Requirements", "description": "Learn about what you need to run code-server.", - "icon": "", + "icon_path": "assets/images/icons/requirements.svg", "path": "./requirements.md" }, { "title": "Install", "description": "How to install code-server.", - "icon": "", + "icon_path": "assets/images/icons/wrench.svg", "path": "./install.md", "children": [ { "title": "npm", - "description": "How to install code-server using npm or yarn", + "description": "How to install code-server using npm", "path": "./npm.md" }, { @@ -34,13 +34,13 @@ { "title": "Usage", "description": "How to set up and use code-server.", - "icon": "", + "icon_path": "assets/images/icons/usage.svg", "path": "./guide.md", "children": [ { - "title": "--link", - "description": "How to run code-server --link", - "path": "./link.md" + "title": "Coder", + "description": "How to run code-server in Coder", + "path": "./coder.md" }, { "title": "iPad", @@ -51,31 +51,41 @@ "title": "Termux", "description": "How to install Termux to run code-server on an Android device.", "path": "./termux.md" + }, + { + "title": "iOS", + "description": "How to use code-server on iOS with iSH.", + "path": "./ios.md" + }, + { + "title": "Android", + "description": "How to run code-server on an Android device using UserLAnd.", + "path": "./android.md" } ] }, { "title": "Collaboration", "description": "How to setup real time collaboration using code server.", - "icon": " ", + "icon_path": "assets/images/icons/collab.svg", "path": "./collaboration.md" }, { "title": "Upgrade", "description": "How to upgrade code-server.", - "icon": "", + "icon_path": "assets/images/icons/upgrade.svg", "path": "./upgrade.md" }, { "title": "FAQ", "description": "Frequently asked questions on installing and running code-server.", - "icon": "", + "icon_path": "assets/images/icons/faq.svg", "path": "./FAQ.md" }, { "title": "Contributing", "description": "How to contribute to code-server.", - "icon": "", + "icon_path": "assets/images/icons/contributing.svg", "path": "./CONTRIBUTING.md", "children": [ { diff --git a/docs/npm.md b/docs/npm.md index f02cec569cba..1ad36a0e120d 100644 --- a/docs/npm.md +++ b/docs/npm.md @@ -1,3 +1,4 @@ + # npm Install Requirements @@ -15,19 +16,24 @@ - [Debugging install issues with npm](#debugging-install-issues-with-npm) + If you're installing code-server via `npm`, you'll need to install additional dependencies required to build the native modules used by VS Code. This article includes installing instructions based on your operating system. +> **WARNING**: Do not use `yarn` to install code-server. Unlike `npm`, it does not respect +> lockfiles for distributed applications. It will instead use the latest version +> available at installation time - which might not be the one used for a given +> code-server release, and [might lead to unexpected behavior](https://github.com/coder/code-server/issues/4927). + ## Node.js version -We use the same major version of Node.js shipped with VSCode's Electron, -which is currently `14.x`. VS Code also [lists Node.js +We use Node.js `24.x`. VS Code also [lists Node.js requirements](https://github.com/microsoft/vscode/wiki/How-to-Contribute#prerequisites). Using other versions of Node.js [may lead to unexpected -behavior](https://github.com/cdr/code-server/issues/1633). +behavior](https://github.com/coder/code-server/issues/1633). ## Ubuntu, Debian @@ -55,8 +61,7 @@ Proceed to [installing](#installing) ## Alpine ```bash -apk add alpine-sdk bash libstdc++ libc6-compat -npm config set python python3 +apk add alpine-sdk bash libstdc++ libc6-compat python3 krb5-dev ``` Proceed to [installing](#installing) @@ -72,7 +77,7 @@ Proceed to [installing](#installing) ## FreeBSD ```sh -pkg install -y git python npm-node14 yarn-node14 pkgconf +pkg install -y git python npm-node24 pkgconf pkg install -y libinotify ``` @@ -85,30 +90,27 @@ Installing code-server requires all of the [prerequisites for VS Code developmen Next, install code-server with: ```bash -yarn global add code-server -# Or: npm install -g code-server +npm install --global code-server code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` -A `postinstall.sh` script will attempt to run. Select your terminal (e.g., Git bash) as the default application for `.sh` files. If an additional dialog does not appear, run the install command again. +A `postinstall.sh` script will attempt to run. Select your terminal (e.g., Git bash) as the default shell for npm run-scripts. If an additional dialog does not appear, run the install command again. If the `code-server` command is not found, you'll need to [add a directory to your PATH](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/). To find the directory, use the following command: ```shell -yarn global bin -# Or: npm config get prefix +npm config get prefix ``` -For help and additional troubleshooting, see [#1397](https://github.com/cdr/code-server/issues/1397). +For help and additional troubleshooting, see [#1397](https://github.com/coder/code-server/issues/1397). ## Installing After adding the dependencies for your OS, install the code-server package globally: ```bash -yarn global add code-server -# Or: npm install -g code-server +npm install --global code-server code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -116,13 +118,13 @@ code-server ## Troubleshooting If you need further assistance, post on our [GitHub Discussions -page](https://github.com/cdr/code-server/discussions). +page](https://github.com/coder/code-server/discussions). ### Issues with Node.js after version upgrades Occasionally, you may run into issues with Node.js. -If you install code-server using `yarn` or `npm`, and you upgrade your Node.js +If you install code-server using `npm`, and you upgrade your Node.js version, you may need to reinstall code-server to recompile native modules. Sometimes, you can get around this by navigating into code-server's `lib/vscode` directory and running `npm rebuild` to recompile the modules. @@ -136,12 +138,12 @@ A step-by-step example of how you might do this is: ### Debugging install issues with npm -`yarn` suppresses logs when running `yarn global add`, so to debug installation issues, install with `npm` instead: +To debug installation issues, install with `npm`: ```shell # Uninstall -npm uninstall -g --unsafe-perm code-server > /dev/null 2>&1 +npm uninstall --global code-server > /dev/null 2>&1 # Install with logging -npm install --loglevel verbose -g --unsafe-perm code-server +npm install --loglevel verbose --global code-server ``` diff --git a/docs/requirements.md b/docs/requirements.md index 128e18fd6b88..835fe5754bd1 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -21,8 +21,8 @@ for communication between the browser and the server. The following steps walk you through setting up a VM running Debian using Google Cloud (though you are welcome to use any machine or VM provider). -If you're [signing up with Google](https://console.cloud.google.com/getting-started) for the first time, you should get a 12-month trial with -$300 of credits. +If you're [signing up with Google](https://console.cloud.google.com/getting-started) for the first time, you should get a 3-month trial with +\$300 of credits. After you sign up and create a new Google Cloud Provider (GCP) project, create a new Compute Engine VM instance: @@ -33,7 +33,7 @@ new Compute Engine VM instance: 4. Choose the **region** that's closest to you based on [GCP ping](https://gcping.com/). 5. Choose a **zone** (any option is fine). -6. We recommend choose an **E2 series instance** from the [general-purpose +6. We recommend choosing an **E2 series instance** from the [general-purpose family](https://cloud.google.com/compute/docs/machine-types#general_purpose). 7. Change the instance type to **custom** and set at least **2 cores** and **2 GB of RAM**. You can add more resources if desired, though you can also edit diff --git a/docs/termux.md b/docs/termux.md index 454b585e6e74..a13ce86116ca 100644 --- a/docs/termux.md +++ b/docs/termux.md @@ -1,71 +1,203 @@ + # Termux - [Install](#install) +- [NPM Installation](#npm-installation) - [Upgrade](#upgrade) - [Known Issues](#known-issues) - - [Search doesn't work](#search-doesnt-work) - - [Backspace doesn't work](#backspace-doesnt-work) + - [Git won't work in `/sdcard`](#git-wont-work-in-sdcard) + - [Many extensions including language packs fail to install](#many-extensions-including-language-packs-fail-to-install) +- [Extra](#extra) + - [Keyboard Shortcuts and Tab Key](#keyboard-shortcuts-and-tab-key) + - [Create a new user](#create-a-new-user) + - [Install Go](#install-go) + - [Install Python](#install-python) - -Termux is a terminal application and Linux environment that you can also use to -run code-server from your Android phone. + ## Install -1. Install Termux from [F-Droid](https://f-droid.org/en/packages/com.termux/). -1. Make sure it's up-to-date: `apt update && apt upgrade` -1. Install required packages: `apt install build-essential python git nodejs yarn` -1. Install code-server: `yarn global add code-server` -1. Run code-server: `code-server` and navigate to localhost:8080 in your browser +1. Get [Termux](https://f-droid.org/en/packages/com.termux/) from **F-Droid**. +2. Run `pkg install tur-repo` +3. Run `pkg install code-server` +4. You can now start code server by simply running `code-server`. + +## NPM Installation + +1. Get [Termux](https://f-droid.org/en/packages/com.termux/) from **F-Droid**. + +2. We will now change using the following command. + +```sh +termux-change-repo +``` + +Now select `Main Repository` then change repo to `Mirrors by Grimler Hosted on grimler.se`. + +3. After successfully updating of repository update and upgrade all the packages by the following command + +```sh +pkg update +pkg upgrade -y +``` + +4. Now let's install requirement dependancy. + +```sh +pkg install -y \ + build-essential \ + binutils \ + pkg-config \ + python3 \ + nodejs-lts +npm config set python python3 +node -v +``` + +you will get Node version `v24` + +5. Now install code-server following our guide on [installing with npm](./npm.md) + +6. Congratulation code-server is installed on your device using the following command. + +```sh +code-server --auth none +``` + +7. If already installed then use the following command for upgradation. + +``` +npm update --global code-server +``` ## Upgrade -To upgrade run: `yarn global upgrade code-server --latest` +1. Remove all previous installs `rm -rf ~/.local/lib/code-server-*` +2. Run the install script again `curl -fsSL https://code-server.dev/install.sh | sh` ## Known Issues -The following details known issues and suggested workarounds for using -code-server with Termux. +### Git won't work in `/sdcard` + +Issue : Using git in the `/sdcard` directory will fail during cloning/commit/staging/etc...\ +Fix : None\ +Potential Workaround : + +1. Create a soft-link from the debian-fs to your folder in `/sdcard` +2. Use git from termux (preferred) + +### Many extensions including language packs fail to install + +Issue: Android is not seen as a Linux environment but as a separate, unsupported platform, so code-server only allows [Web Extensions](https://code.visualstudio.com/api/extension-guides/web-extensions), refusing to download extensions that run on the server.\ +Fix: None\ +Potential workarounds : + +Either + +- Manually download extensions as `.vsix` file and install them via `Extensions: Install from VSIX...` in the Command Palette. + +- Use an override to pretend the platform is Linux: + +Create a JS script that patches `process.platform`: + +```js +// android-as-linux.js +Object.defineProperty(process, "platform", { + get() { + return "linux" + }, +}) +``` + +Then use Node's `--require` option to make sure it is loaded before `code-server` starts: + +```sh +NODE_OPTIONS="--require /path/to/android-as-linux.js" code-server +``` + +⚠️ Note that Android and Linux are not 100% compatible, so use these workarounds at your own risk. Extensions that have native dependencies other than Node or that directly interact with the OS might cause issues. + +## Extra + +### Keyboard Shortcuts and Tab Key + +In order to support the tab key and use keyboard shortcuts, add this to your +settings.json: + +```json +{ + "keyboard.dispatch": "keyCode" +} +``` + +### Create a new user + +To create a new user follow these simple steps - + +1. Create a new user by running `useradd -m`. +2. Change the password by running `passwd `. +3. Give your new user sudo access by running `visudo`, scroll down to `User privilege specification` and add the following line after root `username ALL=(ALL:ALL) ALL`. +4. Now edit the `/etc/passwd` file with your command line editor of choice and at the end of the line that specifies your user change `/bin/sh` to `/bin/bash`. +5. Now switch users by running `su - ` + +- Remember the `-` betweeen `su` and username is required to execute `/etc/profile`,\ + since `/etc/profile` may have some necessary things to be executed you should always add a `-`. + +### Install Go -### Search doesn't work +> From https://golang.org/doc/install -There is a known issue with search not working on Android because it's missing -`bin/rg` ([context](https://github.com/cdr/code-server/issues/1730#issuecomment-721515979)). To fix this: +1. Go to https://golang.org/dl/ and copy the download link for `linux arm` and run the following: -1. Install `ripgrep` with `pkg` +```bash +wget download_link +``` - ```sh - pkg install ripgrep - ``` +2. Extract the downloaded archive. (This step will erase all previous GO installs, make sure to create a backup if you have previously installed GO) -1. Make a soft link using `ln -s` +```bash +rm -rf /usr/local/go && tar -C /usr/local -xzf archive_name +``` - ```sh - # run this command inside the code-server directory - ln -s $PREFIX/bin/rg ./vendor/modules/code-oss-dev/vscode-ripgrep/bin/rg - ``` +3. Run `nano /etc/profile` and add the following line `export PATH=$PATH:/usr/local/go/bin`. +4. Now run `exit` (depending on if you have switched users or not, you may have to run `exit` multiple times to get to normal termux shell) and start Debian again. +5. Check if your install was successful by running `go version` -### Backspace doesn't work +### Install Python -When using Android's on-screen keyboard, the backspace key doesn't work -properly. This is a known upstream issue: +> Run these commands as root -- [Issues with backspace in Codespaces on Android (Surface Duo)](https://github.com/microsoft/vscode/issues/107602) -- [Support mobile platforms](https://github.com/xtermjs/xterm.js/issues/1101) +1. Run the following commands to install required packages to build python: -There are two workarounds. +```bash +sudo apt-get update +sudo apt-get install make build-essential libssl-dev zlib1g-dev \ + libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \ + libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev +``` -**Option 1:** Modify keyboard dispatch settings +2. Install [pyenv](https://github.com/pyenv/pyenv/) from [pyenv-installer](https://github.com/pyenv/pyenv-installer) by running: -1. Open the Command Palette -2. Search for **Preferences: Open Settings (JSON)** -3. Add `"keyboard.dispatch": "keyCode"` +```bash +curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash +``` -The backspace button should work at this point. +3. Run `nano /etc/profile` and add the following: -_Thanks to @Nefomemes for the [suggestion](https://github.com/cdr/code-server/issues/1141#issuecomment-789463707)!_ +```bash +export PYENV_ROOT="/root/.pyenv" +export PATH="/root/.pyenv/bin:$PATH" +eval "$(pyenv init --path)" +eval "$(pyenv virtualenv-init -)" +``` -**Option 2:** Use a Bluetooth keyboard. +4. Exit and start Debian again. +5. Run `pyenv versions` to list all installable versions. +6. Run `pyenv install version` to install the desired python version. + > The build process may take some time (an hour or 2 depending on your device). +7. Run `touch /root/.pyenv/version && echo "your_version_here" > /root/.pyenv/version` +8. (You may have to start Debian again) Run `python3 -V` to verify if PATH works or not. + > If `python3` doesn't work but pyenv says that the install was successful in step 6 then try running `$PYENV_ROOT/versions/your_version/bin/python3`. diff --git a/docs/triage.md b/docs/triage.md index 5717c5eb2adc..6d4120f97209 100644 --- a/docs/triage.md +++ b/docs/triage.md @@ -28,8 +28,8 @@ This will show issues that: 1. If more information is required, please ask the submitter and tag as `waiting-for-info` and wait. 1. Finally, the issue should be moved into the - [code-server](https://github.com/cdr/code-server/projects/1) project where we + [code-server](https://github.com/coder/code-server/projects/1) project where we pick out issues to fix and track their progress. -We also use [milestones](https://github.com/cdr/code-server/milestones) to track +We also use [milestones](https://github.com/coder/code-server/milestones) to track what issues are planned/or were closed for what release. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000000..b13ebc0dc4de --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,84 @@ +import { fixupConfigRules } from "@eslint/compat" +import globals from "globals" +import tsParser from "@typescript-eslint/parser" +import path from "node:path" +import { fileURLToPath } from "node:url" +import js from "@eslint/js" +import { FlatCompat } from "@eslint/eslintrc" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export default [ + ...fixupConfigRules( + compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + "plugin:prettier/recommended", + "prettier", + ), + ), + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.jest, + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 2018, + sourceType: "module", + }, + + settings: { + "import/resolver": { + typescript: { + alwaysTryTypes: true, + }, + }, + }, + + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "none", + }, + ], + + "no-dupe-class-members": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-extra-semi": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-unused-vars": "off", // TODO: Fix these. + "@typescript-eslint/no-empty-object-type": "off", + eqeqeq: "error", + + "import/order": [ + "error", + { + alphabetize: { + order: "asc", + }, + + groups: [["builtin", "external", "internal"], "parent", "sibling"], + }, + ], + + "no-async-promise-executor": "off", + }, + }, +] diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000000..948f73585131 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1739303263, + "narHash": "sha256-c/Z/6gZLN8BIpYh1B3qMzEn0TArjf4F2lmy59lDLVBM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6cc4213488e886db863878a1e3dc26cc932d38b8", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000000..3a1f7efbc122 --- /dev/null +++ b/flake.nix @@ -0,0 +1,37 @@ +{ + description = "code-server"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable-small"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem + (system: + let pkgs = nixpkgs.legacyPackages.${system}; + nodejs = pkgs.nodejs_22; + in { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + imagemagick + nodejs + python3 + pkg-config + git + rsync + jq + moreutils + quilt + bats + openssl + ]; + buildInputs = with pkgs; (lib.optionals (!stdenv.isDarwin) [ libsecret libkrb5 ] + ++ (with xorg; [ libX11 libxkbfile ]) + ++ lib.optionals stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [ + AppKit Cocoa CoreServices Security xcbuild + ])); + }; + } + ); +} diff --git a/install.sh b/install.sh index 61dff00df665..81c1ce68edcd 100755 --- a/install.sh +++ b/install.sh @@ -23,7 +23,7 @@ The remote host must have internet access. ${not_curl_usage-} Usage: - $arg0 [--dry-run] [--version X.X.X] [--method detect] \ + $arg0 [--dry-run] [--version X.X.X] [--edge] [--method detect] \ [--prefix ~/.local] [--rsh ssh] [user@host] --dry-run @@ -32,6 +32,9 @@ Usage: --version X.X.X Install a specific version instead of the latest. + --edge + Install the latest edge version instead of the latest stable version. + --method [detect | standalone] Choose the installation method. Defaults to detect. - detect detects the system package manager and tries to use it. @@ -43,7 +46,7 @@ Usage: Sets the prefix used by standalone release archives. Defaults to ~/.local The release is unarchived into ~/.local/lib/code-server-X.X.X and the binary symlinked into ~/.local/bin/code-server - To install system wide pass ---prefix=/usr/local + To install system wide pass --prefix=/usr/local --rsh Specifies the remote shell for remote installation. Defaults to ssh. @@ -52,7 +55,7 @@ The detection method works as follows: - Debian, Ubuntu, Raspbian: install the deb package from GitHub. - Fedora, CentOS, RHEL, openSUSE: install the rpm package from GitHub. - Arch Linux: install from the AUR (which pulls releases from GitHub). - - FreeBSD, Alpine: install from yarn/npm. + - FreeBSD, Alpine: install from npm. - macOS: install using Homebrew if installed otherwise install from GitHub. - All others: install the release from GitHub. @@ -71,9 +74,13 @@ EOF } echo_latest_version() { - # https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c#gistcomment-2758860 - version="$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/cdr/code-server/releases/latest)" - version="${version#https://github.com/cdr/code-server/releases/tag/}" + if [ "${EDGE-}" ]; then + version="$(curl -fsSL https://api.github.com/repos/coder/code-server/releases | awk 'match($0,/.*"html_url": "(.*\/releases\/tag\/.*)".*/)' | head -n 1 | awk -F '"' '{print $4}')" + else + # https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c#gistcomment-2758860 + version="$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/coder/code-server/releases/latest)" + fi + version="${version#https://github.com/coder/code-server/releases/tag/}" version="${version#v}" echo "$version" } @@ -124,6 +131,11 @@ Or, if you don't want/need a background service you can run: EOF } +echo_coder_postinstall() { + echoh + echoh "Deploy code-server for your team with Coder: https://github.com/coder/coder" +} + main() { if [ "${TRACE-}" ]; then set -x @@ -135,6 +147,7 @@ main() { OPTIONAL \ ALL_FLAGS \ RSH_ARGS \ + EDGE \ RSH ALL_FLAGS="" @@ -170,6 +183,9 @@ main() { --version=*) VERSION="$(parse_arg "$@")" ;; + --edge) + EDGE=1 + ;; --rsh) RSH="$(parse_arg "$@")" shift @@ -232,6 +248,7 @@ main() { if [ "$METHOD" = standalone ]; then if has_standalone; then install_standalone + echo_coder_postinstall exit 0 else echoerr "There are no standalone releases for $ARCH" @@ -275,6 +292,8 @@ main() { npm_fallback install_standalone ;; esac + + echo_coder_postinstall } parse_arg() { @@ -340,7 +359,7 @@ install_deb() { echoh "Installing v$VERSION of the $ARCH deb package from GitHub." echoh - fetch "https://github.com/cdr/code-server/releases/download/v$VERSION/code-server_${VERSION}_$ARCH.deb" \ + fetch "https://github.com/coder/code-server/releases/download/v$VERSION/code-server_${VERSION}_$ARCH.deb" \ "$CACHE_DIR/code-server_${VERSION}_$ARCH.deb" sudo_sh_c dpkg -i "$CACHE_DIR/code-server_${VERSION}_$ARCH.deb" @@ -351,9 +370,9 @@ install_rpm() { echoh "Installing v$VERSION of the $ARCH rpm package from GitHub." echoh - fetch "https://github.com/cdr/code-server/releases/download/v$VERSION/code-server-$VERSION-$ARCH.rpm" \ + fetch "https://github.com/coder/code-server/releases/download/v$VERSION/code-server-$VERSION-$ARCH.rpm" \ "$CACHE_DIR/code-server-$VERSION-$ARCH.rpm" - sudo_sh_c rpm -i "$CACHE_DIR/code-server-$VERSION-$ARCH.rpm" + sudo_sh_c rpm -U "$CACHE_DIR/code-server-$VERSION-$ARCH.rpm" echo_systemd_postinstall rpm } @@ -368,7 +387,7 @@ install_aur() { if [ ! "${DRY_RUN-}" ]; then cd "$CACHE_DIR/code-server-aur" fi - sh_c makepkg -si + sh_c makepkg -si --noconfirm echo_systemd_postinstall AUR } @@ -377,7 +396,7 @@ install_standalone() { echoh "Installing v$VERSION of the $ARCH release from GitHub." echoh - fetch "https://github.com/cdr/code-server/releases/download/v$VERSION/code-server-$VERSION-$OS-$ARCH.tar.gz" \ + fetch "https://github.com/coder/code-server/releases/download/v$VERSION/code-server-$VERSION-$OS-$ARCH.tar.gz" \ "$CACHE_DIR/code-server-$VERSION-$OS-$ARCH.tar.gz" # -w only works if the directory exists so try creating it first. If this @@ -405,35 +424,25 @@ install_standalone() { } install_npm() { - echoh "Installing latest from npm." + echoh "Installing v$VERSION from npm." echoh - YARN_PATH="${YARN_PATH-yarn}" NPM_PATH="${YARN_PATH-npm}" - if command_exists "$YARN_PATH"; then - sh_c="sh_c" - if [ ! "${DRY_RUN-}" ] && [ ! -w "$($YARN_PATH global bin)" ]; then - sh_c="sudo_sh_c" - fi - echoh "Installing with yarn." - echoh - "$sh_c" "$YARN_PATH" global add code-server --unsafe-perm - NPM_BIN_DIR="\$($YARN_PATH global bin)" echo_npm_postinstall - return - elif command_exists "$NPM_PATH"; then + + if command_exists "$NPM_PATH"; then sh_c="sh_c" if [ ! "${DRY_RUN-}" ] && [ ! -w "$(NPM_PATH config get prefix)" ]; then sh_c="sudo_sh_c" fi echoh "Installing with npm." echoh - "$sh_c" "$NPM_PATH" install -g code-server --unsafe-perm + "$sh_c" "$NPM_PATH" install -g "code-server@$VERSION" --unsafe-perm NPM_BIN_DIR="\$($NPM_PATH bin -g)" echo_npm_postinstall return fi - echoerr "Please install npm or yarn to install code-server!" - echoerr "You will need at least node v12 and a few C dependencies." - echoerr "See the docs https://coder.com/docs/code-server/latest/install#yarn-npm" + echoerr "Please install npm to install code-server!" + echoerr "You will need at least node v20 and a few C dependencies." + echoerr "See the docs https://coder.com/docs/code-server/latest/install#npm" exit 1 } @@ -452,9 +461,9 @@ npm_fallback() { # Determine if we have standalone releases on GitHub for the system's arch. has_standalone() { case $ARCH in - amd64) return 0 ;; - # We only have amd64 for macOS. - arm64) + arm64) return 0 ;; + # We only have arm64 for macOS. + amd64) [ "$(distro)" != macos ] return ;; @@ -481,7 +490,7 @@ os() { # - amzn, centos, rhel, fedora, ... -> fedora # - opensuse-{leap,tumbleweed} -> opensuse # - alpine -> alpine -# - arch -> arch +# - arch, manjaro, endeavouros, ... -> arch # # Inspired by https://github.com/docker/docker-install/blob/26ff363bcf3b3f5a00498ac43694bf1c7d9ce16c/install.sh#L111-L120. distro() { @@ -495,7 +504,7 @@ distro() { . /etc/os-release if [ "${ID_LIKE-}" ]; then for id_like in $ID_LIKE; do - case "$id_like" in debian | fedora | opensuse) + case "$id_like" in debian | fedora | opensuse | arch) echo "$id_like" return ;; @@ -552,15 +561,17 @@ sh_c() { sudo_sh_c() { if [ "$(id -u)" = 0 ]; then sh_c "$@" + elif command_exists doas; then + sh_c "doas $*" elif command_exists sudo; then sh_c "sudo $*" elif command_exists su; then - sh_c "su - -c '$*'" + sh_c "su root -c '$*'" else echoh echoerr "This script needs to run the following command as root." echoerr " $*" - echoerr "Please install sudo or su." + echoerr "Please install doas, sudo, or su." exit 1 fi } diff --git a/lib/vscode b/lib/vscode new file mode 160000 index 000000000000..a5b500951314 --- /dev/null +++ b/lib/vscode @@ -0,0 +1 @@ +Subproject commit a5b500951314efd502d07465bd138dfbd714a960 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000000..bcb67b113d19 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6709 @@ +{ + "name": "code-server", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-server", + "version": "0.0.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@coder/logger": "^3.0.1", + "argon2": "^0.44.0", + "compression": "^1.7.4", + "cookie": "^1.1.1", + "cookie-parser": "^1.4.6", + "env-paths": "^2.2.1", + "express": "^5.0.1", + "http-proxy": "^1.18.1", + "httpolyglot": "^0.1.2", + "i18next": "^26.3.1", + "js-yaml": "^4.1.0", + "limiter": "^2.1.0", + "pem": "^1.14.8", + "proxy-agent": "^6.3.1", + "qs": "^6.15.0", + "rotating-file-stream": "^3.1.1", + "safe-compare": "^1.1.4", + "semver": "^7.5.4", + "ws": "^8.14.2", + "xdg-basedir": "^4.0.0" + }, + "bin": { + "code-server": "out/node/entry.js" + }, + "devDependencies": { + "@eslint/compat": "^1.2.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.12.0", + "@types/compression": "^1.7.3", + "@types/cookie-parser": "^1.4.4", + "@types/eslint__js": "^8.42.3", + "@types/express": "^5.0.0", + "@types/http-proxy": "1.17.7", + "@types/js-yaml": "^4.0.6", + "@types/node": "24.x", + "@types/pem": "^1.14.1", + "@types/proxy-from-env": "^1.0.1", + "@types/safe-compare": "^1.1.0", + "@types/semver": "^7.5.2", + "@types/trusted-types": "^2.0.4", + "@types/ws": "^8.5.5", + "doctoc": "^2.2.1", + "eslint": "^9.12.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-prettier": "^5.0.0", + "globals": "^17.6.0", + "prettier": "3.9.4", + "prettier-plugin-sh": "^0.18.0", + "ts-node": "^10.9.1", + "typescript": "^5.6.2", + "typescript-eslint": "^8.8.0" + }, + "engines": { + "node": "24" + } + }, + "node_modules/@coder/logger": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@coder/logger/-/logger-3.0.1.tgz", + "integrity": "sha512-G/wWSaNZW8HvQZWXlXdbZbp/MOQBPH4W1AKSEI3PBfr0qc9G+pdLrXvm3XakQpNVqmD6WFAbLfcjdG0BuoAO0g==", + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", + "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@reteps/dockerfmt": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@reteps/dockerfmt/-/dockerfmt-0.3.6.tgz", + "integrity": "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^v12.20.0 || ^14.13.0 || >=16.0.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/markdown-to-ast": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/markdown-to-ast/-/markdown-to-ast-15.7.1.tgz", + "integrity": "sha512-9DLSah7g6mYNHvO6pssLdFvFPAl3HHyEIm4RE5of/1QN9FXJXDgdOcVV3YpQmbYT/YntuIvOQiqOndCSPWTW2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.7.1", + "debug": "^4.4.3", + "mdast-util-gfm-autolink-literal": "^0.1.3", + "neotraverse": "^0.6.18", + "remark-footnotes": "^3.0.0", + "remark-frontmatter": "^3.0.0", + "remark-gfm": "^1.0.0", + "remark-parse": "^9.0.0", + "structured-source": "^4.0.0", + "unified": "^9.2.2" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", + "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pem": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@types/pem/-/pem-1.14.4.tgz", + "integrity": "sha512-Xt6qY6kX1RD4UmYNhWCCf3OSJrRcwbQIaJ/mQSjjAHxIjXMHx/vHNPOgEU3HdVKS1k/U5CZ6ClQlRo8egkl8xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/proxy-from-env": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/proxy-from-env/-/proxy-from-env-1.0.4.tgz", + "integrity": "sha512-TPR9/bCZAr3V1eHN4G3LD3OLicdJjqX1QRXWuNcCYgE66f/K8jO2ZRtHxI2D9MbnuUP6+qiKSS8eUHp6TFHGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/safe-compare": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/safe-compare/-/safe-compare-1.1.2.tgz", + "integrity": "sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "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/anchor-markdown-header": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/anchor-markdown-header/-/anchor-markdown-header-0.8.4.tgz", + "integrity": "sha512-20eMBMpts7k5rXAAj67geSqc/tsexHZOZJDWQD214YcDuNtyizDa7Q77sYa5rkao2FwsQP1WKRt2X6mphwmhbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "~10.6.0", + "remove-markdown": "^0.6.2" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argon2": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz", + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.0.0", + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, + "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==", + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ccount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", + "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "license": "MIT", + "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/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "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, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctoc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/doctoc/-/doctoc-2.5.0.tgz", + "integrity": "sha512-xWb2P8mQw9x+T9xPYToytRP0/bA68oexdsY0anMG72RdhZGTVPx6oHJfI57gbZe9LsOSfQUNNLwxSspdoTlJIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/markdown-to-ast": "^15.6.0", + "anchor-markdown-header": "^0.8.4", + "htmlparser2": "^7.2.0", + "loglevel": "^1.9.2", + "minimist": "^1.2.6" + }, + "bin": { + "doctoc": "doctoc.js" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promisify": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", + "integrity": "sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "Apache-2.0" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/httpolyglot": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/httpolyglot/-/httpolyglot-0.1.2.tgz", + "integrity": "sha512-ouHI1AaQMLgn4L224527S5+vq6hgvqPteurVfbm7ChViM3He2Wa8KP1Ny7pTYd7QKnDSPKcN8JYfC8r/lmsE3A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/i18next": { + "version": "26.3.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", + "integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip-address": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==", + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "license": "MIT", + "dependencies": { + "just-performance": "4.3.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.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, + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/longest-streak": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz", + "integrity": "sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-footnote": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/mdast-util-footnote/-/mdast-util-footnote-0.1.7.tgz", + "integrity": "sha512-QxNdO8qSxqbO2e3m09KwDKfWiLgqyCurdWTQ198NpbZ2hxntdc+VKS4fDJCmNWbAroUdYnSthu+XbZ8ovh8C3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-to-markdown": "^0.6.0", + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-0.2.0.tgz", + "integrity": "sha512-FHKL4w4S5fdt1KjJCwB0178WJ0evnyyQr5kXTM3wrOVpytD0hrkvd+AOOjU9Td8onOejCkmZ+HQRT3CZ3coHHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-frontmatter": "^0.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz", + "integrity": "sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-gfm-autolink-literal": "^0.1.0", + "mdast-util-gfm-strikethrough": "^0.2.0", + "mdast-util-gfm-table": "^0.1.0", + "mdast-util-gfm-task-list-item": "^0.1.0", + "mdast-util-to-markdown": "^0.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz", + "integrity": "sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ccount": "^1.0.0", + "mdast-util-find-and-replace": "^1.1.0", + "micromark": "^2.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz", + "integrity": "sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-to-markdown": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz", + "integrity": "sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "markdown-table": "^2.0.0", + "mdast-util-to-markdown": "~0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz", + "integrity": "sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-to-markdown": "~0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", + "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromark-extension-footnote": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/micromark-extension-footnote/-/micromark-extension-footnote-0.3.2.tgz", + "integrity": "sha512-gr/BeIxbIWQoUm02cIfK7mdMZ/fbroRpLsck4kvFtjbzP4yi+OPVbnukTc/zy0i7spC2xYE/dbX1Sur8BEDJsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-0.2.2.tgz", + "integrity": "sha512-q6nPLFCMTLtfsctAuS0Xh4vaolxSFUWUWR6PZSrXXiRy+SANGllpcqdXFv2z07l0Xz/6Hl40hK0ffNCJPH2n1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fault": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz", + "integrity": "sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "~2.11.0", + "micromark-extension-gfm-autolink-literal": "~0.5.0", + "micromark-extension-gfm-strikethrough": "~0.6.5", + "micromark-extension-gfm-table": "~0.4.0", + "micromark-extension-gfm-tagfilter": "~0.3.0", + "micromark-extension-gfm-task-list-item": "~0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz", + "integrity": "sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "~2.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz", + "integrity": "sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz", + "integrity": "sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz", + "integrity": "sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz", + "integrity": "sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "~2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pem": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.8.tgz", + "integrity": "sha512-ZpbOf4dj9/fQg5tQzTqv4jSKJQsK7tPl0pm4/pvPcZVjZcJg7TMfr3PBk6gJH97lnpJDu4e4v8UUqEz5daipCg==", + "license": "MIT", + "dependencies": { + "es6-promisify": "^7.0.0", + "md5": "^2.3.0", + "os-tmpdir": "^1.0.2", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-sh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.18.0.tgz", + "integrity": "sha512-cW1XL27FOJQ/qGHOW6IHwdCiNWQsAgK+feA8V6+xUTaH0cD3Mh+tFAtBvEEWvuY6hTDzRV943Fzeii+qMOh7nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@reteps/dockerfmt": "^0.3.6", + "sh-syntax": "^0.5.8" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + }, + "peerDependencies": { + "prettier": "^3.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remark-footnotes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-3.0.0.tgz", + "integrity": "sha512-ZssAvH9FjGYlJ/PBVKdSmfyPc3Cz4rTWgZLI4iE/SX8Nt5l3o3oEjv3wwG5VD7xOjktzdwp5coac+kJV9l4jgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-footnote": "^0.1.0", + "micromark-extension-footnote": "^0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-frontmatter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-3.0.0.tgz", + "integrity": "sha512-mSuDd3svCHs+2PyO29h7iijIZx4plX0fheacJcAoYAASfgzgVIcXGYSq9GFyYocFLftQs8IOmmkgtOovs6d4oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-frontmatter": "^0.2.0", + "micromark-extension-frontmatter": "^0.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz", + "integrity": "sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-gfm": "^0.1.0", + "micromark-extension-gfm": "^0.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remove-markdown": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.6.4.tgz", + "integrity": "sha512-BompiLClzjfh46irZmzv+1Q61jZYlKN3iq/hzae9EOUwf7ctr/5wpD4qwgn/FWDAYE18RORO7z/yr+HXZJCY8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rotating-file-stream": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/rotating-file-stream/-/rotating-file-stream-3.2.9.tgz", + "integrity": "sha512-i9i0KkHh12ryl4xtELg+0gyoFre2PJ9RcQQLzquWsiqygyYsrZLckrqqYrthhnJZGZb4g+KUHtcoWYVq34gaug==", + "license": "MIT", + "engines": { + "node": ">=14.0" + }, + "funding": { + "url": "https://www.blockchain.com/btc/address/12p1p5q7sK75tPyuesZmssiMYr4TKzpSCN" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safe-compare": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", + "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==", + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.2.0" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sh-syntax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.5.8.tgz", + "integrity": "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/sh-syntax" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json index fd2513051684..fc610fb8894d 100644 --- a/package.json +++ b/package.json @@ -1,112 +1,94 @@ { "name": "code-server", "license": "MIT", - "version": "3.12.0", + "version": "0.0.0", "description": "Run VS Code on a remote server.", - "homepage": "https://github.com/cdr/code-server", + "homepage": "https://github.com/coder/code-server", "bugs": { - "url": "https://github.com/cdr/code-server/issues" + "url": "https://github.com/coder/code-server/issues" }, - "repository": "https://github.com/cdr/code-server", + "repository": "https://github.com/coder/code-server", "scripts": { "clean": "./ci/build/clean.sh", "build": "./ci/build/build-code-server.sh", "build:vscode": "./ci/build/build-vscode.sh", + "doctoc": "./ci/dev/doctoc.sh", "release": "./ci/build/build-release.sh", - "release:standalone": "./ci/build/build-standalone-release.sh", - "release:github-draft": "./ci/build/release-github-draft.sh", - "release:github-assets": "./ci/build/release-github-assets.sh", "release:prep": "./ci/build/release-prep.sh", - "test:e2e": "./ci/dev/test-e2e.sh", - "test:standalone-release": "./ci/build/test-standalone-release.sh", - "test:unit": "./ci/dev/test-unit.sh", + "test:e2e": "VSCODE_IPC_HOOK_CLI= ./ci/dev/test-e2e.sh", + "test:e2e:proxy": "USE_PROXY=1 ./ci/dev/test-e2e.sh", + "test:unit": "./ci/dev/test-unit.sh --forceExit --detectOpenHandles", + "test:integration": "./ci/dev/test-integration.sh", + "test:native": "./ci/dev/test-native.sh", "test:scripts": "./ci/dev/test-scripts.sh", "package": "./ci/build/build-packages.sh", + "prettier": "prettier --write --log-level=warn --cache .", + "preinstall": "node ./ci/dev/preinstall.js", "postinstall": "./ci/dev/postinstall.sh", - "_audit": "./ci/dev/audit.sh", - "fmt": "./ci/dev/fmt.sh", - "lint": "./ci/dev/lint.sh", - "test": "echo 'Run yarn test:unit or yarn test:e2e' && exit 1", - "ci": "./ci/dev/ci.sh", - "watch": "VSCODE_IPC_HOOK_CLI= NODE_OPTIONS='--max_old_space_size=32384 --trace-warnings' ts-node ./ci/dev/watch.ts", - "icons": "./ci/dev/gen_icons.sh", - "coverage": "codecov" + "publish:npm": "./ci/steps/publish-npm.sh", + "publish:docker": "./ci/steps/docker-buildx-push.sh", + "fmt": "npm run prettier && ./ci/dev/doctoc.sh", + "lint:scripts": "./ci/dev/lint-scripts.sh", + "lint:ts": "eslint --max-warnings=0 --fix $(git ls-files '*.ts' '*.js' | grep -v 'lib/vscode')", + "test": "echo 'Run npm run test:unit or npm run test:e2e' && exit 1", + "watch": "VSCODE_DEV=1 VSCODE_IPC_HOOK_CLI= NODE_OPTIONS='--max_old_space_size=32384 --trace-warnings' ts-node ./ci/dev/watch.ts", + "icons": "./ci/dev/gen_icons.sh" }, "main": "out/node/entry.js", "devDependencies": { - "@schemastore/package": "^0.0.6", - "@types/body-parser": "^1.19.0", - "@types/browserify": "^12.0.36", - "@types/compression": "^1.7.0", - "@types/cookie-parser": "^1.4.2", - "@types/express": "^4.17.8", - "@types/http-proxy": "^1.17.4", - "@types/js-yaml": "^4.0.0", - "@types/node": "^14.17.1", - "@types/pem": "^1.9.5", + "@eslint/compat": "^1.2.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.12.0", + "@types/compression": "^1.7.3", + "@types/cookie-parser": "^1.4.4", + "@types/eslint__js": "^8.42.3", + "@types/express": "^5.0.0", + "@types/http-proxy": "1.17.7", + "@types/js-yaml": "^4.0.6", + "@types/node": "24.x", + "@types/pem": "^1.14.1", "@types/proxy-from-env": "^1.0.1", "@types/safe-compare": "^1.1.0", - "@types/semver": "^7.1.0", - "@types/split2": "^3.2.0", - "@types/tar-fs": "^2.0.0", - "@types/tar-stream": "^2.1.0", - "@types/ws": "^7.2.6", - "@typescript-eslint/eslint-plugin": "^4.7.0", - "@typescript-eslint/parser": "^4.7.0", - "audit-ci": "^4.0.0", - "browserify": "^17.0.0", - "codecov": "^3.8.3", - "doctoc": "^2.0.0", - "eslint": "^7.7.0", - "eslint-config-prettier": "^8.1.0", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-prettier": "^4.0.0", - "prettier": "^2.2.1", - "prettier-plugin-sh": "^0.7.1", - "shellcheck": "^1.0.0", - "stylelint": "^13.0.0", - "stylelint-config-recommended": "^5.0.0", - "ts-node": "^10.0.0", - "typescript": "^4.1.3" - }, - "resolutions": { - "ansi-regex": "^5.0.1", - "normalize-package-data": "^3.0.0", - "doctoc/underscore": "^1.13.1", - "doctoc/**/trim": "^1.0.0", - "postcss": "^8.2.1", - "browserslist": "^4.16.5", - "safe-buffer": "^5.1.1", - "vfile-message": "^2.0.2", - "argon2/@mapbox/node-pre-gyp/tar": "^6.1.9", - "path-parse": "^1.0.7" + "@types/semver": "^7.5.2", + "@types/trusted-types": "^2.0.4", + "@types/ws": "^8.5.5", + "doctoc": "^2.2.1", + "eslint": "^9.12.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-prettier": "^5.0.0", + "globals": "^17.6.0", + "prettier": "3.9.4", + "prettier-plugin-sh": "^0.18.0", + "ts-node": "^10.9.1", + "typescript": "^5.6.2", + "typescript-eslint": "^8.8.0" }, "dependencies": { - "@coder/logger": "1.1.16", - "argon2": "^0.28.0", - "body-parser": "^1.19.0", + "@coder/logger": "^3.0.1", + "argon2": "^0.44.0", "compression": "^1.7.4", - "cookie-parser": "^1.4.5", - "env-paths": "^2.2.0", - "express": "^5.0.0-alpha.8", - "http-proxy": "^1.18.0", + "cookie": "^1.1.1", + "cookie-parser": "^1.4.6", + "env-paths": "^2.2.1", + "express": "^5.0.1", + "http-proxy": "^1.18.1", "httpolyglot": "^0.1.2", - "js-yaml": "^4.0.0", - "limiter": "^1.1.5", - "pem": "^1.14.2", - "proxy-agent": "^5.0.0", - "proxy-from-env": "^1.1.0", - "qs": "6.10.1", - "rotating-file-stream": "^2.1.1", - "safe-buffer": "^5.1.1", + "i18next": "^26.3.1", + "js-yaml": "^4.1.0", + "limiter": "^2.1.0", + "pem": "^1.14.8", + "proxy-agent": "^6.3.1", + "qs": "^6.15.0", + "rotating-file-stream": "^3.1.1", "safe-compare": "^1.1.4", - "semver": "^7.1.3", - "split2": "^3.2.2", - "tar-fs": "^2.0.0", - "ws": "^8.0.0", - "xdg-basedir": "^4.0.0", - "yarn": "^1.22.4" + "semver": "^7.5.4", + "ws": "^8.14.2", + "xdg-basedir": "^4.0.0" + }, + "resolutions": { + "@types/node": "24.x" }, "bin": { "code-server": "out/node/entry.js" @@ -117,10 +99,11 @@ "ide", "coder", "vscode-remote", - "browser-ide" + "browser-ide", + "remote-development" ], "engines": { - "node": ">= 14" + "node": "24" }, "jest": { "transform": { @@ -129,7 +112,6 @@ "testEnvironment": "node", "testPathIgnorePatterns": [ "/node_modules/", - "/vendor/", "/lib/", "/out/", "test/e2e" @@ -159,10 +141,12 @@ "/release-standalone", "/release-npm-package", "/release-gcp", - "/release-images" + "/release-images", + "/lib" ], "moduleNameMapper": { "^.+\\.(css|less)$": "/test/utils/cssStub.ts" - } + }, + "globalSetup": "/test/utils/globalUnitSetup.ts" } } diff --git a/patches/app-name.diff b/patches/app-name.diff new file mode 100644 index 000000000000..32cb90ed0c28 --- /dev/null +++ b/patches/app-name.diff @@ -0,0 +1,46 @@ +Apply --app-name to VS Code web page titles + +VS Code's `${appName}` title variable comes from `productService.nameLong` in the +web client. code-server already injects per-request product configuration into +VS Code's web bootstrap, so set `nameShort`/`nameLong` from the existing +`--app-name` CLI arg there. + +This keeps the patch minimal and makes browser tab titles honor `--app-name` +without changing unrelated product metadata. + +Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts ++++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +@@ -24,6 +24,7 @@ export const serverOptions: OptionDescri + 'disable-getting-started-override': { type: 'boolean' }, + 'locale': { type: 'string' }, + 'link-protection-trusted-domains': { type: 'string[]' }, ++ 'app-name': { type: 'string' }, + + /* ----- server setup ----- */ + +@@ -124,6 +125,7 @@ export interface ServerParsedArgs { + 'disable-getting-started-override'?: boolean, + 'locale'?: string + 'link-protection-trusted-domains'?: string[], ++ 'app-name'?: string, + + /* ----- server setup ----- */ + +Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts ++++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts +@@ -366,8 +366,11 @@ export class WebClientServer { + linkProtectionTrustedDomains.push(...this._productService.linkProtectionTrustedDomains); + } + ++ const appName = this._environmentService.args['app-name']; + const productConfiguration: Partial> = { + codeServerVersion: this._productService.codeServerVersion, ++ nameShort: appName, ++ nameLong: appName, + rootEndpoint: rootBase, + updateEndpoint: !this._environmentService.args['disable-update-check'] ? rootBase + '/update/check' : undefined, + logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" ? rootBase + '/logout' : undefined, diff --git a/patches/base-path.diff b/patches/base-path.diff new file mode 100644 index 000000000000..e065145ad47d --- /dev/null +++ b/patches/base-path.diff @@ -0,0 +1,307 @@ +Add base path support + +Some users will host code-server behind a path-rewriting reverse proxy, for +example domain.tld/my/base/path. This patch adds support for that since Code +assumes everything is on / by default. + +To test this serve code-server behind a reverse proxy with a path like /code. + +Index: code-server/lib/vscode/src/vs/base/common/network.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/base/common/network.ts ++++ code-server/lib/vscode/src/vs/base/common/network.ts +@@ -245,7 +245,9 @@ class RemoteAuthoritiesImpl { + return URI.from({ + scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource, + authority: `${host}:${port}`, +- path: this._remoteResourcesPath, ++ path: platform.isWeb ++ ? (window.location.pathname + "/" + this._remoteResourcesPath).replace(/\/\/+/g, "/") ++ : this._remoteResourcesPath, + query + }); + } +Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html +=================================================================== +--- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html ++++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html +@@ -11,8 +11,8 @@ + + + +- +- ++ ++ + + + +@@ -27,9 +27,9 @@ + + + +- +- +- ++ ++ ++ + + + +@@ -39,7 +39,7 @@ + + + + + diff --git a/src/browser/pages/global.css b/src/browser/pages/global.css index fcc3ace34300..ba79ea6134c6 100644 --- a/src/browser/pages/global.css +++ b/src/browser/pages/global.css @@ -1,3 +1,7 @@ +:root { + color-scheme: light dark; +} + html, body, #root { @@ -7,9 +11,12 @@ body, body { background: rgb(244, 247, 252); + background: light-dark(rgb(244, 247, 252), #111827); color: #111; + color: light-dark(#111, #ddd); margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; overflow: hidden; } @@ -23,12 +30,15 @@ button { .-button { background-color: rgb(87, 114, 245); + background-color: light-dark(rgb(87, 114, 245), rgb(26, 86, 219)); border-radius: 5px; border: none; box-sizing: border-box; color: white; + color: light-dark(white, white); cursor: pointer; padding: 18px 20px; + font-weight: 500; text-decoration: none; } @@ -45,15 +55,20 @@ button { .card-box { background-color: rgb(250, 253, 258); + background-color: light-dark(rgb(250, 253, 258), #1f2937); border-radius: 5px; - box-shadow: rgba(60, 66, 87, 0.117647) 0px 7px 14px 0px, rgba(0, 0, 0, 0.117647) 0px 3px 6px 0px; + box-shadow: + light-dark(rgba(60, 66, 87, 0.117647), rgba(10, 10, 10, 0.617647)) 0px 7px 14px 0px, + rgba(0, 0, 0, 0.117647) 0px 3px 6px 0px; max-width: 650px; width: 100%; } .card-box > .header { border-bottom: 1px solid #ddd; + border-bottom: 1px solid light-dark(#ddd, #111827); color: #444; + color: light-dark(#444, #fff); padding: 30px; } @@ -64,6 +79,7 @@ button { .card-box > .header > .sub { color: #555; + color: light-dark(#555, #9ca3af); margin-top: 10px; } diff --git a/src/browser/pages/login.css b/src/browser/pages/login.css index 026cac97f5d6..33b541c9e844 100644 --- a/src/browser/pages/login.css +++ b/src/browser/pages/login.css @@ -30,14 +30,23 @@ body { .login-form > .field > .password { background-color: rgb(244, 247, 252); + background-color: light-dark(rgb(244, 247, 252), #374151); border-radius: 5px; border: 1px solid #ddd; + border: 1px solid light-dark(#ddd, #4b5563); box-sizing: border-box; - color: black; flex: 1; padding: 16px; } +.login-form > .field > .password::placeholder { + color: rgb(148 163 184); +} + +.login-form > .field > .password:focus { + outline: 2px solid rgb(63, 131, 248); +} + .login-form > .user { display: none; } diff --git a/src/browser/pages/login.html b/src/browser/pages/login.html index 896927e3812c..c7fb2f2ac67e 100644 --- a/src/browser/pages/login.html +++ b/src/browser/pages/login.html @@ -1,4 +1,4 @@ - + @@ -10,10 +10,11 @@ http-equiv="Content-Security-Policy" content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;" /> - code-server login + + {{I18N_LOGIN_TITLE}} - + @@ -24,30 +25,38 @@
-

Welcome to code-server

-
Please log in below. {{PASSWORD_MSG}}
+

{{WELCOME_TEXT}}

+
{{I18N_LOGIN_BELOW}} {{PASSWORD_MSG}}
+ - diff --git a/src/browser/pages/login.ts b/src/browser/pages/login.ts deleted file mode 100644 index cd3fd0d16542..000000000000 --- a/src/browser/pages/login.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getOptions } from "../../common/util" -import "../register" - -const options = getOptions() -const el = document.getElementById("base") as HTMLInputElement -if (el) { - el.value = options.base -} diff --git a/src/browser/pages/vscode.html b/src/browser/pages/vscode.html deleted file mode 100644 index a01223ceccd0..000000000000 --- a/src/browser/pages/vscode.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts deleted file mode 100644 index ed5849648955..000000000000 --- a/src/browser/pages/vscode.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { getOptions, Options } from "../../common/util" -import "../register" - -// TODO@jsjoeio: Add proper types. -type FixMeLater = any - -// NOTE@jsjoeio -// This lives here ../../../lib/vscode/src/vs/base/common/platform.ts#L106 -export const nlsConfigElementId = "vscode-remote-nls-configuration" - -type NlsConfiguration = { - locale: string - availableLanguages: { [key: string]: string } | {} - _languagePackId?: string - _translationsConfigFile?: string - _cacheRoot?: string - _resolvedLanguagePackCoreLocation?: string - _corruptedFile?: string - _languagePackSupport?: boolean - loadBundle?: FixMeLater -} - -/** - * Helper function to create the path to the bundle - * for getNlsConfiguration. - */ -export function createBundlePath(_resolvedLanguagePackCoreLocation: string | undefined, bundle: string) { - // NOTE@jsjoeio - this comment was here before me - // Refers to operating systems that use a different path separator. - // Probably just Windows but we're not sure if "/" breaks on Windows - // so we'll leave it alone for now. - // FIXME: Only works if path separators are /. - return (_resolvedLanguagePackCoreLocation || "") + "/" + bundle.replace(/\//g, "!") + ".nls.json" -} - -/** - * A helper function to get the NLS Configuration settings. - * - * This is used by VSCode for localizations (i.e. changing - * the display language). - * - * Make sure to wrap this in a try/catch block when you call it. - **/ -export function getNlsConfiguration(_document: Document, base: string) { - const errorMsgPrefix = "[vscode]" - const nlsConfigElement = _document?.getElementById(nlsConfigElementId) - const dataSettings = nlsConfigElement?.getAttribute("data-settings") - - if (!nlsConfigElement) { - throw new Error( - `${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}`, - ) - } - - if (!dataSettings) { - throw new Error( - `${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.`, - ) - } - - const nlsConfig = JSON.parse(dataSettings) as NlsConfiguration - - if (nlsConfig._resolvedLanguagePackCoreLocation) { - // NOTE@jsjoeio - // Not sure why we use Object.create(null) instead of {} - // They are not the same - // See: https://stackoverflow.com/a/15518712/3015595 - // We copied this from ../../../lib/vscode/src/bootstrap.js#L143 - const bundles: { - [key: string]: string - } = Object.create(null) - - type LoadBundleCallback = (_: undefined, result?: string) => void - - nlsConfig.loadBundle = async (bundle: string, _language: string, cb: LoadBundleCallback): Promise => { - const result = bundles[bundle] - - if (result) { - return cb(undefined, result) - } - - try { - const path = createBundlePath(nlsConfig._resolvedLanguagePackCoreLocation, bundle) - const response = await fetch(`${base}/vscode/resource/?path=${encodeURIComponent(path)}`) - const json = await response.json() - bundles[bundle] = json - return cb(undefined, json) - } catch (error) { - return cb(error) - } - } - } - - return nlsConfig -} - -type GetLoaderParams = { - nlsConfig: NlsConfiguration - options: Options - _window: Window -} - -/** - * Link to types in the loader source repo - * https://github.com/microsoft/vscode-loader/blob/main/src/loader.d.ts#L280 - */ -type Loader = { - baseUrl: string - recordStats: boolean - // TODO@jsjoeio: There don't appear to be any types for trustedTypes yet. - trustedTypesPolicy: FixMeLater - paths: { - [key: string]: string - } - "vs/nls": NlsConfiguration -} - -/** - * A helper function which creates a script url if the value - * is valid. - * - * Extracted into a function to make it easier to test - */ -export function _createScriptURL(value: string, origin: string): string { - if (value.startsWith(origin)) { - return value - } - throw new Error(`Invalid script url: ${value}`) -} - -/** - * A helper function to get the require loader - * - * This used by VSCode/code-server - * to load files. - * - * We extracted the logic into a function so that - * it's easier to test. - **/ -export function getConfigurationForLoader({ nlsConfig, options, _window }: GetLoaderParams) { - const loader: Loader = { - // Without the full URL VS Code will try to load file://. - baseUrl: `${window.location.origin}${options.csStaticBase}/vendor/modules/code-oss-dev/out`, - recordStats: true, - trustedTypesPolicy: (_window as FixMeLater).trustedTypes?.createPolicy("amdLoader", { - createScriptURL(value: string): string { - return _createScriptURL(value, window.location.origin) - }, - }), - paths: { - "vscode-textmate": `../node_modules/vscode-textmate/release/main`, - "vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`, - xterm: `../node_modules/xterm/lib/xterm.js`, - "xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`, - "xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, - "xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, - "tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`, - "iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, - jschardet: `../node_modules/jschardet/dist/jschardet.min.js`, - }, - "vs/nls": nlsConfig, - } - - return loader -} - -/** - * Sets the body background color to match the theme. - */ -export function setBodyBackgroundToThemeBackgroundColor(_document: Document, _localStorage: Storage) { - const errorMsgPrefix = "[vscode]" - const colorThemeData = _localStorage.getItem("colorThemeData") - - if (!colorThemeData) { - throw new Error( - `${errorMsgPrefix} Could not set body background to theme background color. Could not find colorThemeData in localStorage.`, - ) - } - - let _colorThemeData - try { - // We wrap this JSON.parse logic in a try/catch - // because it can throw if the JSON is invalid. - // and instead of throwing a random error - // we can throw our own error, which will be more helpful - // to the end user. - _colorThemeData = JSON.parse(colorThemeData) - } catch { - throw new Error( - `${errorMsgPrefix} Could not set body background to theme background color. Could not parse colorThemeData from localStorage.`, - ) - } - - const hasColorMapProperty = Object.prototype.hasOwnProperty.call(_colorThemeData, "colorMap") - if (!hasColorMapProperty) { - throw new Error( - `${errorMsgPrefix} Could not set body background to theme background color. colorThemeData is missing colorMap.`, - ) - } - - const editorBgColor = _colorThemeData.colorMap["editor.background"] - - if (!editorBgColor) { - throw new Error( - `${errorMsgPrefix} Could not set body background to theme background color. colorThemeData.colorMap["editor.background"] is undefined.`, - ) - } - - _document.body.style.background = editorBgColor - - return null -} - -/** - * A helper function to encapsulate all the - * logic used in this file. - * - * We purposely include all of this in a single function - * so that it's easier to test. - */ -export function main(_document: Document | undefined, _window: Window | undefined, _localStorage: Storage | undefined) { - if (!_document) { - throw new Error(`document is undefined.`) - } - - if (!_window) { - throw new Error(`window is undefined.`) - } - - if (!_localStorage) { - throw new Error(`localStorage is undefined.`) - } - - const options = getOptions() - const nlsConfig = getNlsConfiguration(_document, options.base) - - const loader = getConfigurationForLoader({ - nlsConfig, - options, - _window, - }) - - ;(self.require as unknown as Loader) = loader - - setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) -} - -try { - main(document, window, localStorage) -} catch (error) { - console.error("[vscode] failed to initialize VS Code") - console.error(error) -} diff --git a/src/browser/register.ts b/src/browser/register.ts deleted file mode 100644 index 4774ad5fa467..000000000000 --- a/src/browser/register.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { logger } from "@coder/logger" -import { getOptions, normalize, logError } from "../common/util" - -export async function registerServiceWorker(): Promise { - const options = getOptions() - logger.level = options.logLevel - - const path = normalize(`${options.csStaticBase}/out/browser/serviceWorker.js`) - try { - await navigator.serviceWorker.register(path, { - scope: options.base + "/", - }) - logger.info(`[Service Worker] registered`) - } catch (error) { - logError(logger, `[Service Worker] registration`, error) - } -} - -if (typeof navigator !== "undefined" && "serviceWorker" in navigator) { - registerServiceWorker() -} else { - logger.error(`[Service Worker] navigator is undefined`) -} diff --git a/src/browser/security.txt b/src/browser/security.txt new file mode 100644 index 000000000000..18996f284232 --- /dev/null +++ b/src/browser/security.txt @@ -0,0 +1,6 @@ +Contact: mailto:security@coder.com +Acknowledgments: https://coder.com/security/thanks +Preferred-Languages: en-US +Canonical: https://coder.com/.well-known/security.txt +Policy: https://coder.com/security/policy +Hiring: https://coder.com/careers \ No newline at end of file diff --git a/src/browser/serviceWorker.ts b/src/browser/serviceWorker.ts index 25765a1a4a68..37fbf7afa5f5 100644 --- a/src/browser/serviceWorker.ts +++ b/src/browser/serviceWorker.ts @@ -1,12 +1,10 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - self.addEventListener("install", () => { - console.log("[Service Worker] installed") + console.debug("[Service Worker] installed") }) self.addEventListener("activate", (event: any) => { event.waitUntil((self as any).clients.claim()) - console.log("[Service Worker] activated") + console.debug("[Service Worker] activated") }) self.addEventListener("fetch", () => { diff --git a/src/common/emitter.ts b/src/common/emitter.ts index 353ce851e825..78d0d7990ddb 100644 --- a/src/common/emitter.ts +++ b/src/common/emitter.ts @@ -7,7 +7,7 @@ import { logger } from "@coder/logger" export type Callback> = (t: T, p: Promise) => R export interface Disposable { - dispose(): void + dispose(): void | Promise } export interface Event { @@ -46,7 +46,7 @@ export class Emitter { this.listeners.map(async (cb) => { try { await cb(value, promise) - } catch (error) { + } catch (error: any) { logger.error(error.message) } }), diff --git a/src/common/http.ts b/src/common/http.ts index c08c8673b477..f005272bfc53 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -4,6 +4,7 @@ export enum HttpCode { NotFound = 404, BadRequest = 400, Unauthorized = 401, + Forbidden = 403, LargePayload = 413, ServerError = 500, } @@ -13,8 +14,16 @@ export enum HttpCode { * used in the HTTP response. */ export class HttpError extends Error { - public constructor(message: string, public readonly status: HttpCode, public readonly details?: object) { + public constructor( + message: string, + public readonly statusCode: HttpCode, + public readonly details?: object, + ) { super(message) this.name = this.constructor.name } } + +export function getCookieSessionName(suffix?: string): string { + return suffix ? `code-server-session-${suffix.replace(/[^a-zA-Z0-9-]/g, "-")}` : "code-server-session" +} diff --git a/src/common/util.ts b/src/common/util.ts index 4e4f23cfd818..6639beca42b1 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -1,28 +1,3 @@ -/* - * This file exists in two locations: - * - src/common/util.ts - * - lib/vscode/src/vs/server/common/util.ts - * The second is a symlink to the first. - */ - -/** - * Base options included on every page. - */ -export interface Options { - base: string - csStaticBase: string - logLevel: number -} - -/** - * Split a string up to the delimiter. If the delimiter doesn't exist the first - * item will have all the text and the second item will be an empty string. - */ -export const split = (str: string, delimiter: string): [string, string] => { - const index = str.indexOf(delimiter) - return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ""] -} - /** * Appends an 's' to the provided string if count is greater than one; * otherwise the string is returned @@ -39,89 +14,19 @@ export const generateUuid = (length = 24): string => { /** * Remove extra slashes in a URL. + * + * This is meant to fill the job of `path.join` so you can concatenate paths and + * then normalize out any extra slashes. + * + * If you are using `path.join` you do not need this but note that `path` is for + * file system paths, not URLs. */ export const normalize = (url: string, keepTrailing = false): string => { return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "") } -/** - * Remove leading and trailing slashes. - */ -export const trimSlashes = (url: string): string => { - return url.replace(/^\/+|\/+$/g, "") -} - -/** - * Resolve a relative base against the window location. This is used for - * anything that doesn't work with a relative path. - */ -export const resolveBase = (base?: string): string => { - // After resolving the base will either start with / or be an empty string. - if (!base || base.startsWith("/")) { - return base ?? "" - } - const parts = location.pathname.split("/") - parts[parts.length - 1] = base - const url = new URL(location.origin + "/" + parts.join("/")) - return normalize(url.pathname) -} - -/** - * Get options embedded in the HTML or query params. - */ -export const getOptions = (): T => { - let options: T - try { - options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!) - } catch (error) { - options = {} as T - } - - // You can also pass options in stringified form to the options query - // variable. Options provided here will override the ones in the options - // element. - const params = new URLSearchParams(location.search) - const queryOpts = params.get("options") - if (queryOpts) { - options = { - ...options, - ...JSON.parse(queryOpts), - } - } - - options.base = resolveBase(options.base) - options.csStaticBase = resolveBase(options.csStaticBase) - - return options -} - -/** - * Wrap the value in an array if it's not already an array. If the value is - * undefined return an empty array. - */ -export const arrayify = (value?: T | T[]): T[] => { - if (Array.isArray(value)) { - return value - } - if (typeof value === "undefined") { - return [] - } - return [value] -} - -/** - * Get the first string. If there's no string return undefined. - */ -export const getFirstString = (value: string | string[] | object | undefined): string | undefined => { - if (Array.isArray(value)) { - return value[0] - } - - return typeof value === "string" ? value : undefined -} - // TODO: Might make sense to add Error handling to the logger itself. -export function logError(logger: { error: (msg: string) => void }, prefix: string, err: Error | string): void { +export function logError(logger: { error: (msg: string) => void }, prefix: string, err: unknown): void { if (err instanceof Error) { logger.error(`${prefix}: ${err.message} ${err.stack}`) } else { diff --git a/src/node/app.ts b/src/node/app.ts index a4e099c0f86d..2043e3fd4bc0 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -4,18 +4,71 @@ import express, { Express } from "express" import { promises as fs } from "fs" import http from "http" import * as httpolyglot from "httpolyglot" +import { Disposable } from "../common/emitter" import * as util from "../common/util" import { DefaultedArgs } from "./cli" +import { disposer } from "./http" import { isNodeJSErrnoException } from "./util" +import { EditorSessionManager, makeEditorSessionManagerServer } from "./vscodeSocket" import { handleUpgrade } from "./wsRouter" +type SocketOptions = { socket: string; "socket-mode"?: string } +type ListenOptions = DefaultedArgs | SocketOptions + +export interface App extends Disposable { + /** Handles regular HTTP requests. */ + router: Express + /** Handles websocket requests. */ + wsRouter: Express + /** The underlying HTTP server. */ + server: http.Server + /** Handles requests to the editor session management API. */ + editorSessionManagerServer: http.Server +} + +const isSocketOpts = (opts: ListenOptions): opts is SocketOptions => { + return !!(opts as SocketOptions).socket || !(opts as DefaultedArgs).host +} + +export const listen = async (server: http.Server, opts: ListenOptions) => { + if (isSocketOpts(opts)) { + try { + await fs.unlink(opts.socket) + } catch (error: any) { + handleArgsSocketCatchError(error) + } + } + await new Promise(async (resolve, reject) => { + server.on("error", reject) + const onListen = () => { + // Promise resolved earlier so this is an unrelated error. + server.off("error", reject) + server.on("error", (err) => util.logError(logger, "http server error", err)) + resolve() + } + if (isSocketOpts(opts)) { + server.listen(opts.socket, onListen) + } else { + // [] is the correct format when using :: but Node errors with them. + server.listen(opts.port, opts.host.replace(/^\[|\]$/g, ""), onListen) + } + }) + + // NOTE@jsjoeio: we need to chmod after the server is finished + // listening. Otherwise, the socket may not have been created yet. + if (isSocketOpts(opts)) { + if (opts["socket-mode"]) { + await fs.chmod(opts.socket, opts["socket-mode"]) + } + } +} + /** * Create an Express app and an HTTP/S server to serve it. */ -export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => { - const app = express() - - app.use(compression()) +export const createApp = async (args: DefaultedArgs): Promise => { + const router = express() + router.use(compression()) const server = args.cert ? httpolyglot.createServer( @@ -23,75 +76,48 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, cert: args.cert && (await fs.readFile(args.cert.value)), key: args["cert-key"] && (await fs.readFile(args["cert-key"])), }, - app, + router, ) - : http.createServer(app) + : http.createServer(router) - let resolved = false - await new Promise(async (resolve2, reject) => { - const resolve = () => { - resolved = true - resolve2() - } - server.on("error", (err) => { - handleServerError(resolved, err, reject) - }) - - if (args.socket) { - try { - await fs.unlink(args.socket) - } catch (error: any) { - handleArgsSocketCatchError(error) - } - server.listen(args.socket, resolve) - } else { - // [] is the correct format when using :: but Node errors with them. - server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve) - } - }) + const disposeServer = disposer(server) + + await listen(server, args) + + const wsRouter = express() + handleUpgrade(wsRouter, server) + + const editorSessionManager = new EditorSessionManager() + const editorSessionManagerServer = await makeEditorSessionManagerServer(args["session-socket"], editorSessionManager) + const disposeEditorSessionManagerServer = disposer(editorSessionManagerServer) - const wsApp = express() - handleUpgrade(wsApp, server) + const dispose = async () => { + await Promise.all([disposeServer(), disposeEditorSessionManagerServer()]) + } - return [app, wsApp, server] + return { router, wsRouter, server, dispose, editorSessionManagerServer } } /** * Get the address of a server as a string (protocol *is* included) while * ensuring there is one (will throw if there isn't). + * + * The address might be a URL or it might be a pipe or socket path. */ -export const ensureAddress = (server: http.Server): string => { +export const ensureAddress = (server: http.Server, protocol: string): URL | string => { const addr = server.address() + if (!addr) { - throw new Error("server has no address") + throw new Error("Server has no address") } + if (typeof addr !== "string") { - return `http://${addr.address}:${addr.port}` + const host = addr.family === "IPv6" ? `[${addr.address}]` : addr.address + return new URL(`${protocol}://${host}:${addr.port}`) } - return addr -} -/** - * Handles error events from the server. - * - * If the outlying Promise didn't resolve - * then we reject with the error. - * - * Otherwise, we log the error. - * - * We extracted into a function so that we could - * test this logic more easily. - */ -export const handleServerError = (resolved: boolean, err: Error, reject: (err: Error) => void) => { - // Promise didn't resolve earlier so this means it's an error - // that occurs before the server can successfully listen. - // Possibly triggered by listening on an invalid port or socket. - if (!resolved) { - reject(err) - } else { - // Promise resolved earlier so this is an unrelated error. - util.logError(logger, "http server error", err) - } + // If this is a string then it is a pipe or Unix socket. + return addr } /** @@ -103,6 +129,6 @@ export const handleServerError = (resolved: boolean, err: Error, reject: (err: E */ export const handleArgsSocketCatchError = (error: any) => { if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") { - logger.error(error.message ? error.message : error) + throw Error(error.message ? error.message : error) } } diff --git a/src/node/cli.ts b/src/node/cli.ts index 2433ddf2e2a7..0946c8e04344 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -1,14 +1,13 @@ import { field, Level, logger } from "@coder/logger" import { promises as fs } from "fs" -import yaml from "js-yaml" -import * as os from "os" +import { load } from "js-yaml" import * as path from "path" -import { Args as VsArgs } from "../../typings/ipc" -import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util" +import { generateCertificate, generatePassword, paths, splitOnFirstEquals } from "./util" +import { EditorSessionManagerClient } from "./vscodeSocket" export enum Feature { - /** Web socket compression. */ - PermessageDeflate = "permessage-deflate", + // No current experimental features! + Placeholder = "placeholder", } export enum AuthType { @@ -30,7 +29,44 @@ export enum LogLevel { export class OptionalString extends Optional {} -export interface Args extends VsArgs { +/** + * (VS) Code flags provided by the user. + */ +export interface UserProvidedCodeArgs { + "disable-telemetry"?: boolean + force?: boolean + "user-data-dir"?: string + "enable-proposed-api"?: string[] + "extensions-dir"?: string + "builtin-extensions-dir"?: string + "install-extension"?: string[] + "uninstall-extension"?: string[] + "list-extensions"?: boolean + "locate-extension"?: string[] + "show-versions"?: boolean + category?: string + "github-auth"?: string + "disable-update-check"?: boolean + "disable-file-downloads"?: boolean + "disable-file-uploads"?: boolean + "disable-workspace-trust"?: boolean + "disable-getting-started-override"?: boolean + "disable-proxy"?: boolean + "reconnection-grace-time"?: string + "session-socket"?: string + "cookie-suffix"?: string + "link-protection-trusted-domains"?: string[] + // locale is used by both VS Code and code-server. + locale?: string +} + +/** + * Arguments that the user explicitly provided on the command line. All + * arguments must be optional. + * + * For arguments with defaults see DefaultedArgs. + */ +export interface UserProvidedArgs extends UserProvidedCodeArgs { config?: string auth?: AuthType password?: string @@ -38,30 +74,31 @@ export interface Args extends VsArgs { cert?: OptionalString "cert-host"?: string "cert-key"?: string - "disable-telemetry"?: boolean - "disable-update-check"?: boolean enable?: string[] help?: boolean host?: string + port?: number json?: boolean log?: LogLevel open?: boolean - port?: number "bind-addr"?: string socket?: string + "socket-mode"?: string + "trusted-origins"?: string[] version?: boolean - force?: boolean - "list-extensions"?: boolean - "install-extension"?: string[] - "show-versions"?: boolean - "uninstall-extension"?: string[] "proxy-domain"?: string[] - locale?: string - _: string[] + "skip-auth-preflight"?: boolean "reuse-window"?: boolean "new-window"?: boolean - - link?: OptionalString + "ignore-last-opened"?: boolean + verbose?: boolean + "app-name"?: string + "welcome-text"?: string + "abs-proxy-base-path"?: string + i18n?: string + "idle-timeout-seconds"?: number + /* Positional arguments. */ + _?: string[] } interface Option { @@ -80,32 +117,32 @@ interface Option { description?: string /** - * If marked as beta, the option is marked as beta in help. + * If marked as deprecated, the option is marked as deprecated in help. */ - beta?: boolean + deprecated?: boolean } type OptionType = T extends boolean ? "boolean" : T extends OptionalString - ? typeof OptionalString - : T extends LogLevel - ? typeof LogLevel - : T extends AuthType - ? typeof AuthType - : T extends number - ? "number" - : T extends string - ? "string" - : T extends string[] - ? "string[]" - : "unknown" - -type Options = { + ? typeof OptionalString + : T extends LogLevel + ? typeof LogLevel + : T extends AuthType + ? typeof AuthType + : T extends number + ? "number" + : T extends string + ? "string" + : T extends string[] + ? "string[]" + : "unknown" + +export type Options = { [P in keyof T]: Option> } -const options: Options> = { +export const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, password: { type: "string", @@ -134,11 +171,53 @@ const options: Options> = { "Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" + "then notifies you once every week that a new release is available.", }, + "session-socket": { + type: "string", + }, + "cookie-suffix": { + type: "string", + description: + "Adds a suffix to the cookie. This can prevent a collision of cookies for subdomains, making them explixit. \n" + + "Without this flag, no suffix is used. This can also be set with CODE_SERVER_COOKIE_SUFFIX set to any string.", + }, + "disable-file-downloads": { + type: "boolean", + description: + "Disable file downloads from Code. This can also be set with CS_DISABLE_FILE_DOWNLOADS set to 'true' or '1'.", + }, + "disable-file-uploads": { + type: "boolean", + description: "Disable file uploads.", + }, + "disable-workspace-trust": { + type: "boolean", + description: "Disable Workspace Trust feature. This switch only affects the current session.", + }, + "disable-getting-started-override": { + type: "boolean", + description: "Disable the coder/coder override in the Help: Getting Started page.", + }, + "disable-proxy": { + type: "boolean", + description: "Disable domain and path proxy routes.", + }, // --enable can be used to enable experimental features. These features // provide no guarantees. enable: { type: "string[]" }, help: { type: "boolean", short: "h", description: "Show this output." }, json: { type: "boolean" }, + "link-protection-trusted-domains": { + type: "string[]", + description: "Links matching a trusted domain can be opened without link protection.", + }, + locale: { + // The preferred way to set the locale is via the UI. + type: "string", + description: ` + Set vscode display language and language to show on the login page, more info see + https://en.wikipedia.org/wiki/IETF_language_tag + `, + }, open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." }, "bind-addr": { @@ -156,16 +235,22 @@ const options: Options> = { port: { type: "number", description: "" }, socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." }, + "socket-mode": { type: "string", description: "File mode of the socket." }, + "trusted-origins": { + type: "string[]", + description: + "Disables authenticate origin check for trusted origin. Useful if not able to access reverse proxy configuration.", + }, version: { type: "boolean", short: "v", description: "Display version information." }, _: { type: "string[]" }, "user-data-dir": { type: "string", path: true, description: "Path to the user data directory." }, "extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." }, "builtin-extensions-dir": { type: "string", path: true }, - "extra-extensions-dir": { type: "string[]", path: true }, - "extra-builtin-extensions-dir": { type: "string[]", path: true }, "list-extensions": { type: "boolean", description: "List installed VS Code extensions." }, force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." }, + "locate-extension": { type: "string[]" }, + category: { type: "string" }, "install-extension": { type: "string[]", description: @@ -179,7 +264,15 @@ const options: Options> = { }, "uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." }, "show-versions": { type: "boolean", description: "Show VS Code extension versions." }, + "github-auth": { + type: "string", + description: "GitHub authentication token (can only be passed in via $GITHUB_TOKEN or the config file).", + }, "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, + "skip-auth-preflight": { + type: "boolean", + description: "Allows preflight requests through proxy without authentication.", + }, "ignore-last-opened": { type: "boolean", short: "e", @@ -196,23 +289,43 @@ const options: Options> = { description: "Force to open a file or folder in an already opened window.", }, - locale: { type: "string" }, log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, - - link: { - type: OptionalString, - description: ` - Securely bind code-server via our cloud service with the passed name. You'll get a URL like - https://hostname-username.cdr.co at which you can easily access your code-server instance. - Authorization is done via GitHub. - `, - beta: true, + "app-name": { + type: "string", + short: "an", + description: + "Will replace the {{app}} placeholder in any strings, which by default includes the title bar and welcome message", + }, + "welcome-text": { + type: "string", + short: "w", + description: "Text to show on login page", + deprecated: true, + }, + "abs-proxy-base-path": { + type: "string", + description: "The base path to prefix to all absproxy requests", + }, + i18n: { + type: "string", + path: true, + description: "Path to JSON file with custom translations. Merges with default strings and supports all i18n keys.", + }, + "idle-timeout-seconds": { + type: "number", + description: "Timeout in seconds to wait before shutting down when idle.", + }, + "reconnection-grace-time": { + type: "string", + description: + "Override the reconnection grace time in seconds. Clients who disconnect for longer than this duration will need to \n" + + "reload the window. Defaults to 10800 (3 hours).", }, } -export const optionDescriptions = (): string[] => { - const entries = Object.entries(options).filter(([, v]) => !!v.description) +export const optionDescriptions = (opts: Partial>> = options): string[] => { + const entries = Object.entries(opts).filter(([, v]) => !!v.description) const widths = entries.reduce( (prev, [k, v]) => ({ long: k.length > prev.long ? k.length : prev.long, @@ -230,7 +343,7 @@ export const optionDescriptions = (): string[] => { .map((line, i) => { line = line.trim() if (i === 0) { - return " ".repeat(widths.long - k.length) + (v.beta ? "(beta) " : "") + line + return " ".repeat(widths.long - k.length) + (v.deprecated ? "(deprecated) " : "") + line } return " ".repeat(widths.long + widths.short + 6) + line }) @@ -240,25 +353,16 @@ export const optionDescriptions = (): string[] => { }) } -export function splitOnFirstEquals(str: string): string[] { - // we use regex instead of "=" to ensure we split at the first - // "=" and return the following substring with it - // important for the hashed-password which looks like this - // $argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY - // 2 means return two items - // Source: https://stackoverflow.com/a/4607799/3015595 - // We use the ? to say the the substr after the = is optional - const split = str.split(/=(.+)?/, 2) - - return split -} - +/** + * Parse arguments into UserProvidedArgs. This should not go beyond checking + * that arguments are valid types and have values when required. + */ export const parse = ( argv: string[], opts?: { configFile?: string }, -): Args => { +): UserProvidedArgs => { const error = (msg: string): Error => { if (opts?.configFile) { msg = `error reading ${opts.configFile}: ${msg}` @@ -267,7 +371,7 @@ export const parse = ( return new Error(msg) } - const args: Args = { _: [] } + const args: UserProvidedArgs = {} let ended = false for (let i = 0; i < argv.length; ++i) { @@ -281,17 +385,17 @@ export const parse = ( // Options start with a dash and require a value if non-boolean. if (!ended && arg.startsWith("-")) { - let key: keyof Args | undefined + let key: keyof UserProvidedArgs | undefined let value: string | undefined if (arg.startsWith("--")) { const split = splitOnFirstEquals(arg.replace(/^--/, "")) - key = split[0] as keyof Args + key = split[0] as keyof UserProvidedArgs value = split[1] } else { const short = arg.replace(/^-/, "") const pair = Object.entries(options).find(([, v]) => v.short === short) if (pair) { - key = pair[0] as keyof Args + key = pair[0] as keyof UserProvidedArgs } } @@ -307,6 +411,14 @@ export const parse = ( throw new Error("--hashed-password can only be set in the config file or passed in via $HASHED_PASSWORD") } + if (key === "github-auth" && !opts?.configFile) { + throw new Error("--github-auth can only be set in the config file or passed in via $GITHUB_TOKEN") + } + + if (key === "idle-timeout-seconds" && Number(value) <= 60) { + throw new Error("--idle-timeout-seconds must be greater than 60 seconds.") + } + const option = options[key] if (option.type === "boolean") { ;(args[key] as boolean) = true @@ -366,6 +478,10 @@ export const parse = ( } // Everything else goes into _. + if (typeof args._ === "undefined") { + args._ = [] + } + args._.push(arg) } @@ -374,11 +490,28 @@ export const parse = ( throw new Error("--cert-key is missing") } - logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })]) + logger.debug(() => [`parsed ${opts?.configFile ? "config" : "command line"}`, field("args", redactArgs(args))]) return args } +/** + * Redact sensitive information from arguments for logging. + */ +export const redactArgs = (args: UserProvidedArgs): UserProvidedArgs => { + return { + ...args, + password: args.password ? "" : undefined, + "hashed-password": args["hashed-password"] ? "" : undefined, + "github-auth": args["github-auth"] ? "" : undefined, + } +} + +/** + * User-provided arguments with defaults. The distinction between user-provided + * args and defaulted args exists so we can tell the difference between end + * values and what the user actually provided on the command line. + */ export interface DefaultedArgs extends ConfigArgs { auth: AuthType cert?: { @@ -392,6 +525,10 @@ export interface DefaultedArgs extends ConfigArgs { usingEnvHashedPassword: boolean "extensions-dir": string "user-data-dir": string + "session-socket": string + "app-name": string + /* Positional arguments. */ + _: string[] } /** @@ -399,7 +536,7 @@ export interface DefaultedArgs extends ConfigArgs { * with the defaults set. Arguments from the CLI are prioritized over config * arguments. */ -export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise { +export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: ConfigArgs): Promise { const args = Object.assign({}, configArgs || {}, cliArgs) if (!args["user-data-dir"]) { @@ -410,6 +547,11 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args["extensions-dir"] = path.join(args["user-data-dir"], "extensions") } + if (!args["session-socket"]) { + args["session-socket"] = path.join(args["user-data-dir"], "code-server-ipc.sock") + } + process.env.CODE_SERVER_SESSION_SOCKET = args["session-socket"] + // --verbose takes priority over --log and --log takes priority over the // environment variable. if (args.verbose) { @@ -440,7 +582,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.verbose = false break case LogLevel.Warn: - logger.level = Level.Warning + logger.level = Level.Warn args.verbose = false break case LogLevel.Error: @@ -454,20 +596,10 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.auth = AuthType.Password } - const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs) + const addr = bindAddrFromAllSources(configArgs || {}, cliArgs) args.host = addr.host args.port = addr.port - // If we're being exposed to the cloud, we listen on a random address and - // disable auth. - if (args.link) { - args.host = "localhost" - args.port = 0 - args.socket = undefined - args.cert = undefined - args.auth = AuthType.None - } - if (args.cert && !args.cert.value) { const { cert, certKey } = await generateCertificate(args["cert-host"] || "localhost") args.cert = { @@ -481,19 +613,73 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.password = process.env.PASSWORD } + if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) { + args["disable-file-downloads"] = true + } + + if (process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE?.match(/^(1|true)$/)) { + args["disable-getting-started-override"] = true + } + + if (process.env.CS_DISABLE_PROXY?.match(/^(1|true)$/)) { + args["disable-proxy"] = true + } + const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD if (process.env.HASHED_PASSWORD) { args["hashed-password"] = process.env.HASHED_PASSWORD usingEnvPassword = false } + if (process.env.CODE_SERVER_COOKIE_SUFFIX) { + args["cookie-suffix"] = process.env.CODE_SERVER_COOKIE_SUFFIX + } + + if (process.env.GITHUB_TOKEN) { + args["github-auth"] = process.env.GITHUB_TOKEN + } + + if (process.env.CODE_SERVER_RECONNECTION_GRACE_TIME) { + args["reconnection-grace-time"] = process.env.CODE_SERVER_RECONNECTION_GRACE_TIME + } + + if (process.env.CODE_SERVER_IDLE_TIMEOUT_SECONDS) { + if (isNaN(Number(process.env.CODE_SERVER_IDLE_TIMEOUT_SECONDS))) { + logger.info("CODE_SERVER_IDLE_TIMEOUT_SECONDS must be a number") + } + if (Number(process.env.CODE_SERVER_IDLE_TIMEOUT_SECONDS) <= 60) { + throw new Error("--idle-timeout-seconds must be greater than 60 seconds.") + } + args["idle-timeout-seconds"] = Number(process.env.CODE_SERVER_IDLE_TIMEOUT_SECONDS) + } + // Ensure they're not readable by child processes. delete process.env.PASSWORD delete process.env.HASHED_PASSWORD + delete process.env.GITHUB_TOKEN // Filter duplicate proxy domains and remove any leading `*.`. const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, ""))) - args["proxy-domain"] = Array.from(proxyDomains) + const finalProxies = [] + + for (const proxyDomain of proxyDomains) { + if (!proxyDomain.includes("{{port}}")) { + finalProxies.push("{{port}}." + proxyDomain) + } else { + finalProxies.push(proxyDomain) + } + } + + // all proxies are of format anyprefix-{{port}}-anysuffix.{{host}}, where {{host}} is optional + // e.g. code-8080.domain.tld would match for code-{{port}}.domain.tld and code-{{port}}.{{host}} + if (finalProxies.length > 0 && !process.env.VSCODE_PROXY_URI) { + process.env.VSCODE_PROXY_URI = `//${finalProxies[0]}` + } + args["proxy-domain"] = finalProxies + + args["app-name"] ??= process.env.CODE_SERVER_APP_NAME || "code-server" + + args._ = getResolvedPathsFromArgs(args) return { ...args, @@ -502,15 +688,30 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } -async function defaultConfigFile(): Promise { +export function getResolvedPathsFromArgs(args: UserProvidedArgs): string[] { + return (args._ ?? []).map((p) => path.resolve(p)) +} + +/** + * Helper function to return the default config file. + * + * @param {string} password - Password passed in (usually from generatePassword()) + * @returns The default config file: + * + * - bind-addr: 127.0.0.1:8080 + * - auth: password + * - password: + * - cert: false + */ +export function defaultConfigFile(password: string): string { return `bind-addr: 127.0.0.1:8080 auth: password -password: ${await generatePassword()} +password: ${password} cert: false ` } -interface ConfigArgs extends Args { +interface ConfigArgs extends UserProvidedArgs { config: string } @@ -530,11 +731,12 @@ export async function readConfigFile(configPath?: string): Promise { await fs.mkdir(path.dirname(configPath), { recursive: true }) try { - await fs.writeFile(configPath, await defaultConfigFile(), { + const generatedPassword = await generatePassword() + await fs.writeFile(configPath, defaultConfigFile(generatedPassword), { flag: "wx", // wx means to fail if the path exists. }) - logger.info(`Wrote default config file to ${humanPath(configPath)}`) - } catch (error) { + logger.info(`Wrote default config file to ${configPath}`) + } catch (error: any) { // EEXIST is fine; we don't want to overwrite existing configurations. if (error.code !== "EEXIST") { throw error @@ -551,10 +753,10 @@ export async function readConfigFile(configPath?: string): Promise { */ export function parseConfigFile(configFile: string, configPath: string): ConfigArgs { if (!configFile) { - return { _: [], config: configPath } + return { config: configPath } } - const config = yaml.load(configFile, { + const config = load(configFile, { filename: configPath, }) if (!config || typeof config === "string") { @@ -563,12 +765,16 @@ export function parseConfigFile(configFile: string, configPath: string): ConfigA // We convert the config file into a set of flags. // This is a temporary measure until we add a proper CLI library. - const configFileArgv = Object.entries(config).map(([optName, opt]) => { - if (opt === true) { - return `--${optName}` - } - return `--${optName}=${opt}` - }) + const configFileArgv = Object.entries(config) + .map(([optName, opt]) => { + if (opt === true) { + return `--${optName}` + } else if (Array.isArray(opt)) { + return opt.map((o) => `--${optName}=${o}`) + } + return `--${optName}=${opt}` + }) + .flat() const args = parse(configFileArgv, { configFile: configPath, }) @@ -598,11 +804,14 @@ interface Addr { * This function creates the bind address * using the CLI args. */ -export function bindAddrFromArgs(addr: Addr, args: Args): Addr { +export function bindAddrFromArgs(addr: Addr, args: UserProvidedArgs): Addr { addr = { ...addr } if (args["bind-addr"]) { addr = parseBindAddr(args["bind-addr"]) } + if (process.env.CODE_SERVER_HOST) { + addr.host = process.env.CODE_SERVER_HOST + } if (args.host) { addr.host = args.host } @@ -616,7 +825,7 @@ export function bindAddrFromArgs(addr: Addr, args: Args): Addr { return addr } -function bindAddrFromAllSources(...argsConfig: Args[]): Addr { +function bindAddrFromAllSources(...argsConfig: UserProvidedArgs[]): Addr { let addr: Addr = { host: "localhost", port: 8080, @@ -629,62 +838,88 @@ function bindAddrFromAllSources(...argsConfig: Args[]): Addr { return addr } -export const shouldRunVsCodeCli = (args: Args): boolean => { - // Create new interface with only Arg keys - // keyof Args - // Turn that into an array - // Array<...> - type ExtensionArgs = Array - const extensionRelatedArgs: ExtensionArgs = ["list-extensions", "install-extension", "uninstall-extension"] - - const argKeys = Object.keys(args) - - // If any of the extensionRelatedArgs are included in args - // then we don't want to run the vscode cli - return extensionRelatedArgs.some((arg) => argKeys.includes(arg)) -} - /** * Determine if it looks like the user is trying to open a file or folder in an * existing instance. The arguments here should be the arguments the user - * explicitly passed on the command line, not defaults or the configuration. + * explicitly passed on the command line, *NOT DEFAULTS* or the configuration. */ -export const shouldOpenInExistingInstance = async (args: Args): Promise => { +export const shouldOpenInExistingInstance = async ( + args: UserProvidedArgs, + sessionSocket: string, +): Promise => { // Always use the existing instance if we're running from VS Code's terminal. if (process.env.VSCODE_IPC_HOOK_CLI) { + logger.debug("Found VSCODE_IPC_HOOK_CLI") return process.env.VSCODE_IPC_HOOK_CLI } - const readSocketPath = async (): Promise => { - try { - return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8") - } catch (error) { - if (error.code !== "ENOENT") { - throw error - } - } - return undefined - } + const paths = getResolvedPathsFromArgs(args) + const client = new EditorSessionManagerClient(sessionSocket) // If these flags are set then assume the user is trying to open in an - // existing instance since these flags have no effect otherwise. + // existing instance since these flags have no effect otherwise. That means + // if there is no existing instance we should error rather than falling back + // to spawning code-server normally. const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => { - return args[cur as keyof Args] ? prev + 1 : prev + return args[cur as keyof UserProvidedArgs] ? prev + 1 : prev }, 0) if (openInFlagCount > 0) { - return readSocketPath() + logger.debug("Found --reuse-window or --new-window") + const socketPath = await client.getConnectedSocketPath(paths[0]) + if (!socketPath) { + throw new Error(`No opened code-server instances found to handle ${paths[0]}`) + } + return socketPath } // It's possible the user is trying to spawn another instance of code-server. - // Check if any unrelated flags are set (check against one because `_` always - // exists), that a file or directory was passed, and that the socket is - // active. - if (Object.keys(args).length === 1 && args._.length > 0) { - const socketPath = await readSocketPath() - if (socketPath && (await canConnect(socketPath))) { + // 1. Check if any unrelated flags are set (this should only run when + // code-server is invoked exactly like this: `code-server my-file`). + // 2. That a file or directory was passed. + // 3. That the socket is active. + // 4. That an instance exists to handle the path (implied by #3). + if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) { + if (!(await client.canConnect())) { + return undefined + } + const socketPath = await client.getConnectedSocketPath(paths[0]) + if (socketPath) { + logger.debug("Found existing code-server socket") return socketPath } } return undefined } + +/** + * Arguments for running Code's server. + * + * A subset of ../../lib/vscode/src/vs/server/node/serverEnvironmentService.ts:90 + */ +export interface CodeArgs extends UserProvidedCodeArgs { + "accept-server-license-terms"?: boolean + "connection-token"?: string + help: boolean + port?: string + version: boolean + "without-connection-token"?: boolean + "without-browser-env-var"?: boolean + compatibility?: string + log?: string[] +} + +/** + * Convert our arguments to equivalent VS Code server arguments. + * Does not add any extra arguments. + */ +export const toCodeArgs = async (args: DefaultedArgs): Promise => { + return { + ...args, + /** Type casting. */ + help: !!args.help, + version: !!args.version, + port: args.port?.toString(), + log: args.log ? [args.log] : undefined, + } +} diff --git a/src/node/coder_cloud.ts b/src/node/coder_cloud.ts deleted file mode 100644 index 7bca6342a6de..000000000000 --- a/src/node/coder_cloud.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { logger } from "@coder/logger" -import { spawn } from "child_process" -import path from "path" -import split2 from "split2" - -// https://github.com/cdr/coder-cloud -const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") - -function runAgent(...args: string[]): Promise { - logger.debug(`running agent with ${args}`) - - const agent = spawn(coderCloudAgent, args, { - stdio: ["inherit", "inherit", "pipe"], - }) - - agent.stderr.pipe(split2()).on("data", (line) => { - line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") - logger.info(line) - }) - - return new Promise((res, rej) => { - agent.on("error", rej) - - agent.on("close", (code) => { - if (code !== 0) { - rej({ - message: `--link agent exited with ${code}`, - }) - return - } - res() - }) - }) -} - -export function coderCloudBind(csAddr: string, serverName = ""): Promise { - // addr needs to be in host:port format. - // So we trim the protocol. - csAddr = csAddr.replace(/^https?:\/\//, "") - return runAgent("bind", `--code-server-addr=${csAddr}`, serverName) -} diff --git a/src/node/constants.ts b/src/node/constants.ts index d36f9a24a800..06281c6889cf 100644 --- a/src/node/constants.ts +++ b/src/node/constants.ts @@ -1,23 +1,52 @@ import { logger } from "@coder/logger" -import { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package" import * as os from "os" import * as path from "path" -export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJsonFiles { +type PackageJson = { + version: string + commit: string +} + +export function getPackageJson(relativePath: string): PackageJson { let pkg = {} try { pkg = require(relativePath) - } catch (error) { + } catch (error: any) { logger.warn(error.message) } - return pkg + return pkg as PackageJson } -const pkg = getPackageJson("../../package.json") - +export const rootPath = path.resolve(__dirname, "../..") +export const vsRootPath = path.join(rootPath, "lib/vscode") +const PACKAGE_JSON = "package.json" +const pkg = getPackageJson(`${rootPath}/${PACKAGE_JSON}`) +const codePkg = getPackageJson(`${vsRootPath}/${PACKAGE_JSON}`) || { version: "0.0.0" } export const version = pkg.version || "development" export const commit = pkg.commit || "development" -export const rootPath = path.resolve(__dirname, "../..") +export const codeVersion = codePkg.version || "development" export const tmpdir = path.join(os.tmpdir(), "code-server") export const isDevMode = commit === "development" +export const httpProxyUri = + process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy + +/** + * getVersionString returns a human-readable version string suitable + * for outputting to the console. + */ +export function getVersionString(): string { + return [version, commit, "with Code", codeVersion].join(" ") +} + +/** + * getVersionJsonString returns a machine-readable version string + * suitable for outputting to the console. + */ +export function getVersionJsonString(): string { + return JSON.stringify({ + codeServer: version, + commit, + vscode: codeVersion, + }) +} diff --git a/src/node/entry.ts b/src/node/entry.ts index 568718781d62..749b6e966cbc 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -1,20 +1,10 @@ import { logger } from "@coder/logger" -import { - optionDescriptions, - parse, - readConfigFile, - setDefaults, - shouldOpenInExistingInstance, - shouldRunVsCodeCli, -} from "./cli" -import { commit, version } from "./constants" -import { openInExistingInstance, runCodeServer, runVsCodeCli } from "./main" -import * as proxyAgent from "./proxy_agent" +import { optionDescriptions, parse, readConfigFile, setDefaults, shouldOpenInExistingInstance } from "./cli" +import { getVersionString, getVersionJsonString } from "./constants" +import { openInExistingInstance, runCodeServer, runCodeCli, shouldSpawnCliProcess } from "./main" import { isChild, wrapper } from "./wrapper" async function entry(): Promise { - proxyAgent.monkeyPatch(false) - // There's no need to check flags like --help or to spawn in an existing // instance for the child process because these would have already happened in // the parent and the child wouldn't have been spawned. We also get the @@ -24,7 +14,8 @@ async function entry(): Promise { if (isChild(wrapper)) { const args = await wrapper.handshake() wrapper.preventExit() - await runCodeServer(args) + const server = await runCodeServer(args) + wrapper.onDispose(() => server.dispose()) return } @@ -33,9 +24,11 @@ async function entry(): Promise { const args = await setDefaults(cliArgs, configArgs) if (args.help) { - console.log("code-server", version, commit) + console.log("code-server", getVersionString()) console.log("") console.log(`Usage: code-server [options] [path]`) + console.log(` - Opening a directory: code-server ./path/to/your/project`) + console.log(` - Opening a saved workspace: code-server ./path/to/your/project.code-workspace`) console.log("") console.log("Options") optionDescriptions().forEach((description) => { @@ -46,23 +39,21 @@ async function entry(): Promise { if (args.version) { if (args.json) { - console.log({ - codeServer: version, - commit, - vscode: require("../../vendor/modules/code-oss-dev/package.json").version, - }) + console.log(getVersionJsonString()) } else { - console.log(version, commit) + console.log(getVersionString()) } return } - if (shouldRunVsCodeCli(args)) { - return runVsCodeCli(args) + if (shouldSpawnCliProcess(args)) { + logger.debug("Found VS Code arguments; spawning VS Code CLI") + return runCodeCli(args) } - const socketPath = await shouldOpenInExistingInstance(cliArgs) + const socketPath = await shouldOpenInExistingInstance(cliArgs, args["session-socket"]) if (socketPath) { + logger.debug("Trying to open in existing instance") return openInExistingInstance(args, socketPath) } diff --git a/src/node/heart.ts b/src/node/heart.ts index 1eada79b3a70..b78f4edb80d2 100644 --- a/src/node/heart.ts +++ b/src/node/heart.ts @@ -1,5 +1,6 @@ import { logger } from "@coder/logger" import { promises as fs } from "fs" +import { Emitter } from "../common/emitter" /** * Provides a heartbeat using a local file to indicate activity. @@ -8,8 +9,24 @@ export class Heart { private heartbeatTimer?: NodeJS.Timeout private heartbeatInterval = 60000 public lastHeartbeat = 0 + private readonly _onChange = new Emitter<"alive" | "expired" | "unknown">() + readonly onChange = this._onChange.event + private state: "alive" | "expired" | "unknown" = "expired" - public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} + public constructor( + private readonly heartbeatPath: string, + private readonly isActive: () => Promise, + ) { + this.beat = this.beat.bind(this) + this.alive = this.alive.bind(this) + } + + private setState(state: typeof this.state) { + if (this.state !== state) { + this.state = state + this._onChange.emit(this.state) + } + } public alive(): boolean { const now = Date.now() @@ -20,30 +37,38 @@ export class Heart { * timeout and start or reset a timer that keeps running as long as there is * activity. Failures are logged as warnings. */ - public beat(): void { + public async beat(): Promise { if (this.alive()) { + this.setState("alive") return } - logger.trace("heartbeat") - fs.writeFile(this.heartbeatPath, "").catch((error) => { - logger.warn(error.message) - }) + logger.debug("heartbeat") this.lastHeartbeat = Date.now() if (typeof this.heartbeatTimer !== "undefined") { clearTimeout(this.heartbeatTimer) } - this.heartbeatTimer = setTimeout(() => { - this.isActive() - .then((active) => { - if (active) { - this.beat() - } - }) - .catch((error) => { - logger.warn(error.message) - }) + + this.heartbeatTimer = setTimeout(async () => { + try { + if (await this.isActive()) { + this.beat() + } else { + this.setState("expired") + } + } catch (error: unknown) { + logger.warn((error as Error).message) + this.setState("unknown") + } }, this.heartbeatInterval) + + this.setState("alive") + + try { + return await fs.writeFile(this.heartbeatPath, "") + } catch (error: any) { + logger.warn(error.message) + } } /** diff --git a/src/node/http.ts b/src/node/http.ts index d7ffa1f144d4..1429b09a15ef 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -1,13 +1,36 @@ import { field, logger } from "@coder/logger" import * as express from "express" -import * as expressCore from "express-serve-static-core" +import * as http from "http" +import * as net from "net" import qs from "qs" +import { Disposable } from "../common/emitter" import { HttpCode, HttpError } from "../common/http" -import { normalize, Options } from "../common/util" +import { normalize } from "../common/util" import { AuthType, DefaultedArgs } from "./cli" -import { commit, rootPath } from "./constants" +import { version as codeServerVersion } from "./constants" import { Heart } from "./heart" -import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml } from "./util" +import { CoderSettings, SettingsProvider } from "./settings" +import { UpdateProvider } from "./update" +import { + getPasswordMethod, + IsCookieValidArgs, + isCookieValid, + sanitizeString, + escapeHtml, + escapeJSON, + splitOnFirstEquals, +} from "./util" + +/** + * Base options included on every page. + */ +export interface ClientConfiguration { + codeServerVersion: string + /** Relative path from this page to the root. No trailing slash. */ + base: string + /** Relative path from this page to the static root. No trailing slash. */ + csStaticBase: string +} declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -15,10 +38,23 @@ declare global { export interface Request { args: DefaultedArgs heart: Heart + settings: SettingsProvider + updater: UpdateProvider + cookieSessionName: string } } } +export const createClientConfiguration = (req: express.Request): ClientConfiguration => { + const base = relativeRoot(req.originalUrl) + + return { + base, + csStaticBase: base + "/_static", + codeServerVersion, + } +} + /** * Replace common variable strings in HTML templates. */ @@ -27,18 +63,35 @@ export const replaceTemplates = ( content: string, extraOpts?: Omit, ): string => { - const base = relativeRoot(req) - const options: Options = { - base, - csStaticBase: base + "/static/" + commit + rootPath, - logLevel: logger.level, + const serverOptions: ClientConfiguration = { + ...createClientConfiguration(req), ...extraOpts, } + return content .replace(/{{TO}}/g, (typeof req.query.to === "string" && escapeHtml(req.query.to)) || "/") - .replace(/{{BASE}}/g, options.base) - .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) - .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) + .replace(/{{BASE}}/g, serverOptions.base) + .replace(/{{CS_STATIC_BASE}}/g, serverOptions.csStaticBase) + .replace("{{OPTIONS}}", () => escapeJSON(serverOptions)) +} + +/** + * Throw an error if proxy is not enabled. Call `next` if provided. + */ +export const ensureProxyEnabled = (req: express.Request, _?: express.Response, next?: express.NextFunction): void => { + if (!proxyEnabled(req)) { + throw new HttpError("Forbidden", HttpCode.Forbidden) + } + if (next) { + next() + } +} + +/** + * Return true if proxy is enabled. + */ +export const proxyEnabled = (req: express.Request): boolean => { + return !req.args["disable-proxy"] } /** @@ -72,7 +125,7 @@ export const authenticated = async (req: express.Request): Promise => { const passwordMethod = getPasswordMethod(hashedPasswordFromArgs) const isCookieValidArgs: IsCookieValidArgs = { passwordMethod, - cookieKey: sanitizeString(req.cookies.key), + cookieKey: sanitizeString(req.cookies[req.cookieSessionName]), passwordFromArgs: req.args.password || "", hashedPasswordFromArgs: req.args["hashed-password"], } @@ -87,29 +140,52 @@ export const authenticated = async (req: express.Request): Promise => { /** * Get the relative path that will get us to the root of the page. For each - * slash we need to go up a directory. For example: + * slash we need to go up a directory. Will not have a trailing slash. + * + * For example: + * * / => . * /foo => . * /foo/ => ./.. * /foo/bar => ./.. * /foo/bar/ => ./../.. + * + * All paths must be relative in order to work behind a reverse proxy since we + * we do not know the base path. Anything that needs to be absolute (for + * example cookies) must get the base path from the frontend. + * + * All relative paths must be prefixed with the relative root to ensure they + * work no matter the depth at which they happen to appear. + * + * For Express `req.originalUrl` should be used as they remove the base from the + * standard `url` property making it impossible to get the true depth. */ -export const relativeRoot = (req: express.Request): string => { - const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length +export const relativeRoot = (originalUrl: string): string => { + const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) } /** - * Redirect relatively to `/${to}`. Query variables on the current URI will be preserved. - * `to` should be a simple path without any query parameters + * A helper function to construct a redirect path based on + * an Express Request, query and a path to redirect to. + * + * Redirect path is relative to `/${to}`. + */ +export const constructRedirectPath = (req: express.Request, query: qs.ParsedQs, to: string): string => { + const relativePath = normalize(`${relativeRoot(req.originalUrl)}/${to}`, true) + // %2f or %2F are both equalivent to an encoded slash / + const queryString = qs.stringify(query).replace(/%2[fF]/g, "/") + const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}` + + return redirectPath +} + +/** + * Redirect relatively to `/${to}`. Query variables on the current URI will be + * preserved. `to` should be a simple path without any query parameters * `override` will merge with the existing query (use `undefined` to unset). */ -export const redirect = ( - req: express.Request, - res: express.Response, - to: string, - override: expressCore.Query = {}, -): void => { +export const redirect = (req: express.Request, res: express.Response, to: string, override: qs.ParsedQs = {}): void => { const query = Object.assign({}, req.query, override) Object.keys(override).forEach((key) => { if (typeof override[key] === "undefined") { @@ -117,9 +193,7 @@ export const redirect = ( } }) - const relativePath = normalize(`${relativeRoot(req)}/${to}`, true) - const queryString = qs.stringify(query) - const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}` + const redirectPath = constructRedirectPath(req, query, to) logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`) res.redirect(redirectPath) } @@ -157,7 +231,7 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | // default NGINX does this). !host.includes(".") ) { - logger.debug("no valid cookie doman", field("host", host)) + logger.debug("no valid cookie domain", field("host", host)) return undefined } @@ -167,6 +241,199 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | } }) - logger.debug("got cookie doman", field("host", host)) + logger.debug("got cookie domain", field("host", host)) return host || undefined } + +/** + * Return a function capable of fully disposing an HTTP server. + */ +export function disposer(server: http.Server): Disposable["dispose"] { + const sockets = new Set() + let cleanupTimeout: undefined | NodeJS.Timeout + + server.on("connection", (socket) => { + sockets.add(socket) + + socket.on("close", () => { + sockets.delete(socket) + + if (cleanupTimeout && sockets.size === 0) { + clearTimeout(cleanupTimeout) + cleanupTimeout = undefined + } + }) + }) + + return () => { + return new Promise((resolve, reject) => { + // The whole reason we need this disposer is because close will not + // actually close anything; it only prevents future connections then waits + // until everything is closed. + server.close((err) => { + if (err) { + return reject(err) + } + + resolve() + }) + + // If there are sockets remaining we might need to force close them or + // this promise might never resolve. + if (sockets.size > 0) { + // Give sockets a chance to close up shop. + cleanupTimeout = setTimeout(() => { + cleanupTimeout = undefined + + for (const socket of sockets.values()) { + console.warn("a socket was left hanging") + socket.destroy() + } + }, 1000) + } + }) + } +} + +/** + * Get the options for setting a cookie. The options must be identical for + * setting and unsetting cookies otherwise they are considered separate. + */ +export const getCookieOptions = (req: express.Request): express.CookieOptions => { + // Normally we set paths relatively. However browsers do not appear to allow + // cookies to be set relatively which means we need an absolute path. We + // cannot be guaranteed we know the path since a reverse proxy might have + // rewritten it. That means we need to get the path from the frontend. + + // The reason we need to set the path (as opposed to defaulting to /) is to + // avoid code-server instances on different sub-paths clobbering each other or + // from accessing each other's tokens (and to prevent other services from + // accessing code-server's tokens). + + // When logging in or out the request must include the href (the full current + // URL of that page) and the relative path to the root as given to it by the + // backend. Using these two we can determine the true absolute root. + const url = new URL( + req.query.base || req.body?.base || "/", + req.query.href || req.body?.href || "http://" + (req.headers.host || "localhost"), + ) + return { + domain: getCookieDomain(url.host, req.args["proxy-domain"]), + path: normalize(url.pathname) || "/", + sameSite: "lax", + } +} + +/** + * Return the full path to the current page, preserving any trailing slash. + */ +export const self = (req: express.Request): string => { + return normalize(`${req.baseUrl}${req.originalUrl.endsWith("/") ? "/" : ""}`, true) +} + +function getFirstHeader(req: http.IncomingMessage, headerName: string): string | undefined { + const val = req.headers[headerName] + return Array.isArray(val) ? val[0] : val +} + +/** + * Throw a forbidden error if origin checks fail. Call `next` if provided. + */ +export function ensureOrigin(req: express.Request, _?: express.Response, next?: express.NextFunction): void { + try { + authenticateOrigin(req) + if (next) { + next() + } + } catch (error) { + logger.debug(`${error instanceof Error ? error.message : error}; blocking request to ${req.originalUrl}`) + throw new HttpError("Forbidden", HttpCode.Forbidden) + } +} + +/** + * Return true if the origin matches any trusted origin. Entries are matched + * as exact strings, the special wildcard `"*"`, or `*.example.com`-style + * domain wildcards (same as --proxy-domain). + */ +export function isTrustedOrigin(origin: string, trustedOrigins: string[]): boolean { + return trustedOrigins.some((trusted) => { + if (trusted === "*" || trusted === origin) { + return true + } + // *.example.com style: match origin if it is the domain or a subdomain + if (trusted.startsWith("*.")) { + const domain = trusted.slice(2).toLowerCase() + return origin === domain || origin.endsWith("." + domain) + } + return false + }) +} + +/** + * Authenticate the request origin against the host. Throw if invalid. + */ +export function authenticateOrigin(req: express.Request): void { + // A missing origin probably means the source is non-browser. Not sure we + // have a use case for this but let it through. + const originRaw = getFirstHeader(req, "origin") + if (!originRaw) { + return + } + + let origin: string + try { + origin = new URL(originRaw).host.trim().toLowerCase() + } catch (error) { + throw new Error(`unable to parse malformed origin "${originRaw}"`) + } + + const trustedOrigins = req.args["trusted-origins"] || [] + if (isTrustedOrigin(origin, trustedOrigins)) { + return + } + + const host = getHost(req) + if (typeof host === "undefined") { + // A missing host likely means the reverse proxy has not been configured to + // forward the host which means we cannot perform the check. Emit an error + // so an admin can fix the issue. + logger.error("No host headers found") + logger.error("Are you behind a reverse proxy that does not forward the host?") + throw new Error("no host headers found") + } + + if (host !== origin) { + throw new Error(`host "${host}" does not match origin "${origin}"`) + } +} + +/** + * Get the host from headers. It will be trimmed and lowercased. + */ +export function getHost(req: express.Request): string | undefined { + // Honor Forwarded if present. + const forwardedRaw = getFirstHeader(req, "forwarded") + if (forwardedRaw) { + const parts = forwardedRaw.split(/[;,]/) + for (let i = 0; i < parts.length; ++i) { + const [key, value] = splitOnFirstEquals(parts[i]) + if (key.trim().toLowerCase() === "host" && value) { + return value.trim().toLowerCase() + } + } + } + + // Honor X-Forwarded-Host if present. Some reverse proxies will set multiple + // comma-separated hosts. + const xHost = getFirstHeader(req, "x-forwarded-host") + if (xHost) { + const firstXHost = xHost.split(",")[0] + if (firstXHost) { + return firstXHost.trim().toLowerCase() + } + } + + const host = getFirstHeader(req, "host") + return host ? host.trim().toLowerCase() : undefined +} diff --git a/src/node/i18n/index.ts b/src/node/i18n/index.ts new file mode 100644 index 000000000000..e8186067ba98 --- /dev/null +++ b/src/node/i18n/index.ts @@ -0,0 +1,59 @@ +import { promises as fs } from "fs" +import i18next, { init } from "i18next" +import * as en from "./locales/en.json" +import * as ja from "./locales/ja.json" +import * as th from "./locales/th.json" +import * as ur from "./locales/ur.json" +import * as zhCn from "./locales/zh-cn.json" + +const defaultResources = { + en: { + translation: en, + }, + "zh-cn": { + translation: zhCn, + }, + th: { + translation: th, + }, + ja: { + translation: ja, + }, + ur: { + translation: ur, + }, +} + +export async function loadCustomStrings(filePath: string): Promise { + try { + // Read custom strings from file path only + const fileContent = await fs.readFile(filePath, "utf8") + const customStringsData = JSON.parse(fileContent) + + // User-provided strings override all languages. + Object.keys(defaultResources).forEach((locale) => { + i18next.addResourceBundle(locale, "translation", customStringsData) + }) + } catch (error) { + if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { + throw new Error(`Custom strings file not found: ${filePath}\nPlease ensure the file exists and is readable.`) + } else if (error instanceof SyntaxError) { + throw new Error(`Invalid JSON in custom strings file: ${filePath}\n${error.message}`) + } else { + throw new Error( + `Failed to load custom strings from ${filePath}: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } +} + +init({ + lng: "en", + fallbackLng: "en", // language to use if translations in user language are not available. + returnNull: false, + lowerCaseLng: true, + debug: process.env.NODE_ENV === "development", + resources: defaultResources, +}) + +export default i18next diff --git a/src/node/i18n/locales/en.json b/src/node/i18n/locales/en.json new file mode 100644 index 000000000000..150bacc7b817 --- /dev/null +++ b/src/node/i18n/locales/en.json @@ -0,0 +1,13 @@ +{ + "LOGIN_TITLE": "{{app}} login", + "LOGIN_BELOW": "Please log in below.", + "WELCOME": "Welcome to {{app}}", + "LOGIN_PASSWORD": "Check the config file for the password.", + "LOGIN_USING_ENV_PASSWORD": "Password was set from $PASSWORD.", + "LOGIN_USING_HASHED_PASSWORD": "Password was set from $HASHED_PASSWORD.", + "SUBMIT": "SUBMIT", + "PASSWORD_PLACEHOLDER": "PASSWORD", + "LOGIN_RATE_LIMIT": "Login rate limited!", + "MISS_PASSWORD": "Missing password", + "INCORRECT_PASSWORD": "Incorrect password" +} diff --git a/src/node/i18n/locales/ja.json b/src/node/i18n/locales/ja.json new file mode 100644 index 000000000000..5abba920c09f --- /dev/null +++ b/src/node/i18n/locales/ja.json @@ -0,0 +1,13 @@ +{ + "LOGIN_TITLE": "{{app}} ログイン", + "LOGIN_BELOW": "以下によりログインしてください。", + "WELCOME": "ようこそ {{app}} へ!", + "LOGIN_PASSWORD": "パスワードは設定ファイルを確認してください。", + "LOGIN_USING_ENV_PASSWORD": "パスワードは環境変数 $PASSWORD で設定されています。", + "LOGIN_USING_HASHED_PASSWORD": "パスワードは環境変数 $HASHED_PASSWORD で設定されています。", + "SUBMIT": "実行", + "PASSWORD_PLACEHOLDER": "パスワード", + "LOGIN_RATE_LIMIT": "ログイン制限を超えました!", + "MISS_PASSWORD": "パスワードを入力してください。", + "INCORRECT_PASSWORD": "パスワードが間違っています。" +} diff --git a/src/node/i18n/locales/th.json b/src/node/i18n/locales/th.json new file mode 100644 index 000000000000..f6840c773bdb --- /dev/null +++ b/src/node/i18n/locales/th.json @@ -0,0 +1,13 @@ +{ + "LOGIN_TITLE": "เข้าสู่ระบบ {{app}}", + "LOGIN_BELOW": "กรุณาเข้าสู่ระบบด้านล่าง", + "WELCOME": "ยินดีต้อนรับสู่ {{app}}", + "LOGIN_PASSWORD": "ตรวจสอบไฟล์กำหนดค่าเพื่อดูรหัสผ่าน", + "LOGIN_USING_ENV_PASSWORD": "รหัสผ่านถูกกำหนดเป็น $PASSWORD", + "LOGIN_USING_HASHED_PASSWORD": "รรหัสผ่านถูกกำหนดเป็น $HASHED_PASSWORD", + "SUBMIT": "ส่ง", + "PASSWORD_PLACEHOLDER": "รหัสผ่าน", + "LOGIN_RATE_LIMIT": "ถึงขีดจำกัดอัตราการเข้าสู่ระบบ!", + "MISS_PASSWORD": "รหัสผ่านหายไป", + "INCORRECT_PASSWORD": "รหัสผ่านไม่ถูกต้อง" +} diff --git a/src/node/i18n/locales/ur.json b/src/node/i18n/locales/ur.json new file mode 100644 index 000000000000..3560b36ba50d --- /dev/null +++ b/src/node/i18n/locales/ur.json @@ -0,0 +1,13 @@ +{ + "LOGIN_TITLE": "{{app}} لاگ ان کریں", + "LOGIN_BELOW": "براہ کرم نیچے لاگ ان کریں۔", + "WELCOME": "میں خوش آمدید {{app}}", + "LOGIN_PASSWORD": "پاس ورڈ کے لیے کنفگ فائل چیک کریں۔", + "LOGIN_USING_ENV_PASSWORD": "پاس ورڈ $PASSWORD سے سیٹ کیا گیا تھا۔", + "LOGIN_USING_HASHED_PASSWORD": "پاس ورڈ $HASHED_PASSWORD سے سیٹ کیا گیا تھا۔", + "SUBMIT": "جمع کرائیں", + "PASSWORD_PLACEHOLDER": "پاس ورڈ", + "LOGIN_RATE_LIMIT": "لاگ ان کی شرح محدود!", + "MISS_PASSWORD": "پاس ورڈ غائب ہے۔", + "INCORRECT_PASSWORD": "غلط پاس ورڈ" +} diff --git a/src/node/i18n/locales/zh-cn.json b/src/node/i18n/locales/zh-cn.json new file mode 100644 index 000000000000..7d9f2fc21211 --- /dev/null +++ b/src/node/i18n/locales/zh-cn.json @@ -0,0 +1,13 @@ +{ + "LOGIN_TITLE": "{{app}} 登录", + "LOGIN_BELOW": "请在下面登录。", + "WELCOME": "欢迎来到 {{app}}", + "LOGIN_PASSWORD": "查看配置文件中的密码。", + "LOGIN_USING_ENV_PASSWORD": "密码在 $PASSWORD 中设置。", + "LOGIN_USING_HASHED_PASSWORD": "密码在 $HASHED_PASSWORD 中设置。", + "SUBMIT": "提交", + "PASSWORD_PLACEHOLDER": "密码", + "LOGIN_RATE_LIMIT": "登录速率限制!", + "MISS_PASSWORD": "缺少密码", + "INCORRECT_PASSWORD": "密码不正确" +} diff --git a/src/node/main.ts b/src/node/main.ts index 1e9569faef47..c2d3bd57852b 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -1,38 +1,78 @@ import { field, logger } from "@coder/logger" -import * as cp from "child_process" import http from "http" +import * as os from "os" import * as path from "path" -import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc" +import { Disposable } from "../common/emitter" import { plural } from "../common/util" import { createApp, ensureAddress } from "./app" -import { AuthType, DefaultedArgs, Feature } from "./cli" -import { coderCloudBind } from "./coder_cloud" -import { commit, version } from "./constants" +import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli" +import { commit, version, vsRootPath } from "./constants" +import { loadCustomStrings } from "./i18n" import { register } from "./routes" -import { humanPath, isFile, open } from "./util" - -export const runVsCodeCli = (args: DefaultedArgs): void => { - logger.debug("forking vs code cli...") - const vscode = cp.fork(path.resolve(__dirname, "../../vendor/modules/code-oss-dev/out/vs/server/fork"), [], { - env: { - ...process.env, - CODE_SERVER_PARENT_PID: process.pid.toString(), - }, - }) - vscode.once("message", (message: any) => { - logger.debug("got message from VS Code", field("message", message)) - if (message.type !== "ready") { - logger.error("Unexpected response waiting for ready response", field("type", message.type)) - process.exit(1) +import { VSCodeModule } from "./routes/vscode" +import { isDirectory, open } from "./util" +import { wrapper } from "./wrapper" + +/** + * Return true if the user passed an extension-related VS Code flag. + */ +export const shouldSpawnCliProcess = (args: UserProvidedArgs): boolean => { + return ( + !!args["list-extensions"] || + !!args["install-extension"] || + !!args["uninstall-extension"] || + !!args["locate-extension"] + ) +} + +/** + * This is copy of OpenCommandPipeArgs from + * ../../lib/vscode/src/vs/workbench/api/node/extHostCLIServer.ts:15 + * + * Arguments supported by Code's socket. It can be used to perform actions from + * the CLI in a running instance of Code (for example to open a file). + * + * TODO: Can we import this (and other types) directly? + */ +export interface OpenCommandPipeArgs { + type: "open" + fileURIs?: string[] + folderURIs: string[] + forceNewWindow?: boolean + diffMode?: boolean + addMode?: boolean + gotoLineMode?: boolean + forceReuseWindow?: boolean + waitMarkerFilePath?: string +} + +/** + * Run Code's CLI for things like managing extensions. + */ +export const runCodeCli = async (args: DefaultedArgs): Promise => { + logger.debug("Running Code CLI") + try { + // See vscode.loadVSCode for more on this jank. + process.env.CODE_SERVER_PARENT_PID = process.pid.toString() + let modPath = path.join(vsRootPath, "out/server-main.js") + if (os.platform() === "win32") { + // On Windows, absolute paths of ESM modules must be a valid file URI. + modPath = "file:///" + modPath.replace(/\\/g, "/") } - const send: CliMessage = { type: "cli", args } - vscode.send(send) - }) - vscode.once("error", (error) => { - logger.error("Got error from VS Code", field("error", error)) + const mod = (await eval(`import("${modPath}")`)) as VSCodeModule + const serverModule = await mod.loadCodeWithNls() + await serverModule.spawnCli(await toCodeArgs(args)) + // Rather than have the caller handle errors and exit, spawnCli will exit + // itself. Additionally, it does this on a timeout set to 0. So, try + // waiting for VS Code to exit before giving up and doing it ourselves. + await new Promise((r) => setTimeout(r, 1000)) + logger.warn("Code never exited") + process.exit(0) + } catch (error: any) { + // spawnCli catches all errors, but just in case that changes. + logger.error("Got error from Code", error) process.exit(1) - }) - vscode.on("exit", (code) => process.exit(code || 0)) + } } export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise => { @@ -42,27 +82,24 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st fileURIs: [], forceReuseWindow: args["reuse-window"], forceNewWindow: args["new-window"], + gotoLineMode: true, } - for (let i = 0; i < args._.length; i++) { - const fp = path.resolve(args._[i]) - if (await isFile(fp)) { - pipeArgs.fileURIs.push(fp) - } else { + const fp = args._[i] + if (await isDirectory(fp)) { pipeArgs.folderURIs.push(fp) + } else { + pipeArgs.fileURIs.push(fp) } } - if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) { logger.error("--new-window can only be used with folder paths") process.exit(1) } - if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) { logger.error("Please specify at least one file or folder") process.exit(1) } - const vscode = http.request( { path: "/", @@ -71,22 +108,30 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st }, (response) => { response.on("data", (message) => { - logger.debug("got message from VS Code", field("message", message.toString())) + logger.debug("got message from Code", field("message", message.toString())) }) }, ) vscode.on("error", (error: unknown) => { - logger.error("got error from VS Code", field("error", error)) + logger.error("got error from Code", field("error", error)) }) vscode.write(JSON.stringify(pipeArgs)) vscode.end() } -export const runCodeServer = async (args: DefaultedArgs): Promise => { +export const runCodeServer = async ( + args: DefaultedArgs, +): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => { logger.info(`code-server ${version} ${commit}`) - logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) - logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) + // Load custom strings if provided + if (args.i18n) { + await loadCustomStrings(args.i18n) + logger.info("Loaded custom strings") + } + + logger.info(`Using user-data-dir ${args["user-data-dir"]}`) + logger.debug(`Using extensions-dir ${args["extensions-dir"]}`) if (args.auth === AuthType.Password && !args.password && !args["hashed-password"]) { throw new Error( @@ -94,39 +139,76 @@ export const runCodeServer = async (args: DefaultedArgs): Promise = ) } - const [app, wsApp, server] = await createApp(args) - const serverAddress = ensureAddress(server) - await register(app, wsApp, server, args) + const app = await createApp(args) + const protocol = args.cert ? "https" : "http" + const serverAddress = ensureAddress(app.server, protocol) + const { disposeRoutes, heart } = await register(app, args) - logger.info(`Using config file ${humanPath(args.config)}`) - logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) + logger.info(`Using config file ${args.config}`) + logger.info(`${protocol.toUpperCase()} server listening on ${serverAddress.toString()}`) if (args.auth === AuthType.Password) { logger.info(" - Authentication is enabled") if (args.usingEnvPassword) { logger.info(" - Using password from $PASSWORD") } else if (args.usingEnvHashedPassword) { logger.info(" - Using password from $HASHED_PASSWORD") + } else if (args["hashed-password"]) { + logger.info(` - Using hashed-password from ${args.config}`) } else { - logger.info(` - Using password from ${humanPath(args.config)}`) + logger.info(` - Using password from ${args.config}`) } } else { - logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`) + logger.info(" - Authentication is disabled") } if (args.cert) { - logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`) + logger.info(` - Using certificate for HTTPS: ${args.cert.value}`) } else { - logger.info(` - Not serving HTTPS ${args.link ? "(disabled by --link)" : ""}`) + logger.info(" - Not serving HTTPS") } - if (args["proxy-domain"].length > 0) { + if (args["idle-timeout-seconds"]) { + logger.info(` - Idle timeout set to ${args["idle-timeout-seconds"]} seconds`) + + let idleShutdownTimer: NodeJS.Timeout | undefined + const startIdleShutdownTimer = () => { + idleShutdownTimer = setTimeout(() => { + logger.warn(`Idle timeout of ${args["idle-timeout-seconds"]} seconds exceeded`) + wrapper.exit(0) + }, args["idle-timeout-seconds"]! * 1000) + } + + startIdleShutdownTimer() + + heart.onChange((state) => { + clearTimeout(idleShutdownTimer) + if (state === "expired") { + startIdleShutdownTimer() + } + }) + } + + if (args["disable-proxy"]) { + logger.info(" - Proxy disabled") + } else if (args["proxy-domain"].length > 0) { logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`) - args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`)) + args["proxy-domain"].forEach((domain) => logger.info(` - ${domain}`)) + } + if (args["skip-auth-preflight"]) { + logger.info(" - Skipping authentication for preflight requests") + } + if (process.env.VSCODE_PROXY_URI) { + logger.info(`Using proxy URI in PORTS tab: ${process.env.VSCODE_PROXY_URI}`) } - if (args.link) { - await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) - logger.info(" - Connected to cloud agent") + const sessionServerAddress = app.editorSessionManagerServer.address() + if (sessionServerAddress) { + logger.info(`Session server listening on ${sessionServerAddress.toString()}`) + } + + if (process.env.EXTENSIONS_GALLERY) { + logger.info("Using custom extensions gallery") + logger.debug(` - ${process.env.EXTENSIONS_GALLERY}`) } if (args.enable && args.enable.length > 0) { @@ -144,16 +226,20 @@ export const runCodeServer = async (args: DefaultedArgs): Promise = ) } - if (!args.socket && args.open) { - // The web socket doesn't seem to work if browsing with 0.0.0.0. - const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") + if (args.open) { try { - await open(openAddress) - logger.info(`Opened ${openAddress}`) + await open(serverAddress) + logger.info(`Opened ${serverAddress}`) } catch (error) { - logger.error("Failed to open", field("address", openAddress), field("error", error)) + logger.error("Failed to open", field("address", serverAddress.toString()), field("error", error)) } } - return server + return { + server: app.server, + dispose: async () => { + disposeRoutes() + await app.dispose() + }, + } } diff --git a/src/node/plugin.ts b/src/node/plugin.ts deleted file mode 100644 index 036e118e88c9..000000000000 --- a/src/node/plugin.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { field, Level, Logger } from "@coder/logger" -import * as express from "express" -import * as fs from "fs" -import * as path from "path" -import * as semver from "semver" -import * as pluginapi from "../../typings/pluginapi" -import { HttpCode, HttpError } from "../common/http" -import { version } from "./constants" -import { authenticated, ensureAuthenticated, replaceTemplates } from "./http" -import { proxy } from "./proxy" -import * as util from "./util" -import { Router as WsRouter, WebsocketRouter, wss } from "./wsRouter" -const fsp = fs.promises - -// Represents a required module which could be anything. -type Module = any - -/** - * Inject code-server when `require`d. This is required because the API provides - * more than just types so these need to be provided at run-time. - */ -const originalLoad = require("module")._load -require("module")._load = function (request: string, parent: object, isMain: boolean): Module { - return request === "code-server" ? codeServer : originalLoad.apply(this, [request, parent, isMain]) -} - -/** - * The module you get when importing "code-server". - */ -export const codeServer = { - HttpCode, - HttpError, - Level, - authenticated, - ensureAuthenticated, - express, - field, - proxy, - replaceTemplates, - WsRouter, - wss, -} - -interface Plugin extends pluginapi.Plugin { - /** - * These fields are populated from the plugin's package.json - * and now guaranteed to exist. - */ - name: string - version: string - - /** - * path to the node module on the disk. - */ - modulePath: string -} - -interface Application extends pluginapi.Application { - /* - * Clone of the above without functions. - */ - plugin: Omit -} - -/** - * PluginAPI implements the plugin API described in typings/pluginapi.d.ts - * Please see that file for details. - */ -export class PluginAPI { - private readonly plugins = new Map() - private readonly logger: Logger - - public constructor( - logger: Logger, - /** - * These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively. - */ - private readonly csPlugin = "", - private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, - private readonly workingDirectory: string | undefined = undefined, - ) { - this.logger = logger.named("pluginapi") - } - - /** - * applications grabs the full list of applications from - * all loaded plugins. - */ - public async applications(): Promise { - const apps = new Array() - for (const [, p] of this.plugins) { - if (!p.applications) { - continue - } - const pluginApps = await p.applications() - - // Add plugin key to each app. - apps.push( - ...pluginApps.map((app) => { - app = { ...app, path: path.join(p.routerPath, app.path || "") } - app = { ...app, iconPath: path.join(app.path || "", app.iconPath) } - return { - ...app, - plugin: { - name: p.name, - version: p.version, - modulePath: p.modulePath, - - displayName: p.displayName, - description: p.description, - routerPath: p.routerPath, - homepageURL: p.homepageURL, - }, - } - }), - ) - } - return apps - } - - /** - * mount mounts all plugin routers onto r and websocket routers onto wr. - */ - public mount(r: express.Router, wr: express.Router): void { - for (const [, p] of this.plugins) { - if (p.router) { - r.use(`${p.routerPath}`, p.router()) - } - if (p.wsRouter) { - wr.use(`${p.routerPath}`, (p.wsRouter() as WebsocketRouter).router) - } - } - } - - /** - * loadPlugins loads all plugins based on this.csPlugin, - * this.csPluginPath and the built in plugins. - */ - public async loadPlugins(loadBuiltin = true): Promise { - for (const dir of this.csPlugin.split(":")) { - if (!dir) { - continue - } - await this.loadPlugin(dir) - } - - for (const dir of this.csPluginPath.split(":")) { - if (!dir) { - continue - } - await this._loadPlugins(dir) - } - - if (loadBuiltin) { - await this._loadPlugins(path.join(__dirname, "../../plugins")) - } - } - - /** - * _loadPlugins is the counterpart to loadPlugins. - * - * It differs in that it loads all plugins in a single - * directory whereas loadPlugins uses all available directories - * as documented. - */ - private async _loadPlugins(dir: string): Promise { - try { - const entries = await fsp.readdir(dir, { withFileTypes: true }) - for (const ent of entries) { - if (!ent.isDirectory()) { - continue - } - await this.loadPlugin(path.join(dir, ent.name)) - } - } catch (err) { - if (err.code !== "ENOENT") { - this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`) - } - } - } - - private async loadPlugin(dir: string): Promise { - try { - const str = await fsp.readFile(path.join(dir, "package.json"), { - encoding: "utf8", - }) - const packageJSON: PackageJSON = JSON.parse(str) - for (const [, p] of this.plugins) { - if (p.name === packageJSON.name) { - this.logger.warn( - `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, - ) - return - } - } - const p = this._loadPlugin(dir, packageJSON) - this.plugins.set(p.name, p) - } catch (err) { - if (err.code !== "ENOENT") { - this.logger.warn(`failed to load plugin: ${err.stack}`) - } - } - } - - /** - * _loadPlugin is the counterpart to loadPlugin and actually - * loads the plugin now that we know there is no duplicate - * and that the package.json has been read. - */ - private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { - dir = path.resolve(dir) - - const logger = this.logger.named(packageJSON.name) - logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) - - if (!packageJSON.name) { - throw new Error("plugin package.json missing name") - } - if (!packageJSON.version) { - throw new Error("plugin package.json missing version") - } - if (!packageJSON.engines || !packageJSON.engines["code-server"]) { - throw new Error(`plugin package.json missing code-server range like: - "engines": { - "code-server": "^3.7.0" - } -`) - } - if (!semver.satisfies(version, packageJSON.engines["code-server"])) { - throw new Error( - `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, - ) - } - - const pluginModule = require(dir) - if (!pluginModule.plugin) { - throw new Error("plugin module does not export a plugin") - } - - const p = { - name: packageJSON.name, - version: packageJSON.version, - modulePath: dir, - ...pluginModule.plugin, - } as Plugin - - if (!p.displayName) { - throw new Error("plugin missing displayName") - } - if (!p.description) { - throw new Error("plugin missing description") - } - if (!p.routerPath) { - throw new Error("plugin missing router path") - } - if (!p.routerPath.startsWith("/")) { - throw new Error(`plugin router path ${q(p.routerPath)}: invalid`) - } - if (!p.homepageURL) { - throw new Error("plugin missing homepage") - } - - p.init({ - logger: logger, - workingDirectory: this.workingDirectory, - }) - - logger.debug("loaded") - - return p - } - - public async dispose(): Promise { - await Promise.all( - Array.from(this.plugins.values()).map(async (p) => { - if (!p.deinit) { - return - } - try { - await p.deinit() - } catch (error) { - this.logger.error("plugin failed to deinit", field("name", p.name), field("error", error.message)) - } - }), - ) - } -} - -interface PackageJSON { - name: string - version: string - engines: { - "code-server": string - } -} - -function q(s: string | undefined): string { - if (s === undefined) { - s = "undefined" - } - return JSON.stringify(s) -} diff --git a/src/node/proxy.ts b/src/node/proxy.ts index c03d3d5d4cbf..2d9056996d53 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,11 +1,50 @@ +import * as cookie from "cookie" +import type { Request } from "express" import proxyServer from "http-proxy" -import { HttpCode } from "../common/http" +import { getCookieSessionName, HttpCode } from "../common/http" export const proxy = proxyServer.createProxyServer({}) +// The error handler catches when the proxy fails to connect (for example when +// there is nothing running on the target port). proxy.on("error", (error, _, res) => { - res.writeHead(HttpCode.ServerError) - res.end(error.message) + // This could be for either a web socket or a regular request. Despite what + // the types say, writeHead() will not exist on web socket requests (nor will + // status() from Express). But writing out the code manually does not work + // for regular requests thus the branching behavior. + if (typeof res.writeHead !== "undefined") { + res.writeHead(HttpCode.ServerError) + res.end(error.message) + } else { + res.end(`HTTP/1.1 ${HttpCode.ServerError} ${error.message}\r\n\r\n`) + } +}) + +function identity(val: T): T { + return val +} + +// Strip the code-server cookie if it exists to avoid transmitting the cookie +// to potentially malicious local ports. +proxy.on("proxyReq", (preq, req) => { + if (req.headers.cookie) { + const cookieSessionName = getCookieSessionName((req as Request).args["cookie-suffix"]) + // Encoding and decoding are no-ops; we just want to remove the token + // without changing anything else about the cookies because not all + // applications encode/decode the same way `cookie` here does. + preq.setHeader( + "Cookie", + cookie.stringifyCookie( + { + ...cookie.parseCookie(req.headers.cookie, { decode: identity }), + [cookieSessionName]: undefined, + }, + { + encode: identity, + }, + ), + ) + } }) // Intercept the response to rewrite absolute redirects against the base path. diff --git a/src/node/proxy_agent.ts b/src/node/proxy_agent.ts deleted file mode 100644 index 39607c8da81f..000000000000 --- a/src/node/proxy_agent.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { logger } from "@coder/logger" -import * as http from "http" -import * as proxyAgent from "proxy-agent" -import * as proxyFromEnv from "proxy-from-env" - -/** - * This file has nothing to do with the code-server proxy. - * It is to support $HTTP_PROXY, $HTTPS_PROXY and $NO_PROXY. - * - * - https://github.com/cdr/code-server/issues/124 - * - https://www.npmjs.com/package/proxy-agent - * - https://www.npmjs.com/package/proxy-from-env - * - * This file exists in two locations: - * - src/node/proxy_agent.ts - * - lib/vscode/src/vs/base/node/proxy_agent.ts - * The second is a symlink to the first. - */ - -/** - * monkeyPatch patches the node http,https modules to route all requests through the - * agent we get from the proxy-agent package. - * - * This approach only works if there is no code specifying an explicit agent when making - * a request. - * - * None of our code ever passes in a explicit agent to the http,https modules. - * VS Code's does sometimes but only when a user sets the http.proxy configuration. - * See https://code.visualstudio.com/docs/setup/network#_legacy-proxy-server-support - * - * Even if they do, it's probably the same proxy so we should be fine! And those knobs - * are deprecated anyway. - */ -export function monkeyPatch(inVSCode: boolean): void { - if (shouldEnableProxy()) { - const http = require("http") - const https = require("https") - - // If we do not pass in a proxy URL, proxy-agent will get the URL from the environment. - // See https://www.npmjs.com/package/proxy-from-env. - // Also see shouldEnableProxy. - const pa = newProxyAgent(inVSCode) - http.globalAgent = pa - https.globalAgent = pa - } -} - -function newProxyAgent(inVSCode: boolean): http.Agent { - // The reasoning for this split is that VS Code's build process does not have - // esModuleInterop enabled but the code-server one does. As a result depending on where - // we execute, we either have a default attribute or we don't. - // - // I can't enable esModuleInterop in VS Code's build process as it breaks and spits out - // a huge number of errors. And we can't use require as otherwise the modules won't be - // included in the final product. - if (inVSCode) { - return new (proxyAgent as any)() - } else { - return new (proxyAgent as any).default() - } -} - -// If they have $NO_PROXY set to example.com then this check won't work! -// But that's drastically unlikely. -export function shouldEnableProxy(): boolean { - let shouldEnable = false - - const httpProxy = proxyFromEnv.getProxyForUrl(`http://example.com`) - if (httpProxy) { - shouldEnable = true - logger.debug(`using $HTTP_PROXY ${httpProxy}`) - } - - const httpsProxy = proxyFromEnv.getProxyForUrl(`https://example.com`) - if (httpsProxy) { - shouldEnable = true - logger.debug(`using $HTTPS_PROXY ${httpsProxy}`) - } - - return shouldEnable -} diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts deleted file mode 100644 index 5c8541fc9ad4..000000000000 --- a/src/node/routes/apps.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as express from "express" -import { PluginAPI } from "../plugin" - -/** - * Implements the /api/applications endpoint - * - * See typings/pluginapi.d.ts for details. - */ -export function router(papi: PluginAPI): express.Router { - const router = express.Router() - - router.get("/", async (req, res) => { - res.json(await papi.applications()) - }) - - return router -} diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 56b0ea1bb37f..6ffee67fa002 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -1,48 +1,76 @@ import { Request, Router } from "express" import { HttpCode, HttpError } from "../../common/http" -import { normalize } from "../../common/util" -import { authenticated, ensureAuthenticated, redirect } from "../http" +import { getHost, ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" export const router = Router() +const proxyDomainToRegex = (matchString: string): RegExp => { + const escapedMatchString = matchString.replace(/[.*+?^$()|[\]\\]/g, "\\$&") + + // Replace {{port}} with a regex group to capture the port + // Replace {{host}} with .+ to allow any host match (so rely on DNS record here) + let regexString = escapedMatchString.replace("{{port}}", "(\\d+)") + regexString = regexString.replace("{{host}}", ".+") + + regexString = regexString.replace(/[{}]/g, "\\$&") //replace any '{}' that might be left + + return new RegExp("^" + regexString + "$") +} + +let proxyRegexes: RegExp[] = [] +const proxyDomainsToRegex = (proxyDomains: string[]): RegExp[] => { + if (proxyDomains.length !== proxyRegexes.length) { + proxyRegexes = proxyDomains.map(proxyDomainToRegex) + } + return proxyRegexes +} + /** - * Return the port if the request should be proxied. Anything that ends in a - * proxy domain and has a *single* subdomain should be proxied. Anything else - * should return `undefined` and will be handled as normal. + * Return the port if the request should be proxied. + * + * The proxy-domain should be of format anyprefix-{{port}}-anysuffix.{{host}}, where {{host}} is optional + * e.g. code-8080.domain.tld would match for code-{{port}}.domain.tld and code-{{port}}.{{host}}. * - * For example if `coder.com` is specified `8080.coder.com` will be proxied - * but `8080.test.coder.com` and `test.8080.coder.com` will not. */ const maybeProxy = (req: Request): string | undefined => { - // Split into parts. - const host = req.headers.host || "" - const idx = host.indexOf(":") - const domain = idx !== -1 ? host.substring(0, idx) : host - const parts = domain.split(".") - - // There must be an exact match. - const port = parts.shift() - const proxyDomain = parts.join(".") - if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { + const reqDomain = getHost(req) + if (reqDomain === undefined) { return undefined } - return port + const regexs = proxyDomainsToRegex(req.args["proxy-domain"]) + + for (const regex of regexs) { + const match = reqDomain.match(regex) + + if (match) { + return match[1] // match[1] contains the port + } + } + + return undefined } -router.all("*", async (req, res, next) => { +router.all(/.*/, async (req, res, next) => { const port = maybeProxy(req) if (!port) { return next() } + ensureProxyEnabled(req) + + if (req.method === "OPTIONS" && req.args["skip-auth-preflight"]) { + // Allow preflight requests with `skip-auth-preflight` flag + return next() + } + // Must be authenticated to use the proxy. const isAuthenticated = await authenticated(req) if (!isAuthenticated) { // Let the assets through since they're used on the login page. - if (req.path.startsWith("/static/") && req.method === "GET") { + if (req.path.startsWith("/_static/") && req.method === "GET") { return next() } @@ -56,7 +84,7 @@ router.all("*", async (req, res, next) => { return next() } // Redirect all other pages to the login. - const to = normalize(`${req.baseUrl}${req.path}`) + const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined, }) @@ -74,15 +102,15 @@ router.all("*", async (req, res, next) => { export const wsRouter = WsRouter() -wsRouter.ws("*", async (req, _, next) => { +wsRouter.ws(/.*/, async (req, _, next) => { const port = maybeProxy(req) if (!port) { return next() } - // Must be authenticated to use the proxy. + ensureProxyEnabled(req) + ensureOrigin(req) await ensureAuthenticated(req) - proxy.ws(req, req.ws, req.head, { ignorePath: true, target: `http://0.0.0.0:${port}${req.originalUrl}`, diff --git a/src/node/routes/errors.ts b/src/node/routes/errors.ts new file mode 100644 index 000000000000..47286210afe8 --- /dev/null +++ b/src/node/routes/errors.ts @@ -0,0 +1,90 @@ +import { logger } from "@coder/logger" +import express from "express" +import { promises as fs } from "fs" +import path from "path" +import { HttpCode } from "../../common/http" +import { rootPath } from "../constants" +import { replaceTemplates } from "../http" +import { escapeHtml, getMediaMime } from "../util" +import type { WebsocketRequest } from "../wsRouter" + +interface ErrorWithStatusCode { + statusCode: number +} + +interface ErrorWithCode { + code: string +} + +/** Error is network related. */ +export const errorHasStatusCode = (error: any): error is ErrorWithStatusCode => { + return error && "statusCode" in error +} + +/** Error originates from file system. */ +export const errorHasCode = (error: any): error is ErrorWithCode => { + return error && "code" in error +} + +const notFoundCodes = [404, "ENOENT", "EISDIR"] + +/** + * Final HTTP error handler. + * + * Note: This handler intentionally does not call `next()` even though it + * accepts it as an argument; it is expected to be mounted last. + */ +export const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { + let statusCode = 500 + + if (errorHasStatusCode(err)) { + statusCode = err.statusCode + } else if (errorHasCode(err) && notFoundCodes.includes(err.code)) { + statusCode = HttpCode.NotFound + } + + res.status(statusCode) + + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. Even for css/javascript. + if (req.headers.accept && req.headers.accept.includes("text/html")) { + const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") + res.set("Content-Type", getMediaMime(resourcePath)) + const content = await fs.readFile(resourcePath, "utf8") + res.send( + replaceTemplates(req, content) + .replace(/{{ERROR_TITLE}}/g, statusCode.toString()) + .replace(/{{ERROR_HEADER}}/g, statusCode.toString()) + .replace(/{{ERROR_BODY}}/g, escapeHtml(err.message)) + .replace(/{{APP_NAME}}/g, req.args["app-name"]), + ) + } else { + res.json({ + error: err.message, + ...(err.details || {}), + }) + } +} + +/** + * Final WebSocket error handler. + * + * Note: This handler intentionally does not call `next()` even though it + * accepts it as an argument; it is expected to be mounted last. + */ +export const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { + let statusCode = 500 + if (errorHasStatusCode(err)) { + statusCode = err.statusCode + } else if (errorHasCode(err) && notFoundCodes.includes(err.code)) { + statusCode = HttpCode.NotFound + } + if (statusCode >= 500) { + logger.error(`${err.message} ${err.stack}`) + } else { + logger.debug(`${err.message} ${err.stack}`) + } + ;(req as WebsocketRequest).ws.end(`HTTP/1.1 ${statusCode} ${err.message}\r\n\r\n`) +} diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 42edbe117a24..78e30566867d 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -1,29 +1,27 @@ import { logger } from "@coder/logger" -import bodyParser from "body-parser" import cookieParser from "cookie-parser" import * as express from "express" import { promises as fs } from "fs" -import http from "http" import * as path from "path" import * as tls from "tls" -import * as pluginapi from "../../../typings/pluginapi" -import { HttpCode, HttpError } from "../../common/http" +import { Disposable } from "../../common/emitter" +import { getCookieSessionName, HttpCode, HttpError } from "../../common/http" import { plural } from "../../common/util" +import { App } from "../app" import { AuthType, DefaultedArgs } from "../cli" -import { rootPath } from "../constants" +import { commit, rootPath } from "../constants" import { Heart } from "../heart" -import { ensureAuthenticated, redirect, replaceTemplates } from "../http" -import { PluginAPI } from "../plugin" +import { redirect } from "../http" +import { CoderSettings, SettingsProvider } from "../settings" +import { UpdateProvider } from "../update" import { getMediaMime, paths } from "../util" -import { wrapper } from "../wrapper" -import * as apps from "./apps" +import type { WebsocketRequest } from "../wsRouter" import * as domainProxy from "./domainProxy" +import { errorHandler, wsErrorHandler } from "./errors" import * as health from "./health" import * as login from "./login" import * as logout from "./logout" import * as pathProxy from "./pathProxy" -// static is a reserved keyword. -import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" @@ -31,14 +29,20 @@ import * as vscode from "./vscode" * Register all routes and middleware. */ export const register = async ( - app: express.Express, - wsApp: express.Express, - server: http.Server, + app: App, args: DefaultedArgs, -): Promise => { +): Promise<{ disposeRoutes: Disposable["dispose"]; heart: Heart }> => { const heart = new Heart(path.join(paths.data, "heartbeat"), async () => { return new Promise((resolve, reject) => { - server.getConnections((error, count) => { + // getConnections appears to not call the callback when there are no more + // connections. Feels like it must be a bug? For now add a timer to make + // sure we eventually resolve. + const timer = setTimeout(() => { + logger.debug("Node failed to respond with connections; assuming zero") + resolve(false) + }, 5000) + app.server.getConnections((error, count) => { + clearTimeout(timer) if (error) { return reject(error) } @@ -47,145 +51,139 @@ export const register = async ( }) }) }) - server.on("close", () => { - heart.dispose() - }) - app.disable("x-powered-by") - wsApp.disable("x-powered-by") + app.router.disable("x-powered-by") + app.wsRouter.disable("x-powered-by") + + app.router.use(cookieParser()) + app.wsRouter.use(cookieParser()) + + const settings = new SettingsProvider(path.join(args["user-data-dir"], "coder.json")) + const updater = new UpdateProvider("https://api.github.com/repos/coder/code-server/releases/latest", settings) - app.use(cookieParser()) - wsApp.use(cookieParser()) + const cookieSessionName = getCookieSessionName(args["cookie-suffix"]) const common: express.RequestHandler = (req, _, next) => { // /healthz|/healthz/ needs to be excluded otherwise health checks will make // it look like code-server is always in use. if (!/^\/healthz\/?$/.test(req.url)) { + // NOTE@jsjoeio - intentionally not awaiting the .beat() call here because + // we don't want to slow down the request. heart.beat() } // Add common variables routes can use. req.args = args req.heart = heart + req.settings = settings + req.updater = updater + req.cookieSessionName = cookieSessionName next() } - app.use(common) - wsApp.use(common) + app.router.use(common) + app.wsRouter.use(common) - app.use(async (req, res, next) => { + app.router.use(/.*/, async (req, res, next) => { // If we're handling TLS ensure all requests are redirected to HTTPS. // TODO: This does *NOT* work if you have a base path since to specify the // protocol we need to specify the whole path. if (args.cert && !(req.connection as tls.TLSSocket).encrypted) { return res.redirect(`https://${req.headers.host}${req.originalUrl}`) } + next() + }) - // Return robots.txt. - if (req.originalUrl === "/robots.txt") { - const resourcePath = path.resolve(rootPath, "src/browser/robots.txt") - res.set("Content-Type", getMediaMime(resourcePath)) - return res.send(await fs.readFile(resourcePath)) - } + app.router.get(["/security.txt", "/.well-known/security.txt"], async (_, res) => { + const resourcePath = path.resolve(rootPath, "src/browser/security.txt") + res.set("Content-Type", getMediaMime(resourcePath)) + res.send(await fs.readFile(resourcePath)) + }) - next() + app.router.get("/robots.txt", async (_, res) => { + const resourcePath = path.resolve(rootPath, "src/browser/robots.txt") + res.set("Content-Type", getMediaMime(resourcePath)) + res.send(await fs.readFile(resourcePath)) }) - app.use("/", domainProxy.router) - wsApp.use("/", domainProxy.wsRouter.router) + app.router.use("/", domainProxy.router) + app.wsRouter.use("/", domainProxy.wsRouter.router) - app.all("/proxy/(:port)(/*)?", (req, res) => { - pathProxy.proxy(req, res) + app.router.all("/proxy/:port{/*path}", async (req, res) => { + await pathProxy.proxy(req, res) }) - wsApp.get("/proxy/(:port)(/*)?", async (req) => { - await pathProxy.wsProxy(req as pluginapi.WebsocketRequest) + app.wsRouter.get("/proxy/:port{/*path}", async (req) => { + await pathProxy.wsProxy(req as unknown as WebsocketRequest) }) // These two routes pass through the path directly. // So the proxied app must be aware it is running // under /absproxy// - app.all("/absproxy/(:port)(/*)?", (req, res) => { - pathProxy.proxy(req, res, { + app.router.all("/absproxy/:port{/*path}", async (req, res) => { + await pathProxy.proxy(req, res, { passthroughPath: true, + proxyBasePath: args["abs-proxy-base-path"], }) }) - wsApp.get("/absproxy/(:port)(/*)?", async (req) => { - await pathProxy.wsProxy(req as pluginapi.WebsocketRequest, { + app.wsRouter.get("/absproxy/:port{/*path}", async (req) => { + await pathProxy.wsProxy(req as unknown as WebsocketRequest, { passthroughPath: true, + proxyBasePath: args["abs-proxy-base-path"], }) }) - if (!process.env.CS_DISABLE_PLUGINS) { - const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined - const pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir) - await pluginApi.loadPlugins() - pluginApi.mount(app, wsApp) - app.use("/api/applications", ensureAuthenticated, apps.router(pluginApi)) - wrapper.onDispose(() => pluginApi.dispose()) - } - - app.use(bodyParser.json()) - app.use(bodyParser.urlencoded({ extended: true })) - - app.use("/", vscode.router) - wsApp.use("/", vscode.wsRouter.router) - app.use("/vscode", vscode.router) - wsApp.use("/vscode", vscode.wsRouter.router) + app.router.use(express.json()) + app.router.use(express.urlencoded({ extended: true })) + + app.router.use( + "/_static", + express.static(rootPath, { + cacheControl: commit !== "development", + fallthrough: false, + setHeaders: (res, path, stat) => { + // The service worker is served from a sub-path on the static route so + // this is required to allow it to register a higher scope (by default + // the browser only allows it to register from its own path or lower). + if (path.endsWith("/serviceWorker.js")) { + res.setHeader("Service-Worker-Allowed", "/") + } + }, + }), + ) - app.use("/healthz", health.router) - wsApp.use("/healthz", health.wsRouter.router) + app.router.use("/healthz", health.router) + app.wsRouter.use("/healthz", health.wsRouter.router) if (args.auth === AuthType.Password) { - app.use("/login", login.router) - app.use("/logout", logout.router) + app.router.use("/login", login.router) + app.router.use("/logout", logout.router) } else { - app.all("/login", (req, res) => redirect(req, res, "/", {})) - app.all("/logout", (req, res) => redirect(req, res, "/", {})) + app.router.all("/login", (req, res) => redirect(req, res, "/", {})) + app.router.all("/logout", (req, res) => redirect(req, res, "/", {})) } - app.use("/static", _static.router) - app.use("/update", update.router) + app.router.use("/update", update.router) + + // For historic reasons we also load at /vscode because the root was replaced + // by a plugin in v1 of Coder. The plugin system (which was for internal use + // only) has been removed, but leave the additional route for now. + for (const routePrefix of ["/vscode", "/"]) { + app.router.use(routePrefix, vscode.router) + app.wsRouter.use(routePrefix, vscode.wsRouter.router) + } - app.use(() => { + app.router.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) }) - const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { - if (err.code === "ENOENT" || err.code === "EISDIR") { - err.status = HttpCode.NotFound - } - - const status = err.status ?? err.statusCode ?? 500 - res.status(status) - - // Assume anything that explicitly accepts text/html is a user browsing a - // page (as opposed to an xhr request). Don't use `req.accepts()` since - // *every* request that I've seen (in Firefox and Chromium at least) - // includes `*/*` making it always truthy. Even for css/javascript. - if (req.headers.accept && req.headers.accept.includes("text/html")) { - const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") - res.set("Content-Type", getMediaMime(resourcePath)) - const content = await fs.readFile(resourcePath, "utf8") - res.send( - replaceTemplates(req, content) - .replace(/{{ERROR_TITLE}}/g, status) - .replace(/{{ERROR_HEADER}}/g, status) - .replace(/{{ERROR_BODY}}/g, err.message), - ) - } else { - res.json({ - error: err.message, - ...(err.details || {}), - }) - } - } + app.router.use(errorHandler) + app.wsRouter.use(wsErrorHandler) - app.use(errorHandler) - - const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { - logger.error(`${err.message} ${err.stack}`) - ;(req as pluginapi.WebsocketRequest).ws.end() + return { + disposeRoutes: () => { + heart.dispose() + vscode.dispose() + }, + heart, } - - wsApp.use(wsErrorHandler) } diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 999b8dfaf5b9..e3f797e76471 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -3,18 +3,15 @@ import { promises as fs } from "fs" import { RateLimiter as Limiter } from "limiter" import * as path from "path" import { rootPath } from "../constants" -import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http" -import { getPasswordMethod, handlePasswordValidation, humanPath, sanitizeString, escapeHtml } from "../util" - -export enum Cookie { - Key = "key", -} +import { authenticated, getCookieOptions, redirect, replaceTemplates } from "../http" +import i18n from "../i18n" +import { getPasswordMethod, handlePasswordValidation, sanitizeString, escapeHtml } from "../util" // RateLimiter wraps around the limiter library for logins. // It allows 2 logins every minute plus 12 logins every hour. export class RateLimiter { - private readonly minuteLimiter = new Limiter(2, "minute") - private readonly hourLimiter = new Limiter(12, "hour") + private readonly minuteLimiter = new Limiter({ tokensPerInterval: 2, interval: "minute" }) + private readonly hourLimiter = new Limiter({ tokensPerInterval: 12, interval: "hour" }) public canTry(): boolean { // Note: we must check using >= 1 because technically when there are no tokens left @@ -30,17 +27,27 @@ export class RateLimiter { const getRoot = async (req: Request, error?: Error): Promise => { const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8") - let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.` + const locale = req.args["locale"] || "en" + i18n.changeLanguage(locale) + const welcomeText = req.args["welcome-text"] || (i18n.t("WELCOME", { app: req.args["app-name"] }) as string) + + // Determine password message using i18n + let passwordMsg = i18n.t("LOGIN_PASSWORD") if (req.args.usingEnvPassword) { - passwordMsg = "Password was set from $PASSWORD." + passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD") } else if (req.args.usingEnvHashedPassword) { - passwordMsg = "Password was set from $HASHED_PASSWORD." + passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD") } return replaceTemplates( req, content + .replace(/{{I18N_LOGIN_TITLE}}/g, i18n.t("LOGIN_TITLE", { app: req.args["app-name"] })) + .replace(/{{WELCOME_TEXT}}/g, welcomeText) .replace(/{{PASSWORD_MSG}}/g, passwordMsg) + .replace(/{{I18N_LOGIN_BELOW}}/g, i18n.t("LOGIN_BELOW")) + .replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, i18n.t("PASSWORD_PLACEHOLDER")) + .replace(/{{I18N_SUBMIT}}/g, i18n.t("SUBMIT")) .replace(/{{ERROR}}/, error ? `
${escapeHtml(error.message)}
` : ""), ) } @@ -61,18 +68,18 @@ router.get("/", async (req, res) => { res.send(await getRoot(req)) }) -router.post("/", async (req, res) => { - const password = sanitizeString(req.body.password) +router.post<{}, string, { password?: string; base?: string } | undefined, { to?: string }>("/", async (req, res) => { + const password = sanitizeString(req.body?.password) const hashedPasswordFromArgs = req.args["hashed-password"] try { // Check to see if they exceeded their login attempts if (!limiter.canTry()) { - throw new Error("Login rate limited!") + throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string) } if (!password) { - throw new Error("Missing password") + throw new Error(i18n.t("MISS_PASSWORD") as string) } const passwordMethod = getPasswordMethod(hashedPasswordFromArgs) @@ -86,11 +93,7 @@ router.post("/", async (req, res) => { if (isPasswordValid) { // The hash does not add any actual security but we do it for // obfuscation purposes (and as a side effect it handles escaping). - res.cookie(Cookie.Key, hashedPassword, { - domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]), - path: req.body.base || "/", - sameSite: "lax", - }) + res.cookie(req.cookieSessionName, hashedPassword, getCookieOptions(req)) const to = (typeof req.query.to === "string" && req.query.to) || "/" return redirect(req, res, to, { to: undefined }) @@ -110,8 +113,8 @@ router.post("/", async (req, res) => { }), ) - throw new Error("Incorrect password") - } catch (error) { + throw new Error(i18n.t("INCORRECT_PASSWORD") as string) + } catch (error: any) { const renderedHtml = await getRoot(req, error) res.send(renderedHtml) } diff --git a/src/node/routes/logout.ts b/src/node/routes/logout.ts index d1a19dfef286..425b3ce17c8f 100644 --- a/src/node/routes/logout.ts +++ b/src/node/routes/logout.ts @@ -1,17 +1,13 @@ import { Router } from "express" -import { getCookieDomain, redirect } from "../http" -import { Cookie } from "./login" +import { getCookieOptions, redirect } from "../http" +import { sanitizeString } from "../util" export const router = Router() -router.get("/", async (req, res) => { +router.get<{}, undefined, undefined, { base?: string; to?: string }>("/", async (req, res) => { // Must use the *identical* properties used to set the cookie. - res.clearCookie(Cookie.Key, { - domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]), - path: req.query.base || "/", - sameSite: "lax", - }) + res.clearCookie(req.cookieSessionName, getCookieOptions(req)) - const to = (typeof req.query.to === "string" && req.query.to) || "/" - return redirect(req, res, to, { to: undefined, base: undefined }) + const to = sanitizeString(req.query.to) || "/" + return redirect(req, res, to, { to: undefined, base: undefined, href: undefined }) }) diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index e32001743e19..4f84b3f87e71 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -1,31 +1,42 @@ import { Request, Response } from "express" import * as path from "path" -import qs from "qs" -import * as pluginapi from "../../../typings/pluginapi" import { HttpCode, HttpError } from "../../common/http" -import { normalize } from "../../common/util" -import { authenticated, ensureAuthenticated, redirect } from "../http" +import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy as _proxy } from "../proxy" +import type { WebsocketRequest } from "../wsRouter" -const getProxyTarget = (req: Request, passthroughPath?: boolean): string => { - if (passthroughPath) { - return `http://0.0.0.0:${req.params.port}/${req.originalUrl}` +const getProxyTarget = ( + req: Request, + opts?: { + proxyBasePath?: string + }, +): string => { + // If there is a base path, strip it out. + const base = (req as any).base || "" + // Cast since we only have one port param. + const port = parseInt(req.params.port as string, 10) + if (isNaN(port)) { + throw new HttpError("Invalid port", HttpCode.BadRequest) } - const query = qs.stringify(req.query) - return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` + return `http://0.0.0.0:${port}${opts?.proxyBasePath || ""}/${req.originalUrl.slice(base.length)}` } -export function proxy( +export async function proxy( req: Request, res: Response, opts?: { passthroughPath?: boolean + proxyBasePath?: string }, -): void { - if (!authenticated(req)) { +): Promise { + ensureProxyEnabled(req) + + if (req.method === "OPTIONS" && req.args["skip-auth-preflight"]) { + // Allow preflight requests with `skip-auth-preflight` flag + } else if (!(await authenticated(req))) { // If visiting the root (/:port only) redirect to the login page. - if (!req.params[0] || req.params[0] === "/") { - const to = normalize(`${req.baseUrl}${req.path}`) + if (!req.params.path || req.params.path === "/") { + const to = self(req) return redirect(req, res, "login", { to: to !== "/" ? to : undefined, }) @@ -33,27 +44,35 @@ export function proxy( throw new HttpError("Unauthorized", HttpCode.Unauthorized) } + // The base is used for rewriting (redirects, target). if (!opts?.passthroughPath) { - // Absolute redirects need to be based on the subpath when rewriting. - // See proxy.ts. ;(req as any).base = req.path.split(path.sep).slice(0, 3).join(path.sep) } _proxy.web(req, res, { ignorePath: true, - target: getProxyTarget(req, opts?.passthroughPath), + target: getProxyTarget(req, opts), }) } export async function wsProxy( - req: pluginapi.WebsocketRequest, + req: WebsocketRequest, opts?: { passthroughPath?: boolean + proxyBasePath?: string }, ): Promise { + ensureProxyEnabled(req) + ensureOrigin(req) await ensureAuthenticated(req) + + // The base is used for rewriting (redirects, target). + if (!opts?.passthroughPath) { + ;(req as any).base = req.path.split(path.sep).slice(0, 3).join(path.sep) + } + _proxy.ws(req, req.ws, req.head, { ignorePath: true, - target: getProxyTarget(req, opts?.passthroughPath), + target: getProxyTarget(req, opts), }) } diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts deleted file mode 100644 index 29a1ad3bc7ed..000000000000 --- a/src/node/routes/static.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { field, logger } from "@coder/logger" -import { Router } from "express" -import { promises as fs } from "fs" -import * as path from "path" -import { Readable } from "stream" -import * as tarFs from "tar-fs" -import * as zlib from "zlib" -import { HttpCode, HttpError } from "../../common/http" -import { getFirstString } from "../../common/util" -import { rootPath } from "../constants" -import { authenticated, ensureAuthenticated, replaceTemplates } from "../http" -import { getMediaMime, pathToFsPath } from "../util" - -export const router = Router() - -// The commit is for caching. -router.get("/(:commit)(/*)?", async (req, res) => { - // Used by VS Code to load extensions into the web worker. - const tar = getFirstString(req.query.tar) - if (tar) { - await ensureAuthenticated(req) - let stream: Readable = tarFs.pack(pathToFsPath(tar)) - if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) { - logger.debug("gzipping tar", field("path", tar)) - const compress = zlib.createGzip() - stream.pipe(compress) - stream.on("error", (error) => compress.destroy(error)) - stream.on("close", () => compress.end()) - stream = compress - res.header("content-encoding", "gzip") - } - res.set("Content-Type", "application/x-tar") - stream.on("close", () => res.end()) - return stream.pipe(res) - } - - // If not a tar use the remainder of the path to load the resource. - if (!req.params[0]) { - throw new HttpError("Not Found", HttpCode.NotFound) - } - - const resourcePath = path.resolve(req.params[0]) - - // Make sure it's in code-server if you aren't authenticated. This lets - // unauthenticated users load the login assets. - const isAuthenticated = await authenticated(req) - if (!resourcePath.startsWith(rootPath) && !isAuthenticated) { - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - - // Don't cache during development. - can also be used if you want to make a - // static request without caching. - if (req.params.commit !== "development" && req.params.commit !== "-") { - res.header("Cache-Control", "public, max-age=31536000") - } - - // Without this the default is to use the directory the script loaded from. - if (req.headers["service-worker"]) { - res.header("service-worker-allowed", "/") - } - - res.set("Content-Type", getMediaMime(resourcePath)) - - if (resourcePath.endsWith("manifest.json")) { - const content = await fs.readFile(resourcePath, "utf8") - return res.send(replaceTemplates(req, content)) - } - - const content = await fs.readFile(resourcePath) - return res.send(content) -}) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index 5c9aa181e9e2..60d2011eae72 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -1,18 +1,15 @@ import { Router } from "express" import { version } from "../constants" import { ensureAuthenticated } from "../http" -import { UpdateProvider } from "../update" export const router = Router() -const provider = new UpdateProvider() - router.get("/check", ensureAuthenticated, async (req, res) => { - const update = await provider.getUpdate(req.query.force === "true") + const update = await req.updater.getUpdate(req.query.force === "true") res.json({ checked: update.checked, latest: update.version, current: version, - isLatest: provider.isLatestVersion(update), + isLatest: req.updater.isLatestVersion(update), }) }) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index d91abb4b0536..f8c048c6f036 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -1,232 +1,258 @@ +import { logger } from "@coder/logger" import * as crypto from "crypto" -import { Request, Router } from "express" +import * as express from "express" import { promises as fs } from "fs" +import * as http from "http" +import * as net from "net" +import * as os from "os" import * as path from "path" -import qs from "qs" -import * as ipc from "../../../typings/ipc" -import { Emitter } from "../../common/emitter" -import { HttpCode, HttpError } from "../../common/http" -import { getFirstString } from "../../common/util" -import { Feature } from "../cli" -import { isDevMode, rootPath, version } from "../constants" -import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" -import { getMediaMime, pathToFsPath } from "../util" -import { VscodeProvider } from "../vscode" -import { Router as WsRouter } from "../wsRouter" - -export const router = Router() - -const vscode = new VscodeProvider() - -router.get("/", async (req, res) => { - const isAuthenticated = await authenticated(req) - if (!isAuthenticated) { - return redirect(req, res, "login", { - // req.baseUrl can be blank if already at the root. - to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined, - }) - } - - const [content, options] = await Promise.all([ - await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"), - (async () => { - try { - return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query) - } catch (error) { - const devMessage = isDevMode ? "It might not have finished compiling." : "" - throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`) - } - })(), - ]) +import { logError } from "../../common/util" +import { CodeArgs, toCodeArgs } from "../cli" +import { isDevMode, vsRootPath } from "../constants" +import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http" +import { SocketProxyProvider } from "../socket" +import { isFile } from "../util" +import { type WebsocketRequest, Router as WsRouter } from "../wsRouter" - options.productConfiguration.codeServerVersion = version +export const router = express.Router() - res.send( - replaceTemplates( - req, - // Uncomment prod blocks if not in development. TODO: Would this be - // better as a build step? Or maintain two HTML files again? - !isDevMode ? content.replace(//g, "") : content, - { - authed: req.args.auth !== "none", - disableUpdateCheck: !!req.args["disable-update-check"], - }, - ) - .replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`) - .replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`) - .replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`) - .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`), - ) -}) +export const wsRouter = WsRouter() /** - * TODO: Might currently be unused. + * The API of VS Code's web client server. code-server delegates requests to VS + * Code here. + * + * @see ../../../lib/vscode/src/vs/server/node/server.main.ts:72 */ -router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => { - const path = getFirstString(req.query.path) - if (path) { - res.set("Content-Type", getMediaMime(path)) - res.send(await fs.readFile(pathToFsPath(path))) - } -}) +export interface IVSCodeServerAPI { + handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise + handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void + handleServerError(err: Error): void + dispose(): void +} /** - * Used by VS Code to load files. + * VS Code's CLI entrypoint (../../../lib/vscode/src/server-main.js). + * + * Normally VS Code will run `node server-main.js` which starts either the web + * server or the CLI (for installing extensions, etc) but we patch it so we can + * `require` it and call its functions directly in order to integrate with our + * web server. */ -router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => { - const path = getFirstString(req.query.path) - if (path) { - res.set("Content-Type", getMediaMime(path)) - res.send(await fs.readFile(pathToFsPath(path))) - } -}) +export type VSCodeModule = { + // See ../../../lib/vscode/src/server-main.js:339. + loadCodeWithNls(): Promise<{ + // See ../../../lib/vscode/src/vs/server/node/server.main.ts:72. + createServer(address: string | net.AddressInfo | null, args: CodeArgs): Promise + // See ../../../lib/vscode/src/vs/server/node/server.main.ts:65. + spawnCli(args: CodeArgs): Promise + }> +} /** - * VS Code webviews use these paths to load files and to load webview assets - * like HTML and JavaScript. + * Load then create the VS Code server. */ -router.get("/webview/*", ensureAuthenticated, async (req, res) => { - res.set("Content-Type", getMediaMime(req.path)) - if (/^vscode-resource/.test(req.params[0])) { - return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, ""))) - } - return res.send( - await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), - ) -}) - -interface Callback { - uri: { - scheme: string - authority?: string - path?: string - query?: string - fragment?: string +async function loadVSCode(req: express.Request): Promise { + // Since server-main.js is an ES module, we have to use `import`. However, + // tsc will transpile this to `require` unless we change our module type, + // which will also require that we switch to ESM, since a hybrid approach + // breaks importing `rotating-file-stream` for some reason. To work around + // this, use `eval` for now, but we should consider switching to ESM. + let modPath = path.join(vsRootPath, "out/server-main.js") + if (os.platform() === "win32") { + // On Windows, absolute paths of ESM modules must be a valid file URI. + modPath = "file:///" + modPath.replace(/\\/g, "/") } - timeout: NodeJS.Timeout + const mod = (await eval(`import("${modPath}")`)) as VSCodeModule + const serverModule = await mod.loadCodeWithNls() + return serverModule.createServer(null, { + ...(await toCodeArgs(req.args)), + "accept-server-license-terms": true, + // This seems to be used to make the connection token flags optional (when + // set to 1.63) but we have always included them. + compatibility: "1.64", + "without-connection-token": true, + }) } -const callbacks = new Map() -const callbackEmitter = new Emitter<{ id: string; callback: Callback }>() +// To prevent loading the module more than once at a time. We also have the +// resolved value so you do not need to `await` everywhere. +let vscodeServerPromise: Promise | undefined + +// The resolved value from the dynamically loaded VS Code server. Do not use +// without first calling and awaiting `ensureCodeServerLoaded`. +let vscodeServer: IVSCodeServerAPI | undefined /** - * Get vscode-requestId from the query and throw if it's missing or invalid. + * Ensure the VS Code server is loaded. */ -const getRequestId = (req: Request): string => { - if (!req.query["vscode-requestId"]) { - throw new HttpError("vscode-requestId is missing", HttpCode.BadRequest) +export const ensureVSCodeLoaded = async ( + req: express.Request, + _: express.Response, + next: express.NextFunction, +): Promise => { + if (vscodeServer) { + return next() } - - if (typeof req.query["vscode-requestId"] !== "string") { - throw new HttpError("vscode-requestId is not a string", HttpCode.BadRequest) + if (!vscodeServerPromise) { + vscodeServerPromise = loadVSCode(req) } - - return req.query["vscode-requestId"] + try { + vscodeServer = await vscodeServerPromise + } catch (error) { + vscodeServerPromise = undefined // Unset so we can try again. + logError(logger, "CodeServerRouteWrapper", error) + if (isDevMode) { + return next( + new Error( + (error instanceof Error ? error.message : error) + + " (Have you applied the patches? If so, VS Code may still be compiling)", + ), + ) + } + return next(error) + } + return next() } -// Matches VS Code's fetch timeout. -const fetchTimeout = 5 * 60 * 1000 - -// The callback endpoints are used during authentication. A URI is stored on -// /callback and then fetched later on /fetch-callback. -// See ../../../lib/vscode/resources/web/code-web.js -router.get("/callback", ensureAuthenticated, async (req, res) => { - const uriKeys = [ - "vscode-requestId", - "vscode-scheme", - "vscode-authority", - "vscode-path", - "vscode-query", - "vscode-fragment", - ] - - const id = getRequestId(req) - - // Move any query variables that aren't URI keys into the URI's query - // (importantly, this will include the code for oauth). - const query: qs.ParsedQs = {} - for (const key in req.query) { - if (!uriKeys.includes(key)) { - query[key] = req.query[key] - } +router.get("/", ensureVSCodeLoaded, async (req, res, next) => { + const isAuthenticated = await authenticated(req) + const NO_FOLDER_OR_WORKSPACE_QUERY = !req.query.folder && !req.query.workspace + // Ew means the workspace was closed so clear the last folder/workspace. + const FOLDER_OR_WORKSPACE_WAS_CLOSED = req.query.ew + + if (!isAuthenticated) { + const to = self(req) + return redirect(req, res, "login", { + to: to !== "/" ? to : undefined, + }) } - const callback = { - uri: { - scheme: getFirstString(req.query["vscode-scheme"]) || "code-oss", - authority: getFirstString(req.query["vscode-authority"]), - path: getFirstString(req.query["vscode-path"]), - query: (getFirstString(req.query.query) || "") + "&" + qs.stringify(query), - fragment: getFirstString(req.query["vscode-fragment"]), - }, - // Make sure the map doesn't leak if nothing fetches this URI. - timeout: setTimeout(() => callbacks.delete(id), fetchTimeout), + if (NO_FOLDER_OR_WORKSPACE_QUERY && !FOLDER_OR_WORKSPACE_WAS_CLOSED) { + const settings = await req.settings.read() + const lastOpened = settings.query || {} + // This flag disables the last opened behavior + const IGNORE_LAST_OPENED = req.args["ignore-last-opened"] + const HAS_LAST_OPENED_FOLDER_OR_WORKSPACE = lastOpened.folder || lastOpened.workspace + const HAS_FOLDER_OR_WORKSPACE_FROM_CLI = req.args._.length > 0 + const to = self(req) + + let folder = undefined + let workspace = undefined + + // Redirect to the last folder/workspace if nothing else is opened. + if (HAS_LAST_OPENED_FOLDER_OR_WORKSPACE && !IGNORE_LAST_OPENED) { + folder = lastOpened.folder + workspace = lastOpened.workspace + } else if (HAS_FOLDER_OR_WORKSPACE_FROM_CLI) { + const lastEntry = path.resolve(req.args._[req.args._.length - 1]) + const entryIsFile = await isFile(lastEntry) + const IS_WORKSPACE_FILE = entryIsFile && path.extname(lastEntry) === ".code-workspace" + + if (IS_WORKSPACE_FILE) { + workspace = lastEntry + } else if (!entryIsFile) { + folder = lastEntry + } + } + + if (folder || workspace) { + return redirect(req, res, to, { + folder, + workspace, + }) + } } - callbacks.set(id, callback) - callbackEmitter.emit({ id, callback }) + // Store the query parameters so we can use them on the next load. This + // also allows users to create functionality around query parameters. + await req.settings.write({ query: req.query }) - res.sendFile(path.join(rootPath, "vendor/modules/code-oss-dev/resources/web/callback.html")) + next() }) -router.get("/fetch-callback", ensureAuthenticated, async (req, res) => { - const id = getRequestId(req) +router.get("/manifest.json", async (req, res) => { + res.writeHead(200, { "Content-Type": "application/manifest+json" }) - const send = (callback: Callback) => { - clearTimeout(callback.timeout) - callbacks.delete(id) - res.json(callback.uri) - } + res.end( + replaceTemplates( + req, + JSON.stringify( + { + name: req.args["app-name"], + short_name: req.args["app-name"], + start_url: ".", + display: "fullscreen", + display_override: ["window-controls-overlay"], + description: "Run Code on a remote server.", + icons: [192, 512] + .map((size) => [ + { + src: `{{BASE}}/_static/src/browser/media/pwa-icon-${size}.png`, + type: "image/png", + sizes: `${size}x${size}`, + purpose: "any", + }, + { + src: `{{BASE}}/_static/src/browser/media/pwa-icon-maskable-${size}.png`, + type: "image/png", + sizes: `${size}x${size}`, + purpose: "maskable", + }, + ]) + .flat(), + }, + null, + 2, + ), + ), + ) +}) - const callback = callbacks.get(id) - if (callback) { - return send(callback) +let mintKeyPromise: Promise | undefined +router.post("/mint-key", ensureAuthenticated, async (req, res) => { + if (!mintKeyPromise) { + mintKeyPromise = new Promise(async (resolve) => { + const keyPath = path.join(req.args["user-data-dir"], "serve-web-key-half") + logger.debug(`Reading server web key half from ${keyPath}`) + try { + resolve(await fs.readFile(keyPath)) + return + } catch (error: any) { + if (error.code !== "ENOENT") { + logError(logger, `read ${keyPath}`, error) + } + } + // VS Code wants 256 bits. + const key = crypto.randomBytes(32) + try { + await fs.writeFile(keyPath, key) + } catch (error: any) { + logError(logger, `write ${keyPath}`, error) + } + resolve(key) + }) } - - // VS Code will try again if the route returns no content but it seems more - // efficient to just wait on this request for as long as possible? - const handler = callbackEmitter.event(({ id: emitId, callback }) => { - if (id === emitId) { - handler.dispose() - send(callback) - } - }) - - // If the client closes the connection. - req.on("close", () => handler.dispose()) + const key = await mintKeyPromise + res.end(key) }) -export const wsRouter = WsRouter() - -wsRouter.ws("/", ensureAuthenticated, async (req) => { - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - const reply = crypto - .createHash("sha1") - .update(req.headers["sec-websocket-key"] + magic) - .digest("base64") - - const responseHeaders = [ - "HTTP/1.1 101 Switching Protocols", - "Upgrade: websocket", - "Connection: Upgrade", - `Sec-WebSocket-Accept: ${reply}`, - ] - - // See if the browser reports it supports web socket compression. - // TODO: Parse this header properly. - const extensions = req.headers["sec-websocket-extensions"] - const isCompressionSupported = extensions ? extensions.includes("permessage-deflate") : false - - // TODO: For now we only use compression if the user enables it. - const isCompressionEnabled = !!req.args.enable?.includes(Feature.PermessageDeflate) - - const useCompression = isCompressionEnabled && isCompressionSupported - if (useCompression) { - // This response header tells the browser the server supports compression. - responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15") - } +router.all(/.*/, ensureAuthenticated, ensureVSCodeLoaded, async (req, res) => { + vscodeServer!.handleRequest(req, res) +}) - req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n") +const socketProxyProvider = new SocketProxyProvider() +wsRouter.ws(/.*/, ensureOrigin, ensureAuthenticated, ensureVSCodeLoaded, async (req: WebsocketRequest) => { + const wrappedSocket = await socketProxyProvider.createProxy(req.ws) + // This should actually accept a duplex stream but it seems Code has not + // been updated to match the Node 16 types so cast for now. There does not + // appear to be any code specific to sockets so this should be fine. + vscodeServer!.handleUpgrade(req, wrappedSocket as net.Socket) - await vscode.sendWebsocket(req.ws, req.query, useCompression) + req.ws.resume() }) + +export function dispose() { + vscodeServer?.dispose() + socketProxyProvider.stop() +} diff --git a/src/node/settings.ts b/src/node/settings.ts index ee955aad9e99..cc55dd5f9508 100644 --- a/src/node/settings.ts +++ b/src/node/settings.ts @@ -1,8 +1,6 @@ import { logger } from "@coder/logger" -import { Query } from "express-serve-static-core" +import type { ParsedQs } from "qs" import { promises as fs } from "fs" -import * as path from "path" -import { paths } from "./util" export type Settings = { [key: string]: Settings | string | boolean | number } @@ -19,8 +17,8 @@ export class SettingsProvider { public async read(): Promise { try { const raw = (await fs.readFile(this.settingsPath, "utf8")).trim() - return raw ? JSON.parse(raw) : {} - } catch (error) { + return raw ? JSON.parse(raw) : ({} as T) + } catch (error: any) { if (error.code !== "ENOENT") { logger.warn(error.message) } @@ -37,7 +35,7 @@ export class SettingsProvider { const oldSettings = await this.read() const nextSettings = { ...oldSettings, ...settings } await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2)) - } catch (error) { + } catch (error: any) { logger.warn(error.message) } } @@ -54,14 +52,5 @@ export interface UpdateSettings { * Global code-server settings. */ export interface CoderSettings extends UpdateSettings { - lastVisited: { - url: string - workspace: boolean - } - query: Query + query?: ParsedQs } - -/** - * Global code-server settings file. - */ -export const settings = new SettingsProvider(path.join(paths.data, "coder.json")) diff --git a/src/node/socket.ts b/src/node/socket.ts index 1651046d02b2..7ce2068a7029 100644 --- a/src/node/socket.ts +++ b/src/node/socket.ts @@ -1,6 +1,7 @@ import { promises as fs } from "fs" import * as net from "net" import * as path from "path" +import * as stream from "stream" import * as tls from "tls" import { Emitter } from "../common/emitter" import { generateUuid } from "../common/util" @@ -27,10 +28,13 @@ export class SocketProxyProvider { } /** - * Create a socket proxy for TLS sockets. If it's not a TLS socket the - * original socket is returned. This will spawn a proxy server on demand. + * Create a socket proxy for TLS sockets. If it is not a TLS socket the + * original socket or stream is returned. This will spawn a proxy server on + * demand. */ - public async createProxy(socket: net.Socket): Promise { + public async createProxy(socket: tls.TLSSocket | net.Socket): Promise + public async createProxy(socket: stream.Duplex): Promise + public async createProxy(socket: tls.TLSSocket | net.Socket | stream.Duplex): Promise { if (!(socket instanceof tls.TLSSocket)) { return socket } @@ -43,7 +47,7 @@ export class SocketProxyProvider { proxy.once("connect", () => proxy.write(id)) const timeout = setTimeout(() => { - listener.dispose() // eslint-disable-line @typescript-eslint/no-use-before-define + listener.dispose() socket.destroy() proxy.destroy() reject(new Error("TLS socket proxy timed out")) @@ -77,7 +81,7 @@ export class SocketProxyProvider { this.proxyPipe = pipe return Promise.all([ fs.mkdir(path.dirname(this.proxyPipe), { recursive: true }), - fs.rmdir(this.proxyPipe, { recursive: true }), + fs.rm(this.proxyPipe, { force: true, recursive: true }), ]) }) .then(() => { diff --git a/src/node/update.ts b/src/node/update.ts index 6f9aa39e58c7..4e4a1babe501 100644 --- a/src/node/update.ts +++ b/src/node/update.ts @@ -1,10 +1,11 @@ import { field, logger } from "@coder/logger" import * as http from "http" import * as https from "https" +import { ProxyAgent } from "proxy-agent" import * as semver from "semver" import * as url from "url" -import { version } from "./constants" -import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings" +import { httpProxyUri, version } from "./constants" +import { SettingsProvider, UpdateSettings } from "./settings" export interface Update { checked: number @@ -27,12 +28,11 @@ export class UpdateProvider { * The URL for getting the latest version of code-server. Should return JSON * that fulfills `LatestResponse`. */ - private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest", + private readonly latestUrl: string, /** - * Update information will be stored here. If not provided, the global - * settings will be used. + * Update information will be stored here. */ - private readonly settings: SettingsProvider = globalSettings, + private readonly settings: SettingsProvider, ) {} /** @@ -60,7 +60,7 @@ export class UpdateProvider { } logger.debug("got latest version", field("latest", update.version)) return update - } catch (error) { + } catch (error: any) { logger.error("Failed to get latest version", field("error", error.message)) return { checked: now, @@ -103,8 +103,13 @@ export class UpdateProvider { return new Promise((resolve, reject) => { const request = (uri: string): void => { logger.debug("Making request", field("uri", uri)) - const httpx = uri.startsWith("https") ? https : http - const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { + const isHttps = uri.startsWith("https") + const agent = new ProxyAgent({ + keepAlive: true, + getProxyForUrl: () => httpProxyUri || "", + }) + const httpx = isHttps ? https : http + const client = httpx.get(uri, { headers: { "User-Agent": "code-server" }, agent }, (response) => { if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { response.destroy() return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) diff --git a/src/node/uriTransformer.ts b/src/node/uriTransformer.ts deleted file mode 100644 index 50d07b1ce71b..000000000000 --- a/src/node/uriTransformer.ts +++ /dev/null @@ -1,66 +0,0 @@ -// In a bit of a hack, this file is stored in two places -// - src/node/uri_transformer.ts -// - lib/vscode/src/vs/server/uriTransformer.ts - -// The reason for this is that we need a CommonJS-compiled -// version of this file to supply as a command line argument -// to extensionHostProcessSetup.ts; but we also need to include -// it ourselves cleanly in `lib/vscode/src/vs/server`. - -// @oxy: Could not figure out how to compile as a CommonJS module -// in the same tree as VSCode, which is why I came up with the solution -// of storing it in two places. - -// NOTE: copied over from lib/vscode/src/vs/common/uriIpc.ts -// remember to update this for proper type checks! - -interface UriParts { - scheme: string - authority?: string - path?: string -} - -interface IRawURITransformer { - transformIncoming(uri: UriParts): UriParts - transformOutgoing(uri: UriParts): UriParts - transformOutgoingScheme(scheme: string): string -} - -// Using `export =` is deliberate. -// See lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts; -// they include the file directly with a node require and expect a function as `module.exports`. -// `export =` in TypeScript is equivalent to `module.exports =` in vanilla JS. -export = function rawURITransformerFactory(authority: string) { - return new RawURITransformer(authority) -} - -class RawURITransformer implements IRawURITransformer { - constructor(private readonly authority: string) {} - - transformIncoming(uri: UriParts): UriParts { - switch (uri.scheme) { - case "vscode-remote": - return { scheme: "file", path: uri.path } - default: - return uri - } - } - - transformOutgoing(uri: UriParts): UriParts { - switch (uri.scheme) { - case "file": - return { scheme: "vscode-remote", authority: this.authority, path: uri.path } - default: - return uri - } - } - - transformOutgoingScheme(scheme: string): string { - switch (scheme) { - case "file": - return "vscode-remote" - default: - return scheme - } - } -} diff --git a/src/node/util.ts b/src/node/util.ts index ce92a3522535..b7c0c3fa5339 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -1,4 +1,3 @@ -import { logger } from "@coder/logger" import * as argon2 from "argon2" import * as cp from "child_process" import * as crypto from "crypto" @@ -10,7 +9,6 @@ import * as path from "path" import safeCompare from "safe-compare" import * as util from "util" import xdgBasedir from "xdg-basedir" -import { getFirstString } from "../common/util" export interface Paths { data: string @@ -25,10 +23,11 @@ const pattern = [ ].join("|") const re = new RegExp(pattern, "g") +export type OnLineCallback = (strippedLine: string, originalLine: string) => void /** * Split stdout on newlines and strip ANSI codes. */ -export const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => { +export const onLine = (proc: cp.ChildProcess, callback: OnLineCallback): void => { let buffer = "" if (!proc.stdout) { throw new Error("no stdout") @@ -57,10 +56,10 @@ export const paths = getEnvPaths() * On MacOS this function gets the standard XDG directories instead of using the native macOS * ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories. */ -export function getEnvPaths(): Paths { +export function getEnvPaths(platform = process.platform): Paths { const paths = envPaths("code-server", { suffix: "" }) const append = (p: string): string => path.join(p, "code-server") - switch (process.platform) { + switch (platform) { case "darwin": return { // envPaths uses native directories so force Darwin to use the XDG spec @@ -87,19 +86,6 @@ export function getEnvPaths(): Paths { } } -/** - * humanPath replaces the home directory in p with ~. - * Makes it more readable. - * - * @param p - */ -export function humanPath(p?: string): string { - if (!p) { - return "" - } - return p.replace(os.homedir(), "~") -} - export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => { const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`) const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`) @@ -155,12 +141,7 @@ export const generatePassword = async (length = 24): Promise => { * Used to hash the password. */ export const hash = async (password: string): Promise => { - try { - return await argon2.hash(password) - } catch (error) { - logger.error(error) - return "" - } + return await argon2.hash(password) } /** @@ -170,11 +151,7 @@ export const isHashMatch = async (password: string, hash: string) => { if (password === "" || hash === "" || !hash.startsWith("$")) { return false } - try { - return await argon2.verify(hash, password) - } catch (error) { - throw new Error(error) - } + return await argon2.verify(hash, password) } /** @@ -318,10 +295,10 @@ export async function isCookieValid({ * - greater than 0 characters * - trims whitespace */ -export function sanitizeString(str: string): string { +export function sanitizeString(str: unknown): string { // Very basic sanitization of string // Credit: https://stackoverflow.com/a/46719000/3015595 - return typeof str === "string" && str.trim().length > 0 ? str.trim() : "" + return typeof str === "string" ? str.trim() : "" } const mimeTypes: { [key: string]: string } = { @@ -385,107 +362,86 @@ export const getMediaMime = (filePath?: string): string => { return (filePath && mimeTypes[path.extname(filePath)]) || "text/plain" } -export const isWsl = async (): Promise => { - return ( - (process.platform === "linux" && os.release().toLowerCase().indexOf("microsoft") !== -1) || - (await fs.readFile("/proc/version", "utf8")).toLowerCase().indexOf("microsoft") !== -1 - ) -} - /** - * Try opening a URL using whatever the system has set for opening URLs. + * A helper function that checks if the platform is Windows Subsystem for Linux + * (WSL) + * + * @see https://github.com/sindresorhus/is-wsl/blob/main/index.js + * @returns {Boolean} boolean if it is WSL */ -export const open = async (url: string): Promise => { - const args = [] as string[] - const options = {} as cp.SpawnOptions - const platform = (await isWsl()) ? "wsl" : process.platform - let command = platform === "darwin" ? "open" : "xdg-open" - if (platform === "win32" || platform === "wsl") { - command = platform === "wsl" ? "cmd.exe" : "cmd" - args.push("/c", "start", '""', "/b") - url = url.replace(/&/g, "^&") +export const isWsl = async ( + platform: NodeJS.Platform, + osRelease: string, + procVersionFilePath: string, +): Promise => { + if (platform !== "linux") { + return false } - const proc = cp.spawn(command, [...args, url], options) - await new Promise((resolve, reject) => { - proc.on("error", reject) - proc.on("close", (code) => { - return code !== 0 ? reject(new Error(`Failed to open with code ${code}`)) : resolve() - }) - }) -} -/** - * For iterating over an enum's values. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const enumToArray = (t: any): string[] => { - const values = [] as string[] - for (const k in t) { - values.push(t[k]) + if (osRelease.toLowerCase().includes("microsoft")) { + return true } - return values -} -/** - * For displaying all allowed options in an enum. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const buildAllowedMessage = (t: any): string => { - const values = enumToArray(t) - return `Allowed value${values.length === 1 ? " is" : "s are"} ${values.map((t) => `'${t}'`).join(", ")}` + try { + return (await fs.readFile(procVersionFilePath, "utf8")).toLowerCase().includes("microsoft") + } catch (_) { + return false + } } -export const isObject = (obj: T): obj is T => { - return !Array.isArray(obj) && typeof obj === "object" && obj !== null +interface OpenOptions { + args: string[] + command: string + urlSearch: string } /** - * Taken from vs/base/common/charCode.ts. Copied for now instead of importing so - * we don't have to set up a `vs` alias to be able to import with types (since - * the alternative is to directly import from `out`). + * A helper function to construct options for `open` function. + * + * Extract to make it easier to test. + * + * @param platform - platform on machine + * @param urlSearch - url.search + * @returns an object with args, command, options and urlSearch */ -enum CharCode { - Slash = 47, - A = 65, - Z = 90, - a = 97, - z = 122, - Colon = 58, +export function constructOpenOptions(platform: NodeJS.Platform | "wsl", urlSearch: string): OpenOptions { + const args: string[] = [] + let command = platform === "darwin" ? "open" : "xdg-open" + if (platform === "win32" || platform === "wsl") { + command = platform === "wsl" ? "cmd.exe" : "cmd" + args.push("/c", "start", '""', "/b") + urlSearch = urlSearch.replace(/&/g, "^&") + } + + return { + args, + command, + urlSearch, + } } /** - * Compute `fsPath` for the given uri. - * Taken from vs/base/common/uri.ts. It's not imported to avoid also importing - * everything that file imports. + * Try opening an address using whatever the system has set for opening URLs. */ -export function pathToFsPath(path: string, keepDriveLetterCasing = false): string { - const isWindows = process.platform === "win32" - const uri = { authority: undefined, path: getFirstString(path) || "", scheme: "file" } - let value: string - - if (uri.authority && uri.path.length > 1 && uri.scheme === "file") { - // unc path: file://shares/c$/far/boo - value = `//${uri.authority}${uri.path}` - } else if ( - uri.path.charCodeAt(0) === CharCode.Slash && - ((uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z) || - (uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)) && - uri.path.charCodeAt(2) === CharCode.Colon - ) { - if (!keepDriveLetterCasing) { - // windows drive letter: file:///c:/far/boo - value = uri.path[1].toLowerCase() + uri.path.substr(2) - } else { - value = uri.path.substr(1) - } - } else { - // other path - value = uri.path +export const open = async (address: URL | string): Promise => { + if (typeof address === "string") { + throw new Error("Cannot open socket paths") } - if (isWindows) { - value = value.replace(/\//g, "\\") + // Web sockets do not seem to work if browsing with 0.0.0.0. + const url = new URL(address) + if (url.hostname === "0.0.0.0") { + url.hostname = "localhost" } - return value + const platform = (await isWsl(process.platform, os.release(), "/proc/version")) ? "wsl" : process.platform + const { command, args, urlSearch } = constructOpenOptions(platform, url.search) + url.search = urlSearch + const proc = cp.spawn(command, [...args, url.toString()], {}) + await new Promise((resolve, reject) => { + proc.on("error", reject) + proc.on("close", (code) => { + return code !== 0 ? reject(new Error(`Failed to open with code ${code}`)) : resolve() + }) + }) } /** @@ -511,6 +467,15 @@ export const isFile = async (path: string): Promise => { } } +export const isDirectory = async (path: string): Promise => { + try { + const stat = await fs.stat(path) + return stat.isDirectory() + } catch (error) { + return false + } +} + /** * Escapes any HTML string special characters, like &, <, >, ", and '. * @@ -531,5 +496,18 @@ export function escapeHtml(unsafe: string): string { * it has a .code property. */ export function isNodeJSErrnoException(error: unknown): error is NodeJS.ErrnoException { - return error instanceof Error && (error as NodeJS.ErrnoException).code !== undefined + return error !== undefined && (error as NodeJS.ErrnoException).code !== undefined +} + +// TODO: Replace with proper templating system. +export const escapeJSON = (value: cp.Serializable) => JSON.stringify(value).replace(/"/g, """) + +/** + * Split a string on the first equals. The result will always be an array with + * two items regardless of how many equals there are. The second item will be + * undefined if empty or missing. + */ +export function splitOnFirstEquals(str: string): [string, string | undefined] { + const split = str.split(/=(.+)?/, 2) + return [split[0], split[1]] } diff --git a/src/node/vscode.ts b/src/node/vscode.ts deleted file mode 100644 index 2c07f7ce20b7..000000000000 --- a/src/node/vscode.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { logger } from "@coder/logger" -import * as cp from "child_process" -import * as net from "net" -import * as path from "path" -import * as ipc from "../../typings/ipc" -import { arrayify, generateUuid } from "../common/util" -import { rootPath } from "./constants" -import { settings } from "./settings" -import { SocketProxyProvider } from "./socket" -import { isFile } from "./util" -import { onMessage, wrapper } from "./wrapper" - -export class VscodeProvider { - public readonly serverRootPath: string - public readonly vsRootPath: string - private _vscode?: Promise - private readonly socketProvider = new SocketProxyProvider() - - public constructor() { - this.vsRootPath = path.resolve(rootPath, "vendor/modules/code-oss-dev") - this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") - wrapper.onDispose(() => this.dispose()) - } - - public async dispose(): Promise { - this.socketProvider.stop() - if (this._vscode) { - const vscode = await this._vscode - vscode.removeAllListeners() - vscode.kill() - this._vscode = undefined - } - } - - public async initialize( - options: Omit, - query: ipc.Query, - ): Promise { - const { lastVisited } = await settings.read() - let startPath = await this.getFirstPath([ - { url: query.workspace, workspace: true }, - { url: query.folder, workspace: false }, - options.args._ && options.args._.length > 0 - ? { url: path.resolve(options.args._[options.args._.length - 1]) } - : undefined, - !options.args["ignore-last-opened"] ? lastVisited : undefined, - ]) - - if (query.ew) { - startPath = undefined - } - - settings.write({ - lastVisited: startPath, - query, - }) - - const id = generateUuid() - const vscode = await this.fork() - - logger.debug("setting up vs code...") - - this.send( - { - type: "init", - id, - options: { - ...options, - startPath, - }, - }, - vscode, - ) - - const message = await onMessage( - vscode, - (message): message is ipc.OptionsMessage => { - // There can be parallel initializations so wait for the right ID. - return message.type === "options" && message.id === id - }, - ) - - return message.options - } - - private fork(): Promise { - if (this._vscode) { - return this._vscode - } - - logger.debug("forking vs code...") - const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - - const dispose = () => { - vscode.removeAllListeners() - vscode.kill() - this._vscode = undefined - } - - vscode.on("error", (error: Error) => { - logger.error(error.message) - if (error.stack) { - logger.debug(error.stack) - } - dispose() - }) - - vscode.on("exit", (code) => { - logger.error(`VS Code exited unexpectedly with code ${code}`) - dispose() - }) - - this._vscode = onMessage(vscode, (message): message is ipc.ReadyMessage => { - return message.type === "ready" - }).then(() => vscode) - - return this._vscode - } - - /** - * VS Code expects a raw socket. It will handle all the web socket frames. - */ - public async sendWebsocket(socket: net.Socket, query: ipc.Query, permessageDeflate: boolean): Promise { - const vscode = await this._vscode - // TLS sockets cannot be transferred to child processes so we need an - // in-between. Non-TLS sockets will be returned as-is. - const socketProxy = await this.socketProvider.createProxy(socket) - this.send({ type: "socket", query, permessageDeflate }, vscode, socketProxy) - } - - private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { - if (!vscode || vscode.killed) { - throw new Error("vscode is not running") - } - vscode.send(message, socket) - } - - /** - * Choose the first non-empty path from the provided array. - * - * Each array item consists of `url` and an optional `workspace` boolean that - * indicates whether that url is for a workspace. - * - * `url` can be a fully qualified URL or just the path portion. - * - * `url` can also be a query object to make it easier to pass in query - * variables directly but anything that isn't a string or string array is not - * valid and will be ignored. - */ - private async getFirstPath( - startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, - ): Promise { - for (let i = 0; i < startPaths.length; ++i) { - const startPath = startPaths[i] - const url = arrayify(startPath && startPath.url).find((p) => !!p) - if (startPath && url && typeof url === "string") { - return { - url, - // The only time `workspace` is undefined is for the command-line - // argument, in which case it's a path (not a URL) so we can stat it - // without having to parse it. - workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), - } - } - } - return undefined - } -} diff --git a/src/node/vscodeSocket.ts b/src/node/vscodeSocket.ts new file mode 100644 index 000000000000..e01d1a15012b --- /dev/null +++ b/src/node/vscodeSocket.ts @@ -0,0 +1,206 @@ +import { logger } from "@coder/logger" +import express from "express" +import * as http from "http" +import * as path from "path" +import { HttpCode, HttpError } from "../common/http" +import { listen } from "./app" +import { errorHandler } from "./routes/errors" +import { canConnect } from "./util" + +export interface EditorSessionEntry { + workspace: { + id: string + folders: { + uri: { + path: string + } + }[] + } + + socketPath: string +} + +interface DeleteSessionRequest { + socketPath?: string +} + +interface AddSessionRequest { + entry?: EditorSessionEntry +} + +interface GetSessionResponse { + socketPath?: string +} + +export async function makeEditorSessionManagerServer( + codeServerSocketPath: string, + editorSessionManager: EditorSessionManager, +): Promise { + const router = express() + + router.use(express.json()) + + router.get<{}, GetSessionResponse | string | unknown, undefined, { filePath?: string }>( + "/session", + async (req, res) => { + const filePath = req.query.filePath + if (!filePath) { + throw new HttpError("filePath is required", HttpCode.BadRequest) + } + const socketPath = await editorSessionManager.getConnectedSocketPath(filePath) + const response: GetSessionResponse = { socketPath } + res.json(response) + }, + ) + + router.post<{}, string, AddSessionRequest | undefined>("/add-session", async (req, res) => { + const entry = req.body?.entry + if (!entry) { + throw new HttpError("entry is required", HttpCode.BadRequest) + } + editorSessionManager.addSession(entry) + res.status(200).send("session added") + }) + + router.post<{}, string, DeleteSessionRequest | undefined>("/delete-session", async (req, res) => { + const socketPath = req.body?.socketPath + if (!socketPath) { + throw new HttpError("socketPath is required", HttpCode.BadRequest) + } + editorSessionManager.deleteSession(socketPath) + res.status(200).send("session deleted") + }) + + router.use(errorHandler) + + const server = http.createServer(router) + try { + await listen(server, { socket: codeServerSocketPath }) + } catch (e) { + logger.warn(`Could not create socket at ${codeServerSocketPath}`) + } + return server +} + +export class EditorSessionManager { + // Map from socket path to EditorSessionEntry. + private entries = new Map() + + addSession(entry: EditorSessionEntry): void { + logger.debug(`Adding session to session registry: ${entry.socketPath}`) + this.entries.set(entry.socketPath, entry) + } + + getCandidatesForFile(filePath: string): EditorSessionEntry[] { + const matchCheckResults = new Map() + + const checkMatch = (entry: EditorSessionEntry): boolean => { + if (matchCheckResults.has(entry.socketPath)) { + return matchCheckResults.get(entry.socketPath)! + } + const result = entry.workspace.folders.some((folder) => filePath.startsWith(folder.uri.path + path.sep)) + matchCheckResults.set(entry.socketPath, result) + return result + } + + return Array.from(this.entries.values()) + .reverse() // Most recently registered first. + .sort((a, b) => { + // Matches first. + const aMatch = checkMatch(a) + const bMatch = checkMatch(b) + if (aMatch === bMatch) { + return 0 + } + if (aMatch) { + return -1 + } + return 1 + }) + } + + deleteSession(socketPath: string): void { + logger.debug(`Deleting session from session registry: ${socketPath}`) + this.entries.delete(socketPath) + } + + /** + * Returns the best socket path that we can connect to. + * We also delete any sockets that we can't connect to. + */ + async getConnectedSocketPath(filePath: string): Promise { + const candidates = this.getCandidatesForFile(filePath) + let match: EditorSessionEntry | undefined = undefined + + for (const candidate of candidates) { + if (await canConnect(candidate.socketPath)) { + match = candidate + break + } + this.deleteSession(candidate.socketPath) + } + + return match?.socketPath + } +} + +export class EditorSessionManagerClient { + constructor(private codeServerSocketPath: string) {} + + async canConnect() { + return canConnect(this.codeServerSocketPath) + } + + async getConnectedSocketPath(filePath: string): Promise { + const response = await new Promise((resolve, reject) => { + const opts = { + path: "/session?filePath=" + encodeURIComponent(filePath), + socketPath: this.codeServerSocketPath, + method: "GET", + } + const req = http.request(opts, (res) => { + let rawData = "" + res.setEncoding("utf8") + res.on("data", (chunk) => { + rawData += chunk + }) + res.on("end", () => { + try { + const obj = JSON.parse(rawData) + if (res.statusCode === 200) { + resolve(obj) + } else { + reject(new Error("Unexpected status code: " + res.statusCode)) + } + } catch (e: unknown) { + reject(e) + } + }) + }) + req.on("error", reject) + req.end() + }) + return response.socketPath + } + + // Currently only used for tests. + async addSession(request: AddSessionRequest): Promise { + await new Promise((resolve, reject) => { + const opts = { + path: "/add-session", + socketPath: this.codeServerSocketPath, + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json", + }, + } + const req = http.request(opts, () => { + resolve() + }) + req.on("error", reject) + req.write(JSON.stringify(request)) + req.end() + }) + } +} diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index 68eacbbcadc3..607512e2c9fb 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -3,7 +3,7 @@ import * as cp from "child_process" import * as path from "path" import * as rfs from "rotating-file-stream" import { Emitter } from "../common/emitter" -import { DefaultedArgs } from "./cli" +import { DefaultedArgs, redactArgs } from "./cli" import { paths } from "./util" const timeoutInterval = 10000 // 10s, matches VS Code's timeouts. @@ -44,10 +44,11 @@ export function onMessage( } const onMessage = (message: M) => { - ;(customLogger || logger).trace("got message", field("message", message)) if (fn(message)) { cleanup() resolve(message) + } else { + ;(customLogger || logger).debug("got unhandled message", field("message", message)) } } @@ -77,7 +78,10 @@ type ChildMessage = RelaunchMessage | ChildHandshakeMessage type ParentMessage = ParentHandshakeMessage class ProcessError extends Error { - public constructor(message: string, public readonly code: number | undefined) { + public constructor( + message: string, + public readonly code: number | undefined, + ) { super(message) this.name = this.constructor.name Error.captureStackTrace(this, this.constructor) @@ -147,7 +151,7 @@ abstract class Process { * Child process that will clean up after itself if the parent goes away and can * perform a handshake with the parent and ask it to relaunch. */ -class ChildProcess extends Process { +export class ChildProcess extends Process { public logger = logger.named(`child:${process.pid}`) public constructor(private readonly parentPid: number) { @@ -172,6 +176,7 @@ class ChildProcess extends Process { * Initiate the handshake and wait for a response from the parent. */ public async handshake(): Promise { + this.logger.debug("initiating handshake") this.send({ type: "handshake" }) const message = await onMessage( process, @@ -180,6 +185,13 @@ class ChildProcess extends Process { }, this.logger, ) + this.logger.debug( + "got message", + field("message", { + type: message.type, + args: redactArgs(message.args), + }), + ) return message.args } @@ -203,8 +215,9 @@ class ChildProcess extends Process { /** * Parent process wrapper that spawns the child process and performs a handshake - * with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by - * the child. If the child otherwise exits the parent will also exit. + * with it. Will relaunch the child if it receives a SIGUSR1 or SIGUSR2 or is + * asked to by the child. If the child otherwise exits the parent will also + * exit. */ export class ParentProcess extends Process { public logger = logger.named(`parent:${process.pid}`) @@ -227,12 +240,18 @@ export class ParentProcess extends Process { this.relaunch() }) + process.on("SIGUSR2", async () => { + this.logger.info("Received SIGUSR2; hotswapping") + this.relaunch() + }) + const opts = { size: "10M", maxFiles: 10, + path: path.join(paths.data, "coder-logs"), } - this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts) - this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts) + this.logStdoutStream = rfs.createStream("code-server-stdout.log", opts) + this.logStderrStream = rfs.createStream("code-server-stderr.log", opts) this.onDispose(() => this.disposeChild()) @@ -267,13 +286,17 @@ export class ParentProcess extends Process { try { this.started = this._start() await this.started - } catch (error) { + } catch (error: any) { this.logger.error(error.message) this.exit(typeof error.code === "number" ? error.code : 1) } } public start(args: DefaultedArgs): Promise { + // Our logger was created before we parsed CLI arguments so update the level + // in case it has changed. + this.logger.level = logger.level + // Store for relaunches. this.args = args if (!this.started) { @@ -286,17 +309,21 @@ export class ParentProcess extends Process { const child = this.spawn() this.child = child - // Log both to stdout and to the log directory. + // Log child output to stdout/stderr and to the log directory. if (child.stdout) { - child.stdout.pipe(this.logStdoutStream) - child.stdout.pipe(process.stdout) + child.stdout.on("data", (data) => { + this.logStdoutStream.write(data) + process.stdout.write(data) + }) } if (child.stderr) { - child.stderr.pipe(this.logStderrStream) - child.stderr.pipe(process.stderr) + child.stderr.on("data", (data) => { + this.logStderrStream.write(data) + process.stderr.write(data) + }) } - this.logger.debug(`spawned inner process ${child.pid}`) + this.logger.debug(`spawned child process ${child.pid}`) await this.handshake(child) @@ -307,14 +334,13 @@ export class ParentProcess extends Process { } private spawn(): cp.ChildProcess { - // Use spawn (instead of fork) to use the new binary in case it was updated. - return cp.spawn(process.argv[0], process.argv.slice(1), { + return cp.fork(path.join(__dirname, "entry"), { env: { ...process.env, CODE_SERVER_PARENT_PID: process.pid.toString(), - NODE_OPTIONS: `--max-old-space-size=2048 ${process.env.NODE_OPTIONS || ""}`, + NODE_EXEC_PATH: process.execPath, }, - stdio: ["inherit", "inherit", "inherit", "ipc"], + stdio: ["pipe", "pipe", "pipe", "ipc"], }) } @@ -325,13 +351,14 @@ export class ParentProcess extends Process { if (!this.args) { throw new Error("started without args") } - await onMessage( + const message = await onMessage( child, (message): message is ChildHandshakeMessage => { return message.type === "handshake" }, this.logger, ) + this.logger.debug("got message", field("message", message)) this.send(child, { type: "handshake", args: this.args }) } diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts index d829d08213d6..0f901bb8ad7f 100644 --- a/src/node/wsRouter.ts +++ b/src/node/wsRouter.ts @@ -1,29 +1,41 @@ import * as express from "express" import * as expressCore from "express-serve-static-core" import * as http from "http" +import * as stream from "stream" import Websocket from "ws" -import * as pluginapi from "../../typings/pluginapi" + +export interface WebsocketRequest extends express.Request { + ws: stream.Duplex + head: Buffer +} + +interface InternalWebsocketRequest extends WebsocketRequest { + _ws_handled: boolean +} export const handleUpgrade = (app: express.Express, server: http.Server): void => { server.on("upgrade", (req, socket, head) => { socket.pause() - req.ws = socket - req.head = head - req._ws_handled = false + const wreq = req as InternalWebsocketRequest + wreq.ws = socket + wreq.head = head + wreq._ws_handled = false // Send the request off to be handled by Express. - ;(app as any).handle(req, new http.ServerResponse(req), () => { - if (!req._ws_handled) { + ;(app as any).handle(wreq, new http.ServerResponse(wreq), () => { + if (!wreq._ws_handled) { socket.end("HTTP/1.1 404 Not Found\r\n\r\n") } }) }) } -interface InternalWebsocketRequest extends pluginapi.WebsocketRequest { - _ws_handled: boolean -} +export type WebSocketHandler = ( + req: WebsocketRequest, + res: express.Response, + next: express.NextFunction, +) => void | Promise export class WebsocketRouter { public readonly router = express.Router() @@ -31,14 +43,17 @@ export class WebsocketRouter { /** * Handle a websocket at this route. Note that websockets are immediately * paused when they come in. + * + * If the origin header exists it must match the host or the connection will + * be prevented. */ - public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void { + public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void { this.router.get( route, ...handlers.map((handler) => { const wrapped: express.Handler = (req, res, next) => { ;(req as InternalWebsocketRequest)._ws_handled = true - return handler(req as pluginapi.WebsocketRequest, res, next) + return handler(req as WebsocketRequest, res, next) } return wrapped }), diff --git a/test/e2e/appName.test.ts b/test/e2e/appName.test.ts new file mode 100644 index 000000000000..a12795cb0b33 --- /dev/null +++ b/test/e2e/appName.test.ts @@ -0,0 +1,9 @@ +import { version } from "../../src/node/constants" +import { describe, test, expect } from "./baseFixture" + +const appName = "testnäme" +describe("--app-name", [`--app-name=${appName}`], {}, () => { + test("should use app-name for the title", async ({ codeServerPage }) => { + expect(await codeServerPage.page.title()).toContain(appName) + }) +}) diff --git a/test/e2e/baseFixture.ts b/test/e2e/baseFixture.ts index a18722ed4bca..a1293f061c66 100644 --- a/test/e2e/baseFixture.ts +++ b/test/e2e/baseFixture.ts @@ -1,4 +1,3 @@ -import { field, logger } from "@coder/logger" import { test as base } from "@playwright/test" import { CodeServer, CodeServerPage } from "./models/CodeServer" @@ -9,10 +8,15 @@ import { CodeServer, CodeServerPage } from "./models/CodeServer" * * If `includeCredentials` is `true` page requests will be authenticated. */ -export const describe = (name: string, includeCredentials: boolean, fn: (codeServer: CodeServer) => void) => { +export const describe = ( + name: string, + codeServerArgs: string[], + codeServerEnv: NodeJS.ProcessEnv, + fn: (codeServer: CodeServer) => void, +) => { test.describe(name, () => { // This will spawn on demand so nothing is necessary on before. - const codeServer = new CodeServer(name) + const codeServer = new CodeServer(name, codeServerArgs, codeServerEnv, undefined) // Kill code-server after the suite has ended. This may happen even without // doing it explicitly but it seems prudent to be sure. @@ -20,22 +24,13 @@ export const describe = (name: string, includeCredentials: boolean, fn: (codeSer await codeServer.close() }) - const storageState = JSON.parse(process.env.STORAGE || "{}") - - // Sanity check to ensure the cookie is set. - const cookies = storageState?.cookies - if (includeCredentials && (!cookies || cookies.length !== 1 || !!cookies[0].key)) { - logger.error("no cookies", field("storage", JSON.stringify(cookies))) - throw new Error("no credentials to include") - } - test.use({ // Makes `codeServer` and `authenticated` available to the extend call // below. codeServer, - authenticated: includeCredentials, - // This provides a cookie that authenticates with code-server. - storageState: includeCredentials ? storageState : {}, + // NOTE@jsjoeio some tests use --cert which uses a self-signed certificate + // without this option, those tests will fail. + ignoreHTTPSErrors: true, }) fn(codeServer) @@ -43,7 +38,6 @@ export const describe = (name: string, includeCredentials: boolean, fn: (codeSer } interface TestFixtures { - authenticated: boolean codeServer: CodeServer codeServerPage: CodeServerPage } @@ -53,16 +47,15 @@ interface TestFixtures { * ready. */ export const test = base.extend({ - authenticated: false, codeServer: undefined, // No default; should be provided through `test.use`. - codeServerPage: async ({ authenticated, codeServer, page }, use) => { + codeServerPage: async ({ codeServer, page }, use) => { // It's possible code-server might prevent navigation because of unsaved // changes (seems to happen based on timing even if no changes have been // made too). In these cases just accept. page.on("dialog", (d) => d.accept()) const codeServerPage = new CodeServerPage(codeServer, page) - await codeServerPage.setup(authenticated) + await codeServerPage.navigate() await use(codeServerPage) }, }) diff --git a/test/e2e/browser.test.ts b/test/e2e/browser.test.ts deleted file mode 100644 index fab3ac8a2de3..000000000000 --- a/test/e2e/browser.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, test, expect } from "./baseFixture" - -// This is a "gut-check" test to make sure playwright is working as expected -describe("browser", true, () => { - test("browser should display correct userAgent", async ({ codeServerPage, browserName }) => { - const displayNames = { - chromium: "Chrome", - firefox: "Firefox", - webkit: "Safari", - } - const userAgent = await codeServerPage.page.evaluate(() => navigator.userAgent) - - expect(userAgent).toContain(displayNames[browserName]) - }) -}) diff --git a/test/e2e/codeServer.test.ts b/test/e2e/codeServer.test.ts index cfac6f74ceac..199298a1495e 100644 --- a/test/e2e/codeServer.test.ts +++ b/test/e2e/codeServer.test.ts @@ -1,6 +1,19 @@ +import { promises as fs } from "fs" +import * as path from "path" +import { getMaybeProxiedCodeServer } from "../utils/helpers" import { describe, test, expect } from "./baseFixture" +import { CodeServer } from "./models/CodeServer" + +describe("code-server", ["--disable-workspace-trust"], {}, () => { + // TODO@asher: Generalize this? Could be nice if we were to ever need + // multiple migration tests in other suites. + const instances = new Map() + test.afterAll(async () => { + const procs = Array.from(instances.values()) + instances.clear() + await Promise.all(procs.map((cs) => cs.close())) + }) -describe("CodeServer", true, () => { test("should navigate to home page", async ({ codeServerPage }) => { // We navigate codeServer before each test // and we start the test with a storage state @@ -9,19 +22,23 @@ describe("CodeServer", true, () => { const url = codeServerPage.page.url() // We use match because there may be a / at the end // so we don't want it to fail if we expect http://localhost:8080 to match http://localhost:8080/ - expect(url).toMatch(await codeServerPage.address()) + const address = await getMaybeProxiedCodeServer(codeServerPage) + expect(url).toMatch(address) }) test("should always see the code-server editor", async ({ codeServerPage }) => { expect(await codeServerPage.isEditorVisible()).toBe(true) }) - test("should always have a connection", async ({ codeServerPage }) => { - expect(await codeServerPage.isConnected()).toBe(true) - }) - test("should show the Integrated Terminal", async ({ codeServerPage }) => { await codeServerPage.focusTerminal() - expect(await codeServerPage.page.isVisible("#terminal")).toBe(true) + await expect(codeServerPage.page.locator("#terminal")).toBeVisible() + }) + + test("should open a file", async ({ codeServerPage }) => { + const dir = await codeServerPage.workspaceDir + const file = path.join(dir, "foo") + await fs.writeFile(file, "bar") + await codeServerPage.openFile(file) }) }) diff --git a/test/e2e/downloads.test.ts b/test/e2e/downloads.test.ts new file mode 100644 index 000000000000..f08e13b74e0a --- /dev/null +++ b/test/e2e/downloads.test.ts @@ -0,0 +1,108 @@ +import { promises as fs } from "fs" +import * as path from "path" +import { clean } from "../utils/helpers" +import { describe, test, expect } from "./baseFixture" + +describe("Downloads (enabled)", ["--disable-workspace-trust"], {}, async () => { + const testName = "downloads-enabled" + test.beforeAll(async () => { + await clean(testName) + }) + + test("should see the 'Download...' option", async ({ codeServerPage }) => { + // Setup + const workspaceDir = await codeServerPage.workspaceDir + const tmpFilePath = path.join(workspaceDir, "unique-file.txt") + await fs.writeFile(tmpFilePath, "hello world") + + // Action + await codeServerPage.openContextMenu("text=unique-file.txt") + + await expect(codeServerPage.page.locator("text=Download...")).toBeVisible() + }) + + test("should see the 'Show Local' button on Save As", async ({ codeServerPage }) => { + // Setup + const workspaceDir = await codeServerPage.workspaceDir + const fileName = "unique-file-save-as.txt" + const tmpFilePath = path.join(workspaceDir, fileName) + await fs.writeFile(tmpFilePath, "Hello World") + + // Action + await codeServerPage.page.waitForSelector(`text=${fileName}`) + + await codeServerPage.openFile(fileName) + await codeServerPage.page.click(".tab") + await codeServerPage.navigateMenus(["File", "Auto Save"]) + await codeServerPage.page.keyboard.type("Making some edits.") + await codeServerPage.navigateMenus(["File", "Save As..."]) + await codeServerPage.page.waitForSelector(".quick-input-widget") + await expect(codeServerPage.page.locator("text=Show Local")).toBeVisible() + }) + + test("should see the 'Show Local' button on Save File", async ({ codeServerPage }) => { + // Action + await codeServerPage.navigateMenus(["File", "New Text File"]) + await codeServerPage.waitForTab("Untitled-1") + await codeServerPage.navigateMenus(["File", "Save"]) + await codeServerPage.page.waitForSelector(".quick-input-widget") + await expect(codeServerPage.page.locator("text=Show Local")).toBeVisible() + }) + + test("should see the 'Show Local' button on Save Workspace As", async ({ codeServerPage }) => { + // Action + await codeServerPage.navigateMenus(["File", "Save Workspace As..."]) + await codeServerPage.page.waitForSelector(".quick-input-widget") + await expect(codeServerPage.page.locator("text=Show Local")).toBeVisible() + }) +}) + +describe("Downloads (disabled)", ["--disable-workspace-trust", "--disable-file-downloads"], {}, async () => { + const testName = "downloads-disabled" + test.beforeAll(async () => { + await clean(testName) + }) + + test("should not see the 'Download...' option", async ({ codeServerPage }) => { + // Setup + const workspaceDir = await codeServerPage.workspaceDir + const tmpFilePath = path.join(workspaceDir, "unique-file.txt") + await fs.writeFile(tmpFilePath, "Hello World") + + // Action + await codeServerPage.openContextMenu("text=unique-file.txt") + + await expect(codeServerPage.page.locator("text=Download...")).not.toBeVisible() + }) + + test("should not see the 'Show Local' button on Save as", async ({ codeServerPage }) => { + // Setup + const workspaceDir = await codeServerPage.workspaceDir + const fileName = "unique-file-save-as.txt" + const tmpFilePath = path.join(workspaceDir, fileName) + await fs.writeFile(tmpFilePath, "Hello World") + + // Action + await codeServerPage.page.waitForSelector(`text=${fileName}`) + await codeServerPage.openFile(fileName) + await codeServerPage.page.click(".tab") + await codeServerPage.navigateMenus(["File", "Save As..."]) + await expect(codeServerPage.page.locator("text=Show Local")).not.toBeVisible() + }) + + test("should not see the 'Show Local' button on Save File", async ({ codeServerPage }) => { + // Action + await codeServerPage.navigateMenus(["File", "New Text File"]) + await codeServerPage.waitForTab("Untitled-1") + await codeServerPage.navigateMenus(["File", "Save"]) + await codeServerPage.page.waitForSelector(".quick-input-widget") + await expect(codeServerPage.page.locator("text=Show Local")).not.toBeVisible() + }) + + test("should not see the 'Show Local' button on Save Workspace As", async ({ codeServerPage }) => { + // Action + await codeServerPage.navigateMenus(["File", "Save Workspace As..."]) + await codeServerPage.page.waitForSelector(".quick-input-widget") + await expect(codeServerPage.page.locator("text=Show Local")).not.toBeVisible() + }) +}) diff --git a/test/e2e/extensions.test.ts b/test/e2e/extensions.test.ts new file mode 100644 index 000000000000..9861e0004e7b --- /dev/null +++ b/test/e2e/extensions.test.ts @@ -0,0 +1,36 @@ +import { test as base } from "@playwright/test" +import * as path from "path" +import { getMaybeProxiedCodeServer } from "../utils/helpers" +import { describe, test, expect } from "./baseFixture" + +function runTestExtensionTests() { + // This will only work if the test extension is loaded into code-server. + test("should have access to VSCODE_PROXY_URI", async ({ codeServerPage }) => { + const address = await getMaybeProxiedCodeServer(codeServerPage) + + await codeServerPage.waitForTestExtensionLoaded() + await codeServerPage.executeCommandViaMenus("code-server: Get proxy URI") + + // Remove end slash in address. + const normalizedAddress = address.replace(/\/+$/, "") + await expect(codeServerPage.page.getByText(`Info: proxyUri: ${normalizedAddress}/proxy/{{port}}/`)).toBeVisible() + }) +} + +const flags = ["--disable-workspace-trust", "--extensions-dir", path.join(__dirname, "./extensions")] + +describe("Extensions", flags, {}, () => { + runTestExtensionTests() +}) + +if (process.env.USE_PROXY !== "1") { + describe("Extensions with --cert", [...flags, "--cert"], {}, () => { + runTestExtensionTests() + }) +} else { + base.describe("Extensions with --cert", () => { + base.skip("skipped because USE_PROXY is set", () => { + // Playwright will not show this without a function. + }) + }) +} diff --git a/test/e2e/extensions/test-extension/.gitignore b/test/e2e/extensions/test-extension/.gitignore new file mode 100644 index 000000000000..e7b307d8c4f7 --- /dev/null +++ b/test/e2e/extensions/test-extension/.gitignore @@ -0,0 +1 @@ +/extension.js diff --git a/test/e2e/extensions/test-extension/extension.ts b/test/e2e/extensions/test-extension/extension.ts new file mode 100644 index 000000000000..ea4a1f896ce6 --- /dev/null +++ b/test/e2e/extensions/test-extension/extension.ts @@ -0,0 +1,31 @@ +import * as vscode from "vscode" + +export function activate(context: vscode.ExtensionContext) { + vscode.window.showInformationMessage("test extension loaded") + // Test extension + context.subscriptions.push( + vscode.commands.registerCommand("codeServerTest.proxyUri", () => { + if (process.env.VSCODE_PROXY_URI) { + vscode.window.showInformationMessage(`proxyUri: ${process.env.VSCODE_PROXY_URI}`) + } else { + vscode.window.showErrorMessage("No proxy URI was set") + } + }), + ) + + // asExternalUri extension + context.subscriptions.push( + vscode.commands.registerCommand("codeServerTest.asExternalUri", async () => { + const input = await vscode.window.showInputBox({ + prompt: "URL to pass through to asExternalUri", + }) + + if (input) { + const output = await vscode.env.asExternalUri(vscode.Uri.parse(input)) + vscode.window.showInformationMessage(`input: ${input} output: ${output}`) + } else { + vscode.window.showErrorMessage(`Failed to run test case. No input provided.`) + } + }), + ) +} diff --git a/test/e2e/extensions/test-extension/package-lock.json b/test/e2e/extensions/test-extension/package-lock.json new file mode 100644 index 000000000000..ec9e89af85f3 --- /dev/null +++ b/test/e2e/extensions/test-extension/package-lock.json @@ -0,0 +1,41 @@ +{ + "name": "code-server-extension", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-server-extension", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@types/vscode": "^1.56.0", + "typescript": "^5.6.2" + }, + "engines": { + "vscode": "^1.56.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz", + "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/test/e2e/extensions/test-extension/package.json b/test/e2e/extensions/test-extension/package.json new file mode 100644 index 000000000000..c4a4fcefb6a3 --- /dev/null +++ b/test/e2e/extensions/test-extension/package.json @@ -0,0 +1,35 @@ +{ + "name": "code-server-extension", + "description": "code-server test extension", + "version": "0.0.1", + "publisher": "coder", + "license": "MIT", + "activationEvents": [ + "onStartupFinished" + ], + "engines": { + "vscode": "^1.56.0" + }, + "main": "./extension.js", + "contributes": { + "commands": [ + { + "command": "codeServerTest.proxyUri", + "title": "Get proxy URI", + "category": "code-server" + }, + { + "command": "codeServerTest.asExternalUri", + "title": "asExternalUri test", + "category": "code-server" + } + ] + }, + "devDependencies": { + "@types/vscode": "^1.56.0", + "typescript": "^5.6.2" + }, + "scripts": { + "build": "tsc" + } +} diff --git a/test/e2e/extensions/test-extension/tsconfig.json b/test/e2e/extensions/test-extension/tsconfig.json new file mode 100644 index 000000000000..17c0d751da96 --- /dev/null +++ b/test/e2e/extensions/test-extension/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": ".", + "strict": true, + "baseUrl": "./", + "skipLibCheck": true + }, + "include": ["./extension.ts"], + "exclude": ["node_modules"] +} diff --git a/test/e2e/github.test.ts b/test/e2e/github.test.ts new file mode 100644 index 000000000000..aa45886bd1f6 --- /dev/null +++ b/test/e2e/github.test.ts @@ -0,0 +1,38 @@ +import { test as base } from "@playwright/test" +import { describe, expect, test } from "./baseFixture" + +if (process.env.GITHUB_TOKEN) { + describe("GitHub token", ["--disable-workspace-trust"], {}, () => { + test("should be logged in to pull requests extension", async ({ codeServerPage }) => { + await codeServerPage.exec("git init") + await codeServerPage.exec("git remote add origin https://github.com/coder/code-server") + await codeServerPage.installExtension("GitHub.vscode-pull-request-github") + await codeServerPage.executeCommandViaMenus("View: Show Github") + await codeServerPage.page.click("text=Sign in") + await codeServerPage.page.click("text=Allow") + // It should ask to select an account, one of which will be the one we + // pre-injected. + await expect(codeServerPage.page.locator("text=Select an account")).not.toBeVisible() + }) + }) + + describe("No GitHub token", ["--disable-workspace-trust"], { GITHUB_TOKEN: "" }, () => { + test("should not be logged in to pull requests extension", async ({ codeServerPage }) => { + await codeServerPage.exec("git init") + await codeServerPage.exec("git remote add origin https://github.com/coder/code-server") + await codeServerPage.installExtension("GitHub.vscode-pull-request-github") + await codeServerPage.executeCommandViaMenus("View: Show Github") + await codeServerPage.page.click("text=Sign in") + await codeServerPage.page.click("text=Allow") + // Since there is no account it will ask directly for the token (because + // we are on localhost; otherwise it would initiate the oauth flow). + await expect(codeServerPage.page.locator("text=GitHub Personal Access Token")).not.toBeVisible() + }) + }) +} else { + base.describe("GitHub token", () => { + base.skip("skipped because GITHUB_TOKEN is not set", () => { + // Playwright will not show this without a function. + }) + }) +} diff --git a/test/e2e/globalSetup.test.ts b/test/e2e/globalSetup.test.ts deleted file mode 100644 index 8b4589b15349..000000000000 --- a/test/e2e/globalSetup.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { describe, test, expect } from "./baseFixture" - -// This test is to make sure the globalSetup works as expected -// meaning globalSetup ran and stored the storageState -describe("globalSetup", true, () => { - test("should keep us logged in using the storageState", async ({ codeServerPage }) => { - // Make sure the editor actually loaded - expect(await codeServerPage.isEditorVisible()).toBe(true) - }) -}) diff --git a/test/e2e/login.test.ts b/test/e2e/login.test.ts index bc9d5e8e9df3..2ce9ce8e8035 100644 --- a/test/e2e/login.test.ts +++ b/test/e2e/login.test.ts @@ -1,7 +1,7 @@ import { PASSWORD } from "../utils/constants" import { describe, test, expect } from "./baseFixture" -describe("login", false, () => { +describe("login", ["--disable-workspace-trust", "--auth", "password"], {}, () => { test("should see the login page", async ({ codeServerPage }) => { // It should send us to the login page expect(await codeServerPage.page.title()).toBe("code-server login") @@ -24,8 +24,8 @@ describe("login", false, () => { // Skip entering password // Click the submit button and login await codeServerPage.page.click(".submit") - await codeServerPage.page.waitForLoadState("networkidle") - expect(await codeServerPage.page.isVisible("text=Missing password")) + // The required input blocks empty submits, so the server-side error can't render. + await expect(codeServerPage.page.locator("input.password:invalid")).toBeVisible() }) test("should see an error message for incorrect password", async ({ codeServerPage }) => { @@ -34,28 +34,29 @@ describe("login", false, () => { // Click the submit button and login await codeServerPage.page.click(".submit") await codeServerPage.page.waitForLoadState("networkidle") - expect(await codeServerPage.page.isVisible("text=Incorrect password")) + await expect(codeServerPage.page.locator("text=Incorrect password")).toBeVisible() }) test("should hit the rate limiter for too many unsuccessful logins", async ({ codeServerPage }) => { - // Type in password - await codeServerPage.page.fill(".password", "password123") + test.slow() // Click the submit button and login // The current RateLimiter allows 2 logins per minute plus // 12 logins per hour for a total of 14 // See: src/node/routes/login.ts for (let i = 1; i <= 14; i++) { + await codeServerPage.page.fill(".password", "password123") await codeServerPage.page.click(".submit") await codeServerPage.page.waitForLoadState("networkidle") // We double-check that the correct error message shows // which should be for incorrect password - expect(await codeServerPage.page.isVisible("text=Incorrect password")) + await expect(codeServerPage.page.locator("text=Incorrect password")).toBeVisible() } // The 15th should fail for a different reason: // login rate + await codeServerPage.page.fill(".password", "password123") await codeServerPage.page.click(".submit") await codeServerPage.page.waitForLoadState("networkidle") - expect(await codeServerPage.page.isVisible("text=Login rate limited!")) + await expect(codeServerPage.page.locator("text=Login rate limited!")).toBeVisible() }) }) diff --git a/test/e2e/logout.test.ts b/test/e2e/logout.test.ts index 70c26160c712..eb9d57d659a8 100644 --- a/test/e2e/logout.test.ts +++ b/test/e2e/logout.test.ts @@ -1,7 +1,7 @@ // NOTE@jsjoeio commenting out until we can figure out what's wrong // import { describe, test, expect } from "./baseFixture" -// describe("logout", true, () => { +// describe("logout", true, ["--disable-workspace-trust"], {}, () => { // test("should be able logout", async ({ codeServerPage }) => { // // Recommended by Playwright for async navigation // // https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151 diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts index 3acb0cd7b192..4017445db752 100644 --- a/test/e2e/models/CodeServer.ts +++ b/test/e2e/models/CodeServer.ts @@ -1,25 +1,34 @@ -import { Logger, logger } from "@coder/logger" +import { field, Logger, logger } from "@coder/logger" import * as cp from "child_process" import { promises as fs } from "fs" import * as path from "path" import { Page } from "playwright" +import * as util from "util" +import { logError, normalize, plural } from "../../../src/common/util" import { onLine } from "../../../src/node/util" import { PASSWORD, workspaceDir } from "../../utils/constants" -import { tmpdir } from "../../utils/helpers" +import { getMaybeProxiedCodeServer, idleTimer, tmpdir } from "../../utils/helpers" interface CodeServerProcess { process: cp.ChildProcess address: string } -class CancelToken { +class Context { private _canceled = false + private _done = false public canceled(): boolean { return this._canceled } + public finished(): boolean { + return this._done + } public cancel(): void { this._canceled = true } + public finish(): void { + this._done = true + } } /** @@ -30,7 +39,13 @@ export class CodeServer { public readonly logger: Logger private closed = false - constructor(name: string) { + constructor( + name: string, + private readonly args: string[], + private readonly env: NodeJS.ProcessEnv, + private _workspaceDir: Promise | string | undefined, + private readonly entry = process.env.CODE_SERVER_TEST_ENTRY || ".", + ) { this.logger = logger.named(name) } @@ -43,22 +58,60 @@ export class CodeServer { this.process = this.spawn() } const { address } = await this.process + return address } + /** + * The workspace directory code-server opens with. + */ + get workspaceDir(): Promise | string { + if (!this._workspaceDir) { + this._workspaceDir = tmpdir(workspaceDir) + } + return this._workspaceDir + } + /** * Create a random workspace and seed it with settings. */ private async createWorkspace(): Promise { - const dir = await tmpdir(workspaceDir) - await fs.mkdir(path.join(dir, ".vscode")) + const dir = await this.workspaceDir + await fs.mkdir(path.join(dir, "User"), { recursive: true }) await fs.writeFile( - path.join(dir, ".vscode/settings.json"), + path.join(dir, "User/settings.json"), JSON.stringify({ "workbench.startupEditor": "none", + // Disable the welcome popup so we can avoid having to click through it + // on every test. + "workbench.welcomePage.experimentalOnboarding": false, }), "utf8", ) + + const extensionsDir = path.join(__dirname, "../extensions") + const languagepacksContent = { + es: { + hash: "8d919a946475223861fa0c62665a4c50", + extensions: [ + { + extensionIdentifier: { + id: "ms-ceintl.vscode-language-pack-es", + uuid: "47e020a1-33db-4cc0-a1b4-42f97781749a", + }, + version: "1.70.0", + }, + ], + translations: { + vscode: `${extensionsDir}/ms-ceintl.vscode-language-pack-es-1.70.0/translations/main.i18n.json`, + }, + label: "español", + }, + } + + // NOTE@jsjoeio - code-server should automatically generate the languagepacks.json for + // using different display languages. This is a temporary workaround until we fix that. + await fs.writeFile(path.join(dir, "languagepacks.json"), JSON.stringify(languagepacksContent)) return dir } @@ -67,72 +120,128 @@ export class CodeServer { * directories. */ private async spawn(): Promise { - // This will be used both as the workspace and data directory to ensure - // instances don't bleed into each other. const dir = await this.createWorkspace() - + const args = await this.argsWithDefaults([ + "--auth", + "none", + // The workspace to open. + ...(this.args.includes("--ignore-last-opened") ? [] : [dir]), + ...this.args, + // Using port zero will spawn on a random port. + "--bind-addr", + "127.0.0.1:0", + ]) return new Promise((resolve, reject) => { - this.logger.debug("spawning") - const proc = cp.spawn( - "node", - [ - process.env.CODE_SERVER_TEST_ENTRY || ".", - // Using port zero will spawn on a random port. - "--bind-addr", - "127.0.0.1:0", - // Setting the XDG variables would be easier and more thorough but the - // modules we import ignores those variables for non-Linux operating - // systems so use these flags instead. - "--config", - path.join(dir, "config.yaml"), - "--user-data-dir", - dir, - // The last argument is the workspace to open. - dir, - ], - { - cwd: path.join(__dirname, "../../.."), - env: { - ...process.env, - PASSWORD, - }, + this.logger.debug("spawning `node " + args.join(" ") + "`") + const proc = cp.spawn("node", args, { + cwd: path.join(__dirname, "../../.."), + env: { + ...process.env, + ...this.env, + // Prevent code-server from using the existing instance when running + // the e2e tests from an integrated terminal. + VSCODE_IPC_HOOK_CLI: "", + PASSWORD, }, - ) + }) + + const timer = idleTimer("Failed to extract address; did the format change?", reject) proc.on("error", (error) => { this.logger.error(error.message) + timer.dispose() reject(error) }) - proc.on("close", () => { - const error = new Error("closed unexpectedly") + proc.on("close", (code) => { + const error = new Error("code-server closed unexpectedly. Try running with LOG_LEVEL=debug to see more info.") if (!this.closed) { - this.logger.error(error.message) + this.logger.error(error.message, field("code", code)) } + timer.dispose() reject(error) }) + // Tracks when the HTTP and session servers are ready. + let httpAddress: string | undefined + let sessionAddress: string | undefined + let resolved = false proc.stdout.setEncoding("utf8") onLine(proc, (line) => { + // As long as we are actively getting input reset the timer. If we stop + // getting input and still have not found the addresses the timer will + // reject. + timer.reset() + // Log the line without the timestamp. - this.logger.trace(line.replace(/\[.+\]/, "")) + this.logger.debug(line.replace(/\[.+\]/, "")) if (resolved) { return } - const match = line.trim().match(/HTTP server listening on (https?:\/\/[.:\d]+)$/) + + let match = line.trim().match(/HTTPS? server listening on (https?:\/\/[.:\d]+)\/?$/) if (match) { - // Cookies don't seem to work on IP address so swap to localhost. + // Cookies don't seem to work on IP addresses so swap to localhost. // TODO: Investigate whether this is a bug with code-server. - const address = match[1].replace("127.0.0.1", "localhost") - this.logger.debug(`spawned on ${address}`) + httpAddress = match[1].replace("127.0.0.1", "localhost") + } + + match = line.trim().match(/Session server listening on (.+)$/) + if (match) { + sessionAddress = match[1] + } + + if (typeof httpAddress !== "undefined" && typeof sessionAddress !== "undefined") { resolved = true - resolve({ process: proc, address }) + timer.dispose() + this.logger.debug(`code-server is ready: ${httpAddress} ${sessionAddress}`) + resolve({ process: proc, address: httpAddress }) } }) }) } + /** + * Execute a short-lived command. + */ + async run(args: string[]): Promise { + args = await this.argsWithDefaults(args) + this.logger.debug("executing `node " + args.join(" ") + "`") + await util.promisify(cp.exec)("node " + args.join(" "), { + cwd: path.join(__dirname, "../../.."), + env: { + ...process.env, + ...this.env, + // Prevent code-server from using the existing instance when running + // the e2e tests from an integrated terminal. + VSCODE_IPC_HOOK_CLI: "", + }, + }) + } + + /** + * Combine arguments with defaults. + */ + private async argsWithDefaults(args: string[]): Promise { + // This will be used both as the workspace and data directory to ensure + // instances don't bleed into each other. + const dir = await this.workspaceDir + return [ + this.entry, + "--extensions-dir", + path.join(dir, "extensions"), + ...args, + // Setting the XDG variables would be easier and more thorough but the + // modules we import ignores those variables for non-Linux operating + // systems so use these flags instead. + "--config", + path.join(dir, "config.yaml"), + "--user-data-dir", + dir, + ] + } + /** * Close the code-server process. */ @@ -144,6 +253,13 @@ export class CodeServer { proc.kill() } } + + /** + * Whether or not authentication is enabled. + */ + authEnabled(): boolean { + return this.args.includes("password") + } } /** @@ -156,18 +272,46 @@ export class CodeServer { export class CodeServerPage { private readonly editorSelector = "div.monaco-workbench" - constructor(private readonly codeServer: CodeServer, public readonly page: Page) {} + constructor( + private readonly codeServer: CodeServer, + public readonly page: Page, + ) { + this.page.on("console", (message) => { + this.codeServer.logger.debug(message.text()) + }) + this.page.on("pageerror", (error) => { + logError(this.codeServer.logger, "page", error) + }) + } address() { return this.codeServer.address() } /** - * Navigate to code-server. + * The workspace directory code-server opens with. + */ + get workspaceDir() { + return this.codeServer.workspaceDir + } + + /** + * Navigate to a code-server endpoint (root by default). Then wait for the + * editor to become available. */ - async navigate() { - const address = await this.codeServer.address() - await this.page.goto(address, { waitUntil: "networkidle" }) + async navigate(endpoint = "/") { + const address = await getMaybeProxiedCodeServer(this.codeServer) + const noramlizedUrl = normalize(address + endpoint, true) + const to = new URL(noramlizedUrl) + + this.codeServer.logger.info(`navigating to ${to}`) + await this.page.goto(to.toString()) + + // Only reload editor if auth is not enabled. Otherwise we'll get stuck + // reloading the login page. + if (!this.codeServer.authEnabled()) { + await this.reloadUntilEditorIsReady() + } } /** @@ -180,14 +324,13 @@ export class CodeServerPage { this.codeServer.logger.debug("Waiting for editor to be ready...") const editorIsVisible = await this.isEditorVisible() - const editorIsConnected = await this.isConnected() let reloadCount = 0 // Occassionally code-server timeouts in Firefox // we're not sure why // but usually a reload or two fixes it // TODO@jsjoeio @oxy look into Firefox reconnection/timeout issues - while (!editorIsVisible && !editorIsConnected) { + while (!editorIsVisible) { // When a reload happens, we want to wait for all resources to be // loaded completely. Hence why we use that instead of DOMContentLoaded // Read more: https://thisthat.dev/dom-content-loaded-vs-load/ @@ -195,7 +338,7 @@ export class CodeServerPage { // Give it an extra second just in case it's feeling extra slow await this.page.waitForTimeout(1000) reloadCount += 1 - if ((await this.isEditorVisible()) && (await this.isConnected())) { + if (await this.isEditorVisible()) { this.codeServer.logger.debug(`editor became ready after ${reloadCount} reloads`) break } @@ -220,36 +363,71 @@ export class CodeServerPage { } /** - * Checks if the editor is visible + * Checks if the test extension loaded */ - async isConnected() { - this.codeServer.logger.debug("Waiting for network idle...") - - await this.page.waitForLoadState("networkidle") + async waitForTestExtensionLoaded(): Promise { + const selector = "text=test extension loaded" + this.codeServer.logger.debug("Waiting for test extension to load...") - const host = new URL(await this.codeServer.address()).host - // NOTE: This seems to be pretty brittle between version changes. - const hostSelector = `[aria-label="remote ${host}"]` - this.codeServer.logger.debug(`Waiting selector: ${hostSelector}`) - await this.page.waitForSelector(hostSelector) - - return await this.page.isVisible(hostSelector) + await this.page.waitForSelector(selector) } /** - * Focuses Integrated Terminal - * by using "Terminal: Focus Terminal" - * from the Command Palette + * Focuses the integrated terminal by navigating through the command palette. * - * This should focus the terminal no matter - * if it already has focus and/or is or isn't - * visible already. + * This should focus the terminal no matter if it already has focus and/or is + * or isn't visible already. It will always create a new terminal to avoid + * clobbering parallel tests. */ async focusTerminal() { - await this.executeCommandViaMenus("Terminal: Focus Terminal") + const doFocus = async (): Promise => { + await this.executeCommandViaMenus("Terminal: Create New Terminal") + try { + await this.page.waitForLoadState("load") + await this.page.waitForSelector("textarea.xterm-helper-textarea:focus-within", { timeout: 5000 }) + return true + } catch (error) { + return false + } + } + + let attempts = 1 + while (!(await doFocus())) { + ++attempts + this.codeServer.logger.debug(`no focused terminal textarea, retrying (${attempts}/∞)`) + } - // Wait for terminal textarea to show up - await this.page.waitForSelector("textarea.xterm-helper-textarea") + this.codeServer.logger.debug(`opening terminal took ${attempts} ${plural(attempts, "attempt")}`) + } + + /** + * Open a file by using menus. + */ + async openFile(file: string) { + await this.navigateMenus(["File", "Open File..."]) + await this.navigateQuickInput([path.basename(file)]) + await this.waitForTab(file) + } + + /** + * Open a file through an external command. + */ + async openFileExternally(file: string) { + await this.codeServer.run(["--reuse-window", file]) + } + + /** + * Wait for a tab to open for the specified file. + */ + async waitForTab(file: string): Promise { + await this.page.waitForSelector(`.tab :text("${path.basename(file)}")`) + } + + /** + * See if the specified tab is open. + */ + async tabIsVisible(file: string): Promise { + return this.page.isVisible(`.tab :text("${path.basename(file)}")`) } /** @@ -257,7 +435,7 @@ export class CodeServerPage { * it then clicking the match from the results. */ async executeCommandViaMenus(command: string) { - await this.navigateMenus(["View", "Command Palette"]) + await this.navigateMenus(["View", "Command Palette..."]) await this.page.keyboard.type(command) @@ -266,13 +444,45 @@ export class CodeServerPage { } /** - * Navigate through the specified set of menus. If it fails it will keep - * trying. + * Navigate through the items in the selector. `open` is a function that will + * open the menu/popup containing the items through which to navigation. */ - async navigateMenus(menus: string[]) { - const navigate = async (cancelToken: CancelToken) => { - const steps: Array<() => Promise> = [() => this.page.waitForSelector(`${menuSelector}:focus-within`)] - for (const menu of menus) { + async navigateItems(items: string[], selector: string, open?: (selector: string) => void): Promise { + const logger = this.codeServer.logger.named(selector) + + /** + * If the selector loses focus or gets removed this will resolve with false, + * signaling we need to try again. + */ + const openThenWaitClose = async (ctx: Context) => { + if (open) { + await open(selector) + } + this.codeServer.logger.debug(`watching ${selector}`) + try { + await this.page.waitForSelector(`${selector}:not(:focus-within)`) + } catch (error) { + if (!ctx.finished()) { + this.codeServer.logger.debug(`${selector} navigation: ${(error as any).message || error}`) + } + } + return false + } + + /** + * This will step through each item, aborting and returning false if + * canceled or if any navigation step has an error which signals we need to + * try again. + */ + const navigate = async (ctx: Context) => { + const steps: Array<{ fn: () => Promise; name: string }> = [ + { + fn: () => this.page.waitForSelector(`${selector}:focus-within`), + name: "focus", + }, + ] + + for (const item of items) { // Normally these will wait for the item to be visible and then execute // the action. The problem is that if the menu closes these will still // be waiting and continue to execute once the menu is visible again, @@ -280,56 +490,111 @@ export class CodeServerPage { // if the old promise clicks logout before the new one can). By // splitting them into two steps each we can cancel before running the // action. - steps.push(() => this.page.hover(`text=${menu}`, { trial: true })) - steps.push(() => this.page.hover(`text=${menu}`, { force: true })) - steps.push(() => this.page.click(`text=${menu}`, { trial: true })) - steps.push(() => this.page.click(`text=${menu}`, { force: true })) + steps.push({ + fn: () => this.page.hover(`${selector} :text-is("${item}")`, { trial: true }), + name: `${item}:hover:trial`, + }) + steps.push({ + fn: () => this.page.hover(`${selector} :text-is("${item}")`, { force: true }), + name: `${item}:hover:force`, + }) + steps.push({ + fn: () => this.page.click(`${selector} :text-is("${item}")`, { trial: true }), + name: `${item}:click:trial`, + }) + steps.push({ + fn: () => this.page.click(`${selector} :text-is("${item}")`, { force: true }), + name: `${item}:click:force`, + }) } + for (const step of steps) { - await step() - if (cancelToken.canceled()) { - this.codeServer.logger.debug("menu navigation canceled") + try { + logger.debug(`navigation step: ${step.name}`) + await step.fn() + if (ctx.canceled()) { + logger.debug("navigation canceled") + return false + } + } catch (error) { + logger.debug(`navigation: ${(error as any).message || error}`) return false } } return true } - const menuSelector = '[aria-label="Application Menu"]' - const open = async () => { - await this.page.click(menuSelector) - await this.page.waitForSelector(`${menuSelector}:not(:focus-within)`) - return false + // We are seeing the menu closing after opening if we open it too soon and + // the picker getting recreated in the middle of trying to select an item. + // To counter this we will keep trying to navigate through the items every + // time we lose focus or there is an error. + let attempts = 1 + let context = new Context() + while (!(await Promise.race([openThenWaitClose(context), navigate(context)]))) { + ++attempts + logger.debug(`closed, retrying (${attempts}/∞)`) + context.cancel() + context = new Context() } - // TODO: Starting in 1.57 something closes the menu after opening it if we - // open it too soon. To counter that we'll watch for when the menu loses - // focus and when/if it does we'll try again. - // I tried using the classic menu but it doesn't show up at all for some - // reason. I also tried toggle but the menu disappears after toggling. - let retryCount = 0 - let cancelToken = new CancelToken() - while (!(await Promise.race([open(), navigate(cancelToken)]))) { - this.codeServer.logger.debug("menu was closed, retrying") - ++retryCount - cancelToken.cancel() - cancelToken = new CancelToken() - } + context.finish() + logger.debug(`navigation took ${attempts} ${plural(attempts, "attempt")}`) + } - this.codeServer.logger.debug(`menu navigation retries: ${retryCount}`) + /** + * Navigate through a currently opened "quick input" widget, retrying on + * failure. + */ + async navigateQuickInput(items: string[]): Promise { + await this.navigateItems(items, ".quick-input-widget") } /** - * Navigates to code-server then reloads until the editor is ready. - * - * It is recommended to run setup before using this model in any tests. + * Navigate through the menu, retrying on failure. */ - async setup(authenticated: boolean) { - await this.navigate() - // If we aren't authenticated we'll see a login page so we can't wait until - // the editor is ready. - if (authenticated) { - await this.reloadUntilEditorIsReady() - } + async navigateMenus(menus: string[]): Promise { + await this.navigateItems(menus, '[aria-label="Application Menu"]', async (selector) => { + await this.page.click(selector) + }) + } + + /** + * Open context menu on the specified selector. + */ + async openContextMenu(selector: string): Promise { + const el = await this.page.waitForSelector(selector) + await el.click({ button: "right" }) + await this.page.waitForSelector(".context-view-block") + } + + /** + * Execute a command in the root of the instance's workspace directory. + */ + async exec(command: string): Promise { + await util.promisify(cp.exec)(command, { + cwd: await this.workspaceDir, + }) + } + + /** + * Install an extension by ID to the instance's temporary extension + * directory. + */ + async installExtension(id: string): Promise { + const dir = path.join(await this.workspaceDir, "extensions") + await util.promisify(cp.exec)(`node . --install-extension ${id} --extensions-dir ${dir}`, { + cwd: path.join(__dirname, "../../.."), + }) + } + + /** + * Wait for state to be flushed to the database. + */ + async stateFlush(): Promise { + // If we reload too quickly VS Code will be unable to save the state changes + // so wait until those have been written to the database. It flushes every + // five seconds so we need to wait at least that long. + // TODO@asher: There must be a better way. + await this.page.waitForTimeout(5500) } } diff --git a/test/e2e/openHelpAbout.test.ts b/test/e2e/openHelpAbout.test.ts index 3caa10c0c214..b21fca35ea41 100644 --- a/test/e2e/openHelpAbout.test.ts +++ b/test/e2e/openHelpAbout.test.ts @@ -1,12 +1,17 @@ +import { version } from "../../src/node/constants" import { describe, test, expect } from "./baseFixture" -describe("Open Help > About", true, () => { +describe("Open Help > About", ["--disable-workspace-trust"], {}, () => { test("should see code-server version in about dialog", async ({ codeServerPage }) => { // Open using the menu. await codeServerPage.navigateMenus(["Help", "About"]) + const isDevMode = process.env.VSCODE_DEV === "1" + // Look for code-server info div. - const element = await codeServerPage.page.waitForSelector('div[role="dialog"] >> text=code-server') + const element = await codeServerPage.page.waitForSelector( + `div[role="dialog"] >> text=code-server: ${isDevMode ? "Unknown" : "v" + version}`, + ) expect(element).not.toBeNull() }) }) diff --git a/test/e2e/routes.test.ts b/test/e2e/routes.test.ts new file mode 100644 index 000000000000..39c19697e9b9 --- /dev/null +++ b/test/e2e/routes.test.ts @@ -0,0 +1,125 @@ +import { clean, getMaybeProxiedPathname } from "../utils/helpers" +import { describe, test, expect } from "./baseFixture" + +const routes = { + "/": [ + /\.\/manifest.json/, + /\.\/_static\//, + /[a-z]+-[0-9a-z]+\/static\//, + /http:\/\/localhost:[0-9]+\/[a-z]+-[0-9a-z]+\/static\//, + ], + "/vscode": [ + /\.\/vscode\/manifest.json/, + /\.\/_static\//, + /vscode\/[a-z]+-[0-9a-z]+\/static\//, + /http:\/\/localhost:[0-9]+\/vscode\/[a-z]+-[0-9a-z]+\/static\//, + ], + "/vscode/": [ + /\.\/manifest.json/, + /\.\/\.\.\/_static\//, + /[a-z]+-[0-9a-z]+\/static\//, + /http:\/\/localhost:[0-9]+\/vscode\/[a-z]+-[0-9a-z]+\/static\//, + ], +} + +describe("VS Code Routes", ["--disable-workspace-trust"], {}, async () => { + const testName = "vscode-routes-default" + test.beforeAll(async () => { + await clean(testName) + }) + + test("should load all route variations", async ({ codeServerPage }) => { + for (const [route, matchers] of Object.entries(routes)) { + await codeServerPage.navigate(route) + + // Check there were no redirections + const url = new URL(codeServerPage.page.url()) + const pathname = getMaybeProxiedPathname(url) + expect(pathname).toBe(route) + + // Check that assets are pointing to the right spot. Some will be + // relative, without a leading dot (VS Code's assets). Some will be + // relative with a leading dot (our assets). Others will have been + // resolved against the origin. + const elements = await codeServerPage.page.locator("[src]").all() + for (const element of elements) { + const src = await element.getAttribute("src") + if (src && !matchers.some((m) => m.test(src))) { + throw new Error(`${src} did not match any validators for route ${route}`) + } + } + } + }) +}) + +const CODE_WORKSPACE_DIR = process.env.CODE_WORKSPACE_DIR || "" +describe("VS Code Routes with code-workspace", ["--disable-workspace-trust", CODE_WORKSPACE_DIR], {}, async () => { + test("should redirect to the passed in workspace using human-readable query", async ({ codeServerPage }) => { + const url = new URL(codeServerPage.page.url()) + const pathname = getMaybeProxiedPathname(url) + expect(pathname).toBe("/") + expect(url.search).toBe(`?workspace=${CODE_WORKSPACE_DIR}`) + }) +}) + +const CODE_FOLDER_DIR = process.env.CODE_FOLDER_DIR || "" +describe("VS Code Routes with code-workspace", ["--disable-workspace-trust", CODE_FOLDER_DIR], {}, async () => { + test("should redirect to the passed in folder using human-readable query", async ({ codeServerPage }) => { + const url = new URL(codeServerPage.page.url()) + const pathname = getMaybeProxiedPathname(url) + expect(pathname).toBe("/") + expect(url.search).toBe(`?folder=${CODE_FOLDER_DIR}`) + }) +}) + +describe( + "VS Code Routes with ignore-last-opened", + ["--disable-workspace-trust", "--ignore-last-opened"], + {}, + async () => { + test("should not redirect", async ({ codeServerPage }) => { + const folder = process.env.CODE_FOLDER_DIR + + await codeServerPage.navigate(`/?folder=${folder}`) + await codeServerPage.navigate(`/`) + + const url = new URL(codeServerPage.page.url()) + const pathname = getMaybeProxiedPathname(url) + expect(pathname).toBe("/") + expect(url.search).toBe("") + }) + }, +) + +describe("VS Code Routes with no workspace or folder", ["--disable-workspace-trust"], {}, async () => { + test("should redirect to last query folder/workspace", async ({ codeServerPage }) => { + const folder = process.env.CODE_FOLDER_DIR + const workspace = process.env.CODE_WORKSPACE_DIR + await codeServerPage.navigate(`/?folder=${folder}&workspace=${workspace}`) + + // If you visit again without query parameters it will re-attach them by + // redirecting. It should always redirect to the same route. + for (const route of Object.keys(routes)) { + await codeServerPage.navigate(route) + const url = new URL(codeServerPage.page.url()) + const pathname = getMaybeProxiedPathname(url) + expect(pathname).toBe(route) + expect(url.search).toBe(`?folder=${folder}&workspace=${workspace}`) + } + }) +}) + +describe("VS Code Routes with no workspace or folder", ["--disable-workspace-trust"], {}, async () => { + test("should not redirect if ew passed in", async ({ codeServerPage }) => { + const folder = process.env.CODE_FOLDER_DIR + const workspace = process.env.CODE_WORKSPACE_DIR + await codeServerPage.navigate(`/?folder=${folder}&workspace=${workspace}`) + + // Closing the folder should stop the redirecting. + await codeServerPage.navigate("/?ew=true") + const url = new URL(codeServerPage.page.url()) + const pathname = getMaybeProxiedPathname(url) + expect(pathname).toBe("/") + expect(url.search).toBe("?ew=true") + }) +}) diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index 836583a8d566..0de4f222a2a3 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -1,29 +1,20 @@ import * as cp from "child_process" -import * as fs from "fs" +import { promises as fs } from "fs" import * as path from "path" import util from "util" -import { tmpdir } from "../utils/helpers" +import { clean, getMaybeProxiedCodeServer, tmpdir } from "../utils/helpers" import { describe, expect, test } from "./baseFixture" -describe("Integrated Terminal", true, () => { - // Create a new context with the saved storage state - // so we don't have to logged in - const testFileName = "pipe" - const testString = "new string test from e2e test" - let tmpFolderPath = "" - let tmpFile = "" - +describe("Integrated Terminal", ["--disable-workspace-trust"], {}, () => { + const testName = "integrated-terminal" test.beforeAll(async () => { - tmpFolderPath = await tmpdir("integrated-terminal") - tmpFile = path.join(tmpFolderPath, testFileName) + await clean(testName) }) - test.afterAll(async () => { - // Ensure directory was removed - await fs.promises.rmdir(tmpFolderPath, { recursive: true }) - }) + test("should have access to VSCODE_PROXY_URI", async ({ codeServerPage }) => { + const tmpFolderPath = await tmpdir(testName) + const tmpFile = path.join(tmpFolderPath, "pipe") - test("should echo a string to a file", async ({ codeServerPage }) => { const command = `mkfifo '${tmpFile}' && cat '${tmpFile}'` const exec = util.promisify(cp.exec) const output = exec(command, { encoding: "utf8" }) @@ -31,13 +22,32 @@ describe("Integrated Terminal", true, () => { // Open terminal and type in value await codeServerPage.focusTerminal() - await codeServerPage.page.waitForLoadState("load") - await codeServerPage.page.keyboard.type(`echo ${testString} > ${tmpFile}`) + await codeServerPage.page.keyboard.type(`printenv VSCODE_PROXY_URI > ${tmpFile}`) await codeServerPage.page.keyboard.press("Enter") - // It may take a second to process - await codeServerPage.page.waitForTimeout(1000) const { stdout } = await output - expect(stdout).toMatch(testString) + const address = await getMaybeProxiedCodeServer(codeServerPage) + expect(stdout).toMatch(address) + }) + + // TODO@jsjoeio - add test to make sure full code-server path works + test("should be able to invoke `code-server` to open a file", async ({ codeServerPage }) => { + const tmpFolderPath = await tmpdir(testName) + const tmpFile = path.join(tmpFolderPath, "test-file") + await fs.writeFile(tmpFile, "test") + + await codeServerPage.focusTerminal() + + await codeServerPage.page.keyboard.type(`code-server ${tmpFile}`) + await codeServerPage.page.keyboard.press("Enter") + + await codeServerPage.waitForTab(path.basename(tmpFile)) + + const externalTmpFile = path.join(tmpFolderPath, "test-external-file") + await fs.writeFile(externalTmpFile, "foobar") + + await codeServerPage.openFileExternally(externalTmpFile) + + await codeServerPage.waitForTab(path.basename(externalTmpFile)) }) }) diff --git a/test/e2e/uploads.test.ts b/test/e2e/uploads.test.ts new file mode 100644 index 000000000000..05bb9ffd98a3 --- /dev/null +++ b/test/e2e/uploads.test.ts @@ -0,0 +1,56 @@ +import { promises as fs } from "fs" +import * as path from "path" +import { clean } from "../utils/helpers" +import { describe, test, expect } from "./baseFixture" + +describe("Uploads (enabled)", ["--disable-workspace-trust"], {}, () => { + const testName = "uploads-enabled" + test.beforeAll(async () => { + await clean(testName) + }) + + test("should see the 'Upload...' option", async ({ codeServerPage }) => { + // Setup + const workspaceDir = await codeServerPage.workspaceDir + const tmpDirPath = path.join(workspaceDir, "test-directory") + await fs.mkdir(tmpDirPath) + + // Action + await codeServerPage.openContextMenu('span:has-text("test-directory")') + + await expect(codeServerPage.page.locator("text=Upload...")).toBeVisible() + }) + + test("should see the 'Show Local' button on Open File", async ({ codeServerPage }) => { + // Action + await codeServerPage.navigateMenus(["File", "Open File..."]) + await codeServerPage.page.waitForSelector(".quick-input-widget") + await expect(codeServerPage.page.locator("text=Show Local")).toBeVisible() + }) +}) + +describe("Uploads (disabled)", ["--disable-workspace-trust", "--disable-file-uploads"], {}, () => { + const testName = "uploads-disabled" + test.beforeAll(async () => { + await clean(testName) + }) + + test("should not see the 'Upload...' option", async ({ codeServerPage }) => { + // Setup + const workspaceDir = await codeServerPage.workspaceDir + const tmpDirPath = path.join(workspaceDir, "test-directory") + await fs.mkdir(tmpDirPath) + + // Action + await codeServerPage.openContextMenu('span:has-text("test-directory")') + + await expect(codeServerPage.page.locator("text=Upload...")).not.toBeVisible() + }) + + test("should not see the 'Show Local' button on Open File", async ({ codeServerPage }) => { + // Action + await codeServerPage.navigateMenus(["File", "Open File..."]) + await codeServerPage.page.waitForSelector(".quick-input-widget") + await expect(codeServerPage.page.locator("text=Show Local")).not.toBeVisible() + }) +}) diff --git a/test/e2e/webview.test.ts b/test/e2e/webview.test.ts new file mode 100644 index 000000000000..e5a783f37322 --- /dev/null +++ b/test/e2e/webview.test.ts @@ -0,0 +1,25 @@ +import { promises as fs } from "fs" +import * as path from "path" +import { describe, test, expect } from "./baseFixture" + +describe("Webviews", ["--disable-workspace-trust"], {}, () => { + test("should preview a Markdown file", async ({ codeServerPage }) => { + // Create Markdown file + const heading = "Hello world" + const dir = await codeServerPage.workspaceDir + const file = path.join(dir, "text.md") + await fs.writeFile(file, `# ${heading}`) + await codeServerPage.openFile(file) + + // Open Preview + await codeServerPage.executeCommandViaMenus("Markdown: Open Preview to the Side") + // Wait for the iframe to open and load + await codeServerPage.waitForTab(`Preview ${file}`) + + // It's an iframe within an iframe + // so we have to do .frameLocator twice + await expect( + codeServerPage.page.frameLocator("iframe.webview.ready").frameLocator("#active-frame").getByText("Hello world"), + ).toBeVisible() + }) +}) diff --git a/test/integration/help.test.ts b/test/integration/help.test.ts new file mode 100644 index 000000000000..12786f282524 --- /dev/null +++ b/test/integration/help.test.ts @@ -0,0 +1,13 @@ +import { runCodeServerCommand } from "../utils/runCodeServerCommand" + +// NOTE@jsjoeio +// We have this test to ensure that native modules +// work as expected. If this is called on the wrong +// platform, the test will fail. +describe("--help", () => { + it("should list code-server usage", async () => { + const expectedOutput = "Usage: code-server [options] [path]" + const { stdout } = await runCodeServerCommand(["--help"]) + expect(stdout).toMatch(expectedOutput) + }, 20000) +}) diff --git a/test/integration/installExtension.test.ts b/test/integration/installExtension.test.ts new file mode 100644 index 000000000000..5986c964e912 --- /dev/null +++ b/test/integration/installExtension.test.ts @@ -0,0 +1,22 @@ +import { clean, tmpdir } from "../utils/helpers" +import { runCodeServerCommand } from "../utils/runCodeServerCommand" + +describe("--install-extension", () => { + const testName = "installExtension" + let tempDir: string + let setupFlags: string[] + + beforeEach(async () => { + await clean(testName) + tempDir = await tmpdir(testName) + setupFlags = ["--extensions-dir", tempDir] + }) + it("should use EXTENSIONS_GALLERY when set", async () => { + const extName = "author.extension" + await expect( + runCodeServerCommand([...setupFlags, "--install-extension", extName], { + EXTENSIONS_GALLERY: "{}", + }), + ).rejects.toThrow("No extension gallery service configured") + }) +}) diff --git a/test/package-lock.json b/test/package-lock.json new file mode 100644 index 000000000000..6032ad15f1ab --- /dev/null +++ b/test/package-lock.json @@ -0,0 +1,5043 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "license": "MIT", + "devDependencies": { + "@jest-mock/express": "^1.4.5", + "@playwright/test": "^1.61.0", + "@types/jest": "^27.0.2", + "@types/jsdom": "^16.2.13", + "@types/node-fetch": "^2.5.8", + "@types/wtfnode": "^0.7.0", + "argon2": "^0.44.0", + "extract-zip": "^2.0.1", + "jest": "^27.3.1", + "jest-fetch-mock": "^3.0.3", + "jsdom": "^16.4.0", + "node-fetch": "^2.6.7", + "playwright": "^1.61.0", + "ts-jest": "^27.0.7", + "wtfnode": "^0.9.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", + "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "convert-source-map": "^2.0.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": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "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-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "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-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, + "license": "MIT", + "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.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "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, + "license": "MIT" + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest-mock/express": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@jest-mock/express/-/express-1.4.5.tgz", + "integrity": "sha512-bERM1jnutyH7VMahdaOHAKy7lgX47zJ7+RTz2eMz0wlCttd9CkhsKFEyoWmJBSz/ow0nVj3lCuRqLem4QDYFkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/console": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/core": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/source-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9", + "source-map": "^0.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "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.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "16.2.15", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.15.tgz", + "integrity": "sha512-nwF87yjBKuX/roqGYerZZM0Nv1pZDMAT5YhOHYeM/72Fic+VEqJh4nyoqoapzJnW3pUlfxPY5FhgsJtM+dRnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/parse5": "^6.0.3", + "@types/tough-cookie": "*" + } + }, + "node_modules/@types/node": { + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/wtfnode": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@types/wtfnode/-/wtfnode-0.7.3.tgz", + "integrity": "sha512-UMkHpx+o2xRWLJ7PmT3bBzvIA9/0oFw80oPtY/xO4jfdq+Gznn4wP7K9B/JjMxyxy+wF+5oRPIykxeBbEDjwRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "16.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", + "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argon2": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz", + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.0.0", + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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, + "license": "BSD-3-Clause", + "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-jest-hoist": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@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-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^27.5.1", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "Apache-2.0", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001667", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", + "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.32", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", + "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "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, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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==", + "dev": true, + "license": "BSD-2-Clause", + "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-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, + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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/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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "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==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BSD-3-Clause", + "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/istanbul-lib-instrument/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "license": "BSD-3-Clause", + "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.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^27.5.1", + "import-local": "^3.0.2", + "jest-cli": "^27.5.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-cli": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, + "node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-jasmine2": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.5.1", + "leven": "^3.1.0", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.5.1", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/form-data": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "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, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "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-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, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", + "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throat": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", + "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-jest": { + "version": "27.1.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz", + "integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^27.0.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@types/jest": "^27.0.0", + "babel-jest": ">=27.0.0 <28", + "jest": "^27.0.0", + "typescript": ">=3.8 <5.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/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, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wtfnode": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.3.tgz", + "integrity": "sha512-MXjgxJovNVYUkD85JBZTKT5S5ng/e56sNuRZlid7HcGTNrIODa5UPtqE3i0daj7fJ2SGj5Um2VmiphQVyVKK5A==", + "dev": true, + "license": "ISC", + "bin": { + "wtfnode": "proxy.js" + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/test/package.json b/test/package.json index 1a7f46655f5e..46ae308fda52 100644 --- a/test/package.json +++ b/test/package.json @@ -2,26 +2,27 @@ "license": "MIT", "#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.", "devDependencies": { - "@playwright/test": "^1.12.1", - "@types/jest": "^26.0.20", - "@types/jsdom": "^16.2.6", + "@jest-mock/express": "^1.4.5", + "@playwright/test": "^1.61.0", + "@types/jest": "^27.0.2", + "@types/jsdom": "^16.2.13", "@types/node-fetch": "^2.5.8", - "@types/supertest": "^2.0.10", "@types/wtfnode": "^0.7.0", - "argon2": "^0.28.0", - "jest": "^26.6.3", + "argon2": "^0.44.0", + "extract-zip": "^2.0.1", + "jest": "^27.3.1", "jest-fetch-mock": "^3.0.3", "jsdom": "^16.4.0", - "node-fetch": "^2.6.1", - "playwright": "^1.12.1", - "supertest": "^6.1.1", - "ts-jest": "^26.4.4", - "wtfnode": "^0.9.0" + "node-fetch": "^2.6.7", + "playwright": "^1.61.0", + "ts-jest": "^27.0.7", + "wtfnode": "^0.9.1" }, "resolutions": { "ansi-regex": "^5.0.1", - "argon2/@mapbox/node-pre-gyp/tar": "^6.1.9", "set-value": "^4.0.1", - "tmpl": "^1.0.5" + "tmpl": "^1.0.5", + "path-parse": "^1.0.7", + "json-schema": "^0.4.0" } } diff --git a/test/playwright.config.ts b/test/playwright.config.ts index 679dd33f9399..1de4b92ca3b6 100644 --- a/test/playwright.config.ts +++ b/test/playwright.config.ts @@ -2,12 +2,20 @@ import { PlaywrightTestConfig } from "@playwright/test" import path from "path" -// Run tests in three browsers. +// The default configuration runs all tests in three browsers with workers equal +// to half the available threads. See 'npm run test:e2e --help' to customize +// from the command line. For example: +// npm run test:e2e -- --workers 1 # Run with one worker +// npm run test:e2e -- --project Chromium # Only run on Chromium +// npm run test:e2e -- --grep login # Run tests matching "login" +// PWDEBUG=1 npm run test:e2e # Run Playwright inspector const config: PlaywrightTestConfig = { testDir: path.join(__dirname, "e2e"), // Search for tests in this directory. timeout: 60000, // Each test is given 60 seconds. retries: process.env.CI ? 2 : 1, // Retry in CI due to flakiness. - globalSetup: require.resolve("./utils/globalSetup.ts"), + // Limit the number of failures on CI to save resources + maxFailures: process.env.CI ? 3 : undefined, + globalSetup: require.resolve("./utils/globalE2eSetup.ts"), reporter: "list", // Put any shared options on the top level. use: { @@ -20,16 +28,17 @@ const config: PlaywrightTestConfig = { name: "Chromium", use: { browserName: "chromium" }, }, - - { - name: "Firefox", - use: { browserName: "firefox" }, - }, - - { - name: "WebKit", - use: { browserName: "webkit" }, - }, + // Firefox seems to have bugs with opening context menus in the file tree. + // { + // name: "Firefox", + // use: { browserName: "firefox" }, + // }, + // Keeps failing with "Underlying ArrayBuffer has been detached from the view or out-of-bounds" + // Not sure what we can do about it...so skip for now. + // { + // name: "WebKit", + // use: { browserName: "webkit" }, + // }, ], } diff --git a/test/scripts/install.bats b/test/scripts/install.bats index 651236b7a8dc..67e64c9192a3 100644 --- a/test/scripts/install.bats +++ b/test/scripts/install.bats @@ -11,14 +11,14 @@ function should-use-deb() { DISTRO=$1 ARCH=$2 OS=linux run "$SCRIPT" --dry-run [ "$status" -eq 0 ] [ "${lines[1]}" = "Installing v$VERSION of the $2 deb package from GitHub." ] - [ "${lines[-5]}" = "deb package has been installed." ] + [ "${lines[-6]}" = "deb package has been installed." ] } function should-use-rpm() { DISTRO=$1 ARCH=$2 OS=linux run "$SCRIPT" --dry-run [ "$status" -eq 0 ] [ "${lines[1]}" = "Installing v$VERSION of the $2 rpm package from GitHub." ] - [ "${lines[-5]}" = "rpm package has been installed." ] + [ "${lines[-6]}" = "rpm package has been installed." ] } function should-fallback-npm() { @@ -26,22 +26,22 @@ function should-fallback-npm() { [ "$status" -eq 0 ] [ "${lines[1]}" = "No standalone releases for $2." ] [ "${lines[2]}" = "Falling back to installation from npm." ] - [ "${lines[3]}" = "Installing latest from npm." ] - [ "${lines[-5]}" = "npm package has been installed." ] + [ "${lines[3]}" = "Installing v$VERSION from npm." ] + [ "${lines[-6]}" = "npm package has been installed." ] } function should-use-npm() { YARN_PATH=true DISTRO=$1 ARCH=$2 OS=linux run "$SCRIPT" --dry-run [ "$status" -eq 0 ] - [ "${lines[1]}" = "Installing latest from npm." ] - [ "${lines[-5]}" = "npm package has been installed." ] + [ "${lines[1]}" = "Installing v$VERSION from npm." ] + [ "${lines[-6]}" = "npm package has been installed." ] } function should-use-aur() { DISTRO=$1 ARCH=$2 OS=linux run "$SCRIPT" --dry-run [ "$status" -eq 0 ] [ "${lines[1]}" = "Installing latest from the AUR." ] - [ "${lines[-5]}" = "AUR package has been installed." ] + [ "${lines[-6]}" = "AUR package has been installed." ] } function should-fallback-npm-brew() { @@ -51,22 +51,22 @@ function should-fallback-npm-brew() { [ "${lines[2]}" = "Falling back to standalone installation." ] [ "${lines[3]}" = "No standalone releases for $1." ] [ "${lines[4]}" = "Falling back to installation from npm." ] - [ "${lines[5]}" = "Installing latest from npm." ] - [ "${lines[-5]}" = "npm package has been installed." ] + [ "${lines[5]}" = "Installing v$VERSION from npm." ] + [ "${lines[-6]}" = "npm package has been installed." ] } function should-use-brew() { BREW_PATH=true OS=macos ARCH=$1 run "$SCRIPT" --dry-run [ "$status" -eq 0 ] [ "${lines[1]}" = "Installing latest from Homebrew." ] - [ "${lines[-3]}" = "Brew release has been installed." ] + [ "${lines[-4]}" = "Brew release has been installed." ] } function should-use-standalone() { DISTRO=$1 ARCH=$2 OS=$3 run "$SCRIPT" --method standalone --dry-run [ "$status" -eq 0 ] [ "${lines[1]}" = "Installing v$VERSION of the $2 release from GitHub." ] - [[ "${lines[-5]}" = "Standalone release has been installed"* ]] + [[ "${lines[-6]}" = "Standalone release has been installed"* ]] } @test "$SCRIPT_NAME: usage with --help" { @@ -132,16 +132,16 @@ function should-use-standalone() { # macOS use homebrew but falls back to standalone when brew is unavailable then # to npm for unsupported architectures. -@test "$SCRIPT_NAME: macos arm64 (no brew)" { - should-fallback-npm-brew "arm64" -} @test "$SCRIPT_NAME: macos amd64 (no brew)" { - BREW_PATH= OS=macos ARCH=amd64 run "$SCRIPT" --dry-run + should-fallback-npm-brew "amd64" +} +@test "$SCRIPT_NAME: macos arm64 (no brew)" { + BREW_PATH= OS=macos ARCH=arm64 run "$SCRIPT" --dry-run [ "$status" -eq 0 ] [ "${lines[1]}" = "Homebrew not installed." ] [ "${lines[2]}" = "Falling back to standalone installation." ] - [ "${lines[3]}" = "Installing v$VERSION of the amd64 release from GitHub." ] - [[ "${lines[-5]}" = "Standalone release has been installed"* ]] + [ "${lines[3]}" = "Installing v$VERSION of the arm64 release from GitHub." ] + [[ "${lines[-6]}" = "Standalone release has been installed"* ]] } @test "$SCRIPT_NAME: macos i386 (no brew)" { should-fallback-npm-brew "i386" diff --git a/test/scripts/steps-lib.bats b/test/scripts/steps-lib.bats new file mode 100644 index 000000000000..2071a062ea9f --- /dev/null +++ b/test/scripts/steps-lib.bats @@ -0,0 +1,46 @@ +#!/usr/bin/env bats + +SCRIPT_NAME="steps-lib.sh" +SCRIPT="$BATS_TEST_DIRNAME/../../ci/steps/$SCRIPT_NAME" + +source "$SCRIPT" + +@test "is_env_var_set should return 1 if env var is not set" { + run is_env_var_set "ASDF_TEST_SET" + [ "$status" = 1 ] +} + +@test "is_env_var_set should return 0 if env var is set" { + ASDF_TEST_SET="test" run is_env_var_set "ASDF_TEST_SET" + [ "$status" = 0 ] +} + +@test "directory_exists should 1 if directory doesn't exist" { + run directory_exists "/tmp/asdfasdfasdf" + [ "$status" = 1 ] +} + +@test "directory_exists should 0 if directory exists" { + run directory_exists "$(pwd)" + [ "$status" = 0 ] +} + +@test "file_exists should 1 if file doesn't exist" { + run file_exists "hello-asfd.sh" + [ "$status" = 1 ] +} + +@test "file_exists should 0 if file exists" { + run file_exists "$SCRIPT" + [ "$status" = 0 ] +} + +@test "is_executable should 1 if file isn't executable" { + run is_executable "hello-asfd.sh" + [ "$status" = 1 ] +} + +@test "is_executable should 0 if file is executable" { + run is_executable "$SCRIPT" + [ "$status" = 0 ] +} \ No newline at end of file diff --git a/test/unit/browser/pages/login.test.ts b/test/unit/browser/pages/login.test.ts deleted file mode 100644 index 92d4d1176c5e..000000000000 --- a/test/unit/browser/pages/login.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { JSDOM } from "jsdom" -import { LocationLike } from "../../common/util.test" - -describe("login", () => { - describe("there is an element with id 'base'", () => { - beforeEach(() => { - const dom = new JSDOM() - global.document = dom.window.document - - const location: LocationLike = { - pathname: "/healthz", - origin: "http://localhost:8080", - } - - global.location = location as Location - }) - afterEach(() => { - // Reset the global.document - global.document = undefined as any as Document - global.location = undefined as any as Location - }) - it("should set the value to options.base", () => { - // Mock getElementById - const spy = jest.spyOn(document, "getElementById") - // Create a fake element and set the attribute - const mockElement = document.createElement("input") - mockElement.setAttribute("id", "base") - const expected = { - base: "./hello-world", - csStaticBase: "./static/development/Users/jp/Dev/code-server", - logLevel: 2, - disableTelemetry: false, - disableUpdateCheck: false, - } - mockElement.setAttribute("data-settings", JSON.stringify(expected)) - document.body.appendChild(mockElement) - spy.mockImplementation(() => mockElement) - // Load file - require("../../../../src/browser/pages/login") - - const el: HTMLInputElement | null = document.querySelector("input#base") - expect(el?.value).toBe("/hello-world") - }) - }) - describe("there is not an element with id 'base'", () => { - let spy: jest.SpyInstance - - beforeAll(() => { - // This is important because we're manually requiring the file - // If you don't call this before all tests - // the module registry from other tests may cause side effects. - jest.resetModuleRegistry() - }) - - beforeEach(() => { - const dom = new JSDOM() - global.document = dom.window.document - spy = jest.spyOn(document, "getElementById") - - const location: LocationLike = { - pathname: "/healthz", - origin: "http://localhost:8080", - } - - global.location = location as Location - }) - - afterEach(() => { - spy.mockClear() - jest.resetModules() - // Reset the global.document - global.document = undefined as any as Document - global.location = undefined as any as Location - }) - - afterAll(() => { - jest.restoreAllMocks() - }) - - it("should do nothing", () => { - spy.mockImplementation(() => null) - // Load file - require("../../../../src/browser/pages/login") - - // It's called once by getOptions in the top of the file - // and then another to get the base element - expect(spy).toHaveBeenCalledTimes(2) - }) - }) -}) diff --git a/test/unit/browser/pages/vscode.test.ts b/test/unit/browser/pages/vscode.test.ts deleted file mode 100644 index 52c1d89c3618..000000000000 --- a/test/unit/browser/pages/vscode.test.ts +++ /dev/null @@ -1,400 +0,0 @@ -/** - * @jest-environment jsdom - */ -import fetchMock from "jest-fetch-mock" -import { JSDOM } from "jsdom" -import { - getNlsConfiguration, - nlsConfigElementId, - getConfigurationForLoader, - setBodyBackgroundToThemeBackgroundColor, - _createScriptURL, - main, - createBundlePath, -} from "../../../../src/browser/pages/vscode" - -describe("vscode", () => { - describe("getNlsConfiguration", () => { - let _document: Document - - beforeEach(() => { - // We use underscores to not confuse with global values - const { window: _window } = new JSDOM() - _document = _window.document - fetchMock.enableMocks() - }) - - afterEach(() => { - fetchMock.resetMocks() - }) - - it("should throw an error if no nlsConfigElement", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}` - - expect(() => { - getNlsConfiguration(_document, "") - }).toThrowError(errorMessage) - }) - it("should throw an error if no nlsConfig", () => { - const mockElement = _document.createElement("div") - mockElement.setAttribute("id", nlsConfigElementId) - _document.body.appendChild(mockElement) - - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.` - - expect(() => { - getNlsConfiguration(_document, "") - }).toThrowError(errorMessage) - - _document.body.removeChild(mockElement) - }) - it("should return the correct configuration", () => { - const mockElement = _document.createElement("div") - const dataSettings = { - first: "Jane", - last: "Doe", - } - - mockElement.setAttribute("id", nlsConfigElementId) - mockElement.setAttribute("data-settings", JSON.stringify(dataSettings)) - _document.body.appendChild(mockElement) - const actual = getNlsConfiguration(_document, "") - - expect(actual).toStrictEqual(dataSettings) - - _document.body.removeChild(mockElement) - }) - it("should return and have a loadBundle property if _resolvedLangaugePackCoreLocation", async () => { - const mockElement = _document.createElement("div") - const dataSettings = { - locale: "en", - availableLanguages: ["en", "de"], - _resolvedLanguagePackCoreLocation: "./", - } - - mockElement.setAttribute("id", nlsConfigElementId) - mockElement.setAttribute("data-settings", JSON.stringify(dataSettings)) - _document.body.appendChild(mockElement) - const nlsConfig = getNlsConfiguration(_document, "") - - expect(nlsConfig._resolvedLanguagePackCoreLocation).not.toBe(undefined) - expect(nlsConfig.loadBundle).not.toBe(undefined) - - const mockCallbackFn = jest.fn((_, bundle) => { - return bundle - }) - - fetchMock.mockOnce(JSON.stringify({ key: "hello world" })) - // Ensure that load bundle works as expected - // by mocking the fetch response and checking that the callback - // had the expected value - await nlsConfig.loadBundle("hello", "en", mockCallbackFn) - expect(mockCallbackFn).toHaveBeenCalledTimes(1) - expect(mockCallbackFn).toHaveBeenCalledWith(undefined, { key: "hello world" }) - - // Call it again to ensure it loads from the cache - // it should return the same value - await nlsConfig.loadBundle("hello", "en", mockCallbackFn) - expect(mockCallbackFn).toHaveBeenCalledTimes(2) - expect(mockCallbackFn).toHaveBeenCalledWith(undefined, { key: "hello world" }) - - fetchMock.mockReject(new Error("fake error message")) - const mockCallbackFn2 = jest.fn((error) => error) - // Call it for a different bundle and mock a failed fetch call - // to ensure we get the expected error - const error = await nlsConfig.loadBundle("goodbye", "es", mockCallbackFn2) - expect(error.message).toEqual("fake error message") - - // Clean up - _document.body.removeChild(mockElement) - }) - }) - describe("createBundlePath", () => { - it("should return the correct path", () => { - const _resolvedLangaugePackCoreLocation = "./languages" - const bundle = "/bundle.js" - const expected = "./languages/!bundle.js.nls.json" - const actual = createBundlePath(_resolvedLangaugePackCoreLocation, bundle) - expect(actual).toBe(expected) - }) - it("should return the correct path (even if _resolvedLangaugePackCoreLocation is undefined)", () => { - const _resolvedLangaugePackCoreLocation = undefined - const bundle = "/bundle.js" - const expected = "/!bundle.js.nls.json" - const actual = createBundlePath(_resolvedLangaugePackCoreLocation, bundle) - expect(actual).toBe(expected) - }) - }) - describe("setBodyBackgroundToThemeBackgroundColor", () => { - let _document: Document - let _localStorage: Storage - - beforeEach(() => { - // We need to set the url in the JSDOM constructor - // to prevent this error "SecurityError: localStorage is not available for opaque origins" - // See: https://github.com/jsdom/jsdom/issues/2304#issuecomment-622314949 - const { window: _window } = new JSDOM("", { url: "http://localhost" }) - _document = _window.document - _localStorage = _window.localStorage - }) - it("should return null", () => { - const test = { - colorMap: { - [`editor.background`]: "#ff3270", - }, - } - _localStorage.setItem("colorThemeData", JSON.stringify(test)) - - expect(setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)).toBeNull() - - _localStorage.removeItem("colorThemeData") - }) - it("should throw an error if it can't find colorThemeData in localStorage", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. Could not find colorThemeData in localStorage.` - - expect(() => { - setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) - }).toThrowError(errorMessage) - }) - it("should throw an error if there is an error parsing colorThemeData from localStorage", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. Could not parse colorThemeData from localStorage.` - - _localStorage.setItem( - "colorThemeData", - '{"id":"vs-dark max-SS-Cyberpunk-themes-cyberpunk-umbra-color-theme-json","label":"Activate UMBRA protocol","settingsId":"Activate "errorForeground":"#ff3270","foreground":"#ffffff","sideBarTitle.foreground":"#bbbbbb"},"watch\\":::false}', - ) - - expect(() => { - setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) - }).toThrowError(errorMessage) - - localStorage.removeItem("colorThemeData") - }) - it("should throw an error if there is no colorMap property", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. colorThemeData is missing colorMap.` - - const test = { - id: "hey-joe", - } - _localStorage.setItem("colorThemeData", JSON.stringify(test)) - - expect(() => { - setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) - }).toThrowError(errorMessage) - - _localStorage.removeItem("colorThemeData") - }) - it("should throw an error if there is no editor.background color", () => { - const errorMsgPrefix = "[vscode]" - const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. colorThemeData.colorMap["editor.background"] is undefined.` - - const test = { - id: "hey-joe", - colorMap: { - editor: "#fff", - }, - } - _localStorage.setItem("colorThemeData", JSON.stringify(test)) - - expect(() => { - setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) - }).toThrowError(errorMessage) - - _localStorage.removeItem("colorThemeData") - }) - it("should set the body background to the editor background color", () => { - const test = { - colorMap: { - [`editor.background`]: "#ff3270", - }, - } - _localStorage.setItem("colorThemeData", JSON.stringify(test)) - - setBodyBackgroundToThemeBackgroundColor(_document, _localStorage) - - // When the body.style.backgroundColor is set using hex - // it is converted to rgb - // which is why we use that in the assertion - expect(_document.body.style.backgroundColor).toBe("rgb(255, 50, 112)") - - _localStorage.removeItem("colorThemeData") - }) - }) - describe("getConfigurationForLoader", () => { - let _window: Window - - beforeEach(() => { - const { window: __window } = new JSDOM() - // @ts-expect-error the Window from JSDOM is not exactly the same as Window - // so we expect an error here - _window = __window - }) - it("should return a loader object (with undefined trustedTypesPolicy)", () => { - const options = { - base: ".", - csStaticBase: "/", - logLevel: 1, - } - const nlsConfig = { - first: "Jane", - last: "Doe", - locale: "en", - availableLanguages: {}, - } - const loader = getConfigurationForLoader({ - options, - _window, - nlsConfig: nlsConfig, - }) - - expect(loader).toStrictEqual({ - baseUrl: "http://localhost//vendor/modules/code-oss-dev/out", - paths: { - "iconv-lite-umd": "../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js", - jschardet: "../node_modules/jschardet/dist/jschardet.min.js", - "tas-client-umd": "../node_modules/tas-client-umd/lib/tas-client-umd.js", - "vscode-oniguruma": "../node_modules/vscode-oniguruma/release/main", - "vscode-textmate": "../node_modules/vscode-textmate/release/main", - xterm: "../node_modules/xterm/lib/xterm.js", - "xterm-addon-search": "../node_modules/xterm-addon-search/lib/xterm-addon-search.js", - "xterm-addon-unicode11": "../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js", - "xterm-addon-webgl": "../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js", - }, - recordStats: true, - - trustedTypesPolicy: undefined, - "vs/nls": { - availableLanguages: {}, - first: "Jane", - last: "Doe", - locale: "en", - }, - }) - }) - it("should return a loader object with trustedTypesPolicy", () => { - interface PolicyOptions { - createScriptUrl: (url: string) => string - } - - function mockCreatePolicy(policyName: string, options: PolicyOptions) { - return { - name: policyName, - ...options, - } - } - - const mockFn = jest.fn(mockCreatePolicy) - - // @ts-expect-error we are adding a custom property to window - _window.trustedTypes = { - createPolicy: mockFn, - } - - const options = { - base: "/", - csStaticBase: "/", - logLevel: 1, - } - const nlsConfig = { - first: "Jane", - last: "Doe", - locale: "en", - availableLanguages: {}, - } - const loader = getConfigurationForLoader({ - options, - _window, - nlsConfig: nlsConfig, - }) - - expect(loader.trustedTypesPolicy).not.toBe(undefined) - expect(loader.trustedTypesPolicy.name).toBe("amdLoader") - - // Check that we can actually create a script URL - // using the createScriptURL on the loader object - const scriptUrl = loader.trustedTypesPolicy.createScriptURL("http://localhost/foo.js") - expect(scriptUrl).toBe("http://localhost/foo.js") - }) - }) - describe("_createScriptURL", () => { - it("should return the correct url", () => { - const url = _createScriptURL("localhost/foo/bar.js", "localhost") - - expect(url).toBe("localhost/foo/bar.js") - }) - it("should throw if the value doesn't start with the origin", () => { - expect(() => { - _createScriptURL("localhost/foo/bar.js", "coder.com") - }).toThrow("Invalid script url: localhost/foo/bar.js") - }) - }) - describe("main", () => { - let _window: Window - let _document: Document - let _localStorage: Storage - - beforeEach(() => { - // We need to set the url in the JSDOM constructor - // to prevent this error "SecurityError: localStorage is not available for opaque origins" - // See: https://github.com/jsdom/jsdom/issues/2304#issuecomment-62231494 - const { window: __window } = new JSDOM("", { url: "http://localhost" }) - // @ts-expect-error the Window from JSDOM is not exactly the same as Window - // so we expect an error here - _window = __window - _document = __window.document - _localStorage = __window.localStorage - - const mockElement = _document.createElement("div") - const dataSettings = { - first: "Jane", - last: "Doe", - } - - mockElement.setAttribute("id", nlsConfigElementId) - mockElement.setAttribute("data-settings", JSON.stringify(dataSettings)) - _document.body.appendChild(mockElement) - - const test = { - colorMap: { - [`editor.background`]: "#ff3270", - }, - } - _localStorage.setItem("colorThemeData", JSON.stringify(test)) - }) - afterEach(() => { - _localStorage.removeItem("colorThemeData") - }) - it("should throw if document is missing", () => { - expect(() => { - main(undefined, _window, _localStorage) - }).toThrow("document is undefined.") - }) - it("should throw if window is missing", () => { - expect(() => { - main(_document, undefined, _localStorage) - }).toThrow("window is undefined.") - }) - it("should throw if localStorage is missing", () => { - expect(() => { - main(_document, _window, undefined) - }).toThrow("localStorage is undefined.") - }) - it("should add loader to self.require", () => { - main(_document, _window, _localStorage) - - expect(Object.prototype.hasOwnProperty.call(self, "require")).toBe(true) - }) - it("should not throw in browser context", () => { - // Assuming we call it in a normal browser context - // where everything is defined - expect(() => { - main(_document, _window, _localStorage) - }).not.toThrow() - }) - }) -}) diff --git a/test/unit/browser/register.test.ts b/test/unit/browser/register.test.ts deleted file mode 100644 index 1c213196602c..000000000000 --- a/test/unit/browser/register.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { JSDOM } from "jsdom" -import { registerServiceWorker } from "../../../src/browser/register" -import { createLoggerMock } from "../../utils/helpers" -import { LocationLike } from "../common/util.test" - -describe("register", () => { - describe("when navigator and serviceWorker are defined", () => { - const mockRegisterFn = jest.fn() - - beforeAll(() => { - const { window } = new JSDOM() - global.window = window as unknown as Window & typeof globalThis - global.document = window.document - global.navigator = window.navigator - global.location = window.location - - Object.defineProperty(global.navigator, "serviceWorker", { - value: { - register: mockRegisterFn, - }, - }) - }) - - const loggerModule = createLoggerMock() - beforeEach(() => { - jest.clearAllMocks() - jest.mock("@coder/logger", () => loggerModule) - }) - - afterEach(() => { - jest.resetModules() - }) - - afterAll(() => { - jest.restoreAllMocks() - - // We don't want these to stay around because it can affect other tests - global.window = undefined as unknown as Window & typeof globalThis - global.document = undefined as unknown as Document & typeof globalThis - global.navigator = undefined as unknown as Navigator & typeof globalThis - global.location = undefined as unknown as Location & typeof globalThis - }) - - it("test should have access to browser globals from beforeAll", () => { - expect(typeof global.window).not.toBeFalsy() - expect(typeof global.document).not.toBeFalsy() - expect(typeof global.navigator).not.toBeFalsy() - expect(typeof global.location).not.toBeFalsy() - }) - - it("should register a ServiceWorker", () => { - // Load service worker like you would in the browser - require("../../../src/browser/register") - expect(mockRegisterFn).toHaveBeenCalled() - expect(mockRegisterFn).toHaveBeenCalledTimes(1) - }) - - it("should log an error if something doesn't work", () => { - const message = "Can't find browser" - const error = new Error(message) - - mockRegisterFn.mockImplementation(() => { - throw error - }) - - // Load service worker like you would in the browser - require("../../../src/browser/register") - - expect(mockRegisterFn).toHaveBeenCalled() - expect(loggerModule.logger.error).toHaveBeenCalled() - expect(loggerModule.logger.error).toHaveBeenCalledTimes(1) - expect(loggerModule.logger.error).toHaveBeenCalledWith( - `[Service Worker] registration: ${error.message} ${error.stack}`, - ) - }) - }) - - describe("when navigator and serviceWorker are NOT defined", () => { - const loggerModule = createLoggerMock() - beforeEach(() => { - jest.clearAllMocks() - jest.mock("@coder/logger", () => loggerModule) - }) - - afterAll(() => { - jest.restoreAllMocks() - }) - - it("should log an error", () => { - // Load service worker like you would in the browser - require("../../../src/browser/register") - expect(loggerModule.logger.error).toHaveBeenCalled() - expect(loggerModule.logger.error).toHaveBeenCalledTimes(1) - expect(loggerModule.logger.error).toHaveBeenCalledWith("[Service Worker] navigator is undefined") - }) - }) - - describe("registerServiceWorker", () => { - let serviceWorkerPath: string - let serviceWorkerScope: string - const mockFn = jest.fn((path: string, options: { scope: string }) => { - serviceWorkerPath = path - serviceWorkerScope = options.scope - return undefined - }) - - beforeAll(() => { - const location: LocationLike = { - pathname: "", - origin: "http://localhost:8080", - } - const { window } = new JSDOM() - global.window = window as unknown as Window & typeof globalThis - global.document = window.document - global.navigator = window.navigator - global.location = location as Location - - Object.defineProperty(global.navigator, "serviceWorker", { - value: { - register: mockFn, - }, - }) - }) - - afterEach(() => { - mockFn.mockClear() - jest.resetModules() - }) - - afterAll(() => { - jest.restoreAllMocks() - - // We don't want these to stay around because it can affect other tests - global.window = undefined as unknown as Window & typeof globalThis - global.document = undefined as unknown as Document & typeof globalThis - global.navigator = undefined as unknown as Navigator & typeof globalThis - global.location = undefined as unknown as Location & typeof globalThis - }) - it("should register when options.base is undefined", async () => { - // Mock getElementById - const csStaticBasePath = "/static/development/Users/jp/Dev/code-server" - const spy = jest.spyOn(document, "getElementById") - // Create a fake element and set the attribute - const mockElement = document.createElement("div") - mockElement.id = "coder-options" - mockElement.setAttribute( - "data-settings", - `{"csStaticBase":"${csStaticBasePath}","logLevel":2,"disableUpdateCheck":false}`, - ) - // Return mockElement from the spy - // this way, when we call "getElementById" - // it returns the element - spy.mockImplementation(() => mockElement) - - await registerServiceWorker() - - expect(mockFn).toBeCalled() - expect(serviceWorkerPath).toMatch(`${csStaticBasePath}/out/browser/serviceWorker.js`) - expect(serviceWorkerScope).toMatch("/") - }) - it("should register when options.base is defined", async () => { - const csStaticBasePath = "/static/development/Users/jp/Dev/code-server" - const spy = jest.spyOn(document, "getElementById") - // Create a fake element and set the attribute - const mockElement = document.createElement("div") - mockElement.id = "coder-options" - mockElement.setAttribute( - "data-settings", - `{"base":"proxy/","csStaticBase":"${csStaticBasePath}","logLevel":2,"disableUpdateCheck":false}`, - ) - // Return mockElement from the spy - // this way, when we call "getElementById" - // it returns the element - spy.mockImplementation(() => mockElement) - - await registerServiceWorker() - - expect(mockFn).toBeCalled() - expect(serviceWorkerPath).toMatch(`/out/browser/serviceWorker.js`) - expect(serviceWorkerScope).toMatch("/") - }) - }) -}) diff --git a/test/unit/browser/serviceWorker.test.ts b/test/unit/browser/serviceWorker.test.ts deleted file mode 100644 index 8f41173b8173..000000000000 --- a/test/unit/browser/serviceWorker.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -interface MockEvent { - claim: jest.Mock - waitUntil?: jest.Mock -} - -interface Listener { - event: string - cb: (event?: MockEvent) => void -} - -describe("serviceWorker", () => { - let listeners: Listener[] = [] - let spy: jest.SpyInstance - let claimSpy: jest.Mock - let waitUntilSpy: jest.Mock - - function emit(event: string) { - listeners - .filter((listener) => listener.event === event) - .forEach((listener) => { - switch (event) { - case "activate": - listener.cb({ - claim: jest.fn(), - waitUntil: jest.fn(() => waitUntilSpy()), - }) - break - default: - listener.cb() - } - }) - } - - beforeEach(() => { - claimSpy = jest.fn() - spy = jest.spyOn(console, "log") - waitUntilSpy = jest.fn() - - Object.assign(global, { - self: global, - addEventListener: (event: string, cb: () => void) => { - listeners.push({ event, cb }) - }, - clients: { - claim: claimSpy.mockResolvedValue("claimed"), - }, - }) - }) - - afterEach(() => { - jest.restoreAllMocks() - jest.resetModules() - spy.mockClear() - claimSpy.mockClear() - - // Clear all the listeners - listeners = [] - }) - - it("should add 3 listeners: install, activate and fetch", () => { - require("../../../src/browser/serviceWorker.ts") - const listenerEventNames = listeners.map((listener) => listener.event) - - expect(listeners).toHaveLength(3) - expect(listenerEventNames).toContain("install") - expect(listenerEventNames).toContain("activate") - expect(listenerEventNames).toContain("fetch") - }) - - it("should call the proper callbacks for 'install'", async () => { - require("../../../src/browser/serviceWorker.ts") - emit("install") - expect(spy).toHaveBeenCalledWith("[Service Worker] installed") - expect(spy).toHaveBeenCalledTimes(1) - }) - - it("should do nothing when 'fetch' is called", async () => { - require("../../../src/browser/serviceWorker.ts") - emit("fetch") - expect(spy).not.toHaveBeenCalled() - }) - - it("should call the proper callbacks for 'activate'", async () => { - require("../../../src/browser/serviceWorker.ts") - emit("activate") - - // Activate serviceWorker - expect(spy).toHaveBeenCalledWith("[Service Worker] activated") - expect(waitUntilSpy).toHaveBeenCalled() - expect(claimSpy).toHaveBeenCalled() - }) -}) diff --git a/test/unit/common/emitter.test.ts b/test/unit/common/emitter.test.ts index 46a5dddd7efb..cec5fa611610 100644 --- a/test/unit/common/emitter.test.ts +++ b/test/unit/common/emitter.test.ts @@ -1,24 +1,16 @@ -// Note: we need to import logger from the root -// because this is the logger used in logError in ../src/common/util import { logger } from "@coder/logger" - import { Emitter } from "../../../src/common/emitter" +import { mockLogger } from "../../utils/helpers" describe("emitter", () => { - let spy: jest.SpyInstance - beforeEach(() => { - spy = jest.spyOn(logger, "error") + mockLogger() }) afterEach(() => { jest.clearAllMocks() }) - afterAll(() => { - jest.restoreAllMocks() - }) - it("should run the correct callbacks", async () => { const HELLO_WORLD = "HELLO_WORLD" const GOODBYE_WORLD = "GOODBYE_WORLD" @@ -85,8 +77,8 @@ describe("emitter", () => { await emitter.emit({ event: HELLO_WORLD, callback: mockCallback }) // Check that error was called - expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(message) + expect(logger.error).toHaveBeenCalled() + expect(logger.error).toHaveBeenCalledTimes(1) + expect(logger.error).toHaveBeenCalledWith(message) }) }) diff --git a/test/unit/common/http.test.ts b/test/unit/common/http.test.ts index fd49ae183b84..ba4981377498 100644 --- a/test/unit/common/http.test.ts +++ b/test/unit/common/http.test.ts @@ -19,7 +19,7 @@ describe("http", () => { const httpError = new HttpError(message, HttpCode.BadRequest) expect(httpError.message).toBe(message) - expect(httpError.status).toBe(400) + expect(httpError.statusCode).toBe(400) expect(httpError.details).toBeUndefined() }) it("should have details if provided", () => { diff --git a/test/unit/common/util.test.ts b/test/unit/common/util.test.ts index 85422aa84629..9e17b5734691 100644 --- a/test/unit/common/util.test.ts +++ b/test/unit/common/util.test.ts @@ -1,6 +1,7 @@ +import { logger } from "@coder/logger" import { JSDOM } from "jsdom" import * as util from "../../../src/common/util" -import { createLoggerMock } from "../../utils/helpers" +import { mockLogger } from "../../utils/helpers" const dom = new JSDOM() global.document = dom.window.document @@ -23,16 +24,6 @@ describe("util", () => { }) }) - describe("split", () => { - it("should split at a comma", () => { - expect(util.split("Hello,world", ",")).toStrictEqual(["Hello", "world"]) - }) - - it("shouldn't split if the delimiter doesn't exist", () => { - expect(util.split("Hello world", ",")).toStrictEqual(["Hello world", ""]) - }) - }) - describe("plural", () => { it("should add an s if count is greater than 1", () => { expect(util.plural(2, "dog")).toBe("dogs") @@ -56,184 +47,30 @@ describe("util", () => { }) }) - describe("trimSlashes", () => { - it("should remove leading slashes", () => { - expect(util.trimSlashes("/hello-world")).toBe("hello-world") - }) - - it("should remove trailing slashes", () => { - expect(util.trimSlashes("hello-world/")).toBe("hello-world") - }) - - it("should remove both leading and trailing slashes", () => { - expect(util.trimSlashes("/hello-world/")).toBe("hello-world") - }) - - it("should remove multiple leading and trailing slashes", () => { - expect(util.trimSlashes("///hello-world////")).toBe("hello-world") - }) - }) - - describe("resolveBase", () => { - beforeEach(() => { - const location: LocationLike = { - pathname: "/healthz", - origin: "http://localhost:8080", - } - - // Because resolveBase is not a pure function - // and relies on the global location to be set - // we set it before all the tests - // and tell TS that our location should be looked at - // as Location (even though it's missing some properties) - global.location = location as Location - }) - - it("should resolve a base", () => { - expect(util.resolveBase("localhost:8080")).toBe("/localhost:8080") - }) - - it("should resolve a base with a forward slash at the beginning", () => { - expect(util.resolveBase("/localhost:8080")).toBe("/localhost:8080") - }) - - it("should resolve a base with query params", () => { - expect(util.resolveBase("localhost:8080?folder=hello-world")).toBe("/localhost:8080") - }) - - it("should resolve a base with a path", () => { - expect(util.resolveBase("localhost:8080/hello/world")).toBe("/localhost:8080/hello/world") - }) - - it("should resolve a base to an empty string when not provided", () => { - expect(util.resolveBase()).toBe("") - }) - }) - - describe("getOptions", () => { - beforeEach(() => { - const location: LocationLike = { - pathname: "/healthz", - origin: "http://localhost:8080", - // search: "?environmentId=600e0187-0909d8a00cb0a394720d4dce", - } - - // Because resolveBase is not a pure function - // and relies on the global location to be set - // we set it before all the tests - // and tell TS that our location should be looked at - // as Location (even though it's missing some properties) - global.location = location as Location - }) - - afterEach(() => { - jest.restoreAllMocks() - }) - - it("should return options with base and cssStaticBase even if it doesn't exist", () => { - expect(util.getOptions()).toStrictEqual({ - base: "", - csStaticBase: "", - }) - }) - - it("should return options when they do exist", () => { - // Mock getElementById - const spy = jest.spyOn(document, "getElementById") - // Create a fake element and set the attribute - const mockElement = document.createElement("div") - mockElement.setAttribute( - "data-settings", - '{"base":".","csStaticBase":"./static/development/Users/jp/Dev/code-server","logLevel":2,"disableUpdateCheck":false}', - ) - // Return mockElement from the spy - // this way, when we call "getElementById" - // it returns the element - spy.mockImplementation(() => mockElement) - - expect(util.getOptions()).toStrictEqual({ - base: "", - csStaticBase: "/static/development/Users/jp/Dev/code-server", - disableUpdateCheck: false, - logLevel: 2, - }) - }) - - it("should include queryOpts", () => { - // Trying to understand how the implementation works - // 1. It grabs the search params from location.search (i.e. ?) - // 2. it then grabs the "options" param if it exists - // 3. then it creates a new options object - // spreads the original options - // then parses the queryOpts - location.search = '?options={"logLevel":2}' - expect(util.getOptions()).toStrictEqual({ - base: "", - csStaticBase: "", - logLevel: 2, - }) - }) - }) - - describe("arrayify", () => { - it("should return value it's already an array", () => { - expect(util.arrayify(["hello", "world"])).toStrictEqual(["hello", "world"]) - }) - - it("should wrap the value in an array if not an array", () => { - expect( - util.arrayify({ - name: "Coder", - version: "3.8", - }), - ).toStrictEqual([{ name: "Coder", version: "3.8" }]) - }) - - it("should return an empty array if the value is undefined", () => { - expect(util.arrayify(undefined)).toStrictEqual([]) - }) - }) - - describe("getFirstString", () => { - it("should return the string if passed a string", () => { - expect(util.getFirstString("Hello world!")).toBe("Hello world!") - }) - - it("should get the first string from an array", () => { - expect(util.getFirstString(["Hello", "World"])).toBe("Hello") - }) - - it("should return undefined if the value isn't an array or a string", () => { - expect(util.getFirstString({ name: "Coder" })).toBe(undefined) + describe("logError", () => { + beforeAll(() => { + mockLogger() }) - }) - describe("logError", () => { afterEach(() => { jest.clearAllMocks() }) - afterAll(() => { - jest.restoreAllMocks() - }) - - const loggerModule = createLoggerMock() - it("should log an error with the message and stack trace", () => { const message = "You don't have access to that folder." const error = new Error(message) - util.logError(loggerModule.logger, "ui", error) + util.logError(logger, "ui", error) - expect(loggerModule.logger.error).toHaveBeenCalled() - expect(loggerModule.logger.error).toHaveBeenCalledWith(`ui: ${error.message} ${error.stack}`) + expect(logger.error).toHaveBeenCalled() + expect(logger.error).toHaveBeenCalledWith(`ui: ${error.message} ${error.stack}`) }) it("should log an error, even if not an instance of error", () => { - util.logError(loggerModule.logger, "api", "oh no") + util.logError(logger, "api", "oh no") - expect(loggerModule.logger.error).toHaveBeenCalled() - expect(loggerModule.logger.error).toHaveBeenCalledWith("api: oh no") + expect(logger.error).toHaveBeenCalled() + expect(logger.error).toHaveBeenCalledWith("api: oh no") }) }) }) diff --git a/test/unit/helpers.test.ts b/test/unit/helpers.test.ts index e3de12e2a007..033449e9b160 100644 --- a/test/unit/helpers.test.ts +++ b/test/unit/helpers.test.ts @@ -1,12 +1,17 @@ import { promises as fs } from "fs" -import { getAvailablePort, tmpdir, useEnv } from "../../test/utils/helpers" +import { clean, getAvailablePort, getMaybeProxiedPathname, tmpdir, useEnv } from "../../test/utils/helpers" +import { REVERSE_PROXY_BASE_PATH } from "../utils/constants" /** * This file is for testing test helpers (not core code). */ describe("test helpers", () => { + const testName = "temp-dir" + beforeAll(async () => { + await clean(testName) + }) + it("should return a temp directory", async () => { - const testName = "temp-dir" const pathToTempDir = await tmpdir(testName) expect(pathToTempDir).toContain(testName) expect(fs.access(pathToTempDir)).resolves.toStrictEqual(undefined) @@ -52,3 +57,22 @@ describe("getAvailablePort", () => { expect(portOne).not.toEqual(portTwo) }) }) + +describe("getMaybeProxiedPathname", () => { + it("should return the route", () => { + const route = "/vscode" + const url = new URL(`http://localhost:3000${route}`) + const actual = getMaybeProxiedPathname(url) + expect(actual).toBe(route) + }) + it("should strip proxy if env var set", () => { + const envKey = "USE_PROXY" + const [setValue, resetValue] = useEnv(envKey) + setValue("1") + const route = "/vscode" + const url = new URL(`http://localhost:3000/8000/${REVERSE_PROXY_BASE_PATH}${route}`) + const actual = getMaybeProxiedPathname(url) + expect(actual).toBe(route) + resetValue() + }) +}) diff --git a/test/unit/node/__snapshots__/app.test.ts.snap b/test/unit/node/__snapshots__/app.test.ts.snap deleted file mode 100644 index 10a0e6c6d765..000000000000 --- a/test/unit/node/__snapshots__/app.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`handleServerError should log an error if resolved is true 1`] = `"Cannot read property 'handle' of undefined"`; diff --git a/test/unit/node/app.test.ts b/test/unit/node/app.test.ts index d3fccc3caeef..e56ed77bda20 100644 --- a/test/unit/node/app.test.ts +++ b/test/unit/node/app.test.ts @@ -1,27 +1,29 @@ import { logger } from "@coder/logger" -import { promises, rmdirSync } from "fs" +import { promises } from "fs" import * as http from "http" import * as https from "https" import * as path from "path" -import { createApp, ensureAddress, handleArgsSocketCatchError, handleServerError } from "../../../src/node/app" +import { createApp, ensureAddress, handleArgsSocketCatchError, listen } from "../../../src/node/app" import { OptionalString, setDefaults } from "../../../src/node/cli" import { generateCertificate } from "../../../src/node/util" -import { getAvailablePort, tmpdir } from "../../utils/helpers" +import { clean, mockLogger, getAvailablePort, tmpdir } from "../../utils/helpers" describe("createApp", () => { - let spy: jest.SpyInstance let unlinkSpy: jest.SpyInstance let port: number let tmpDirPath: string let tmpFilePath: string beforeAll(async () => { - tmpDirPath = await tmpdir("unlink-socket") + mockLogger() + + const testName = "app" + await clean(testName) + tmpDirPath = await tmpdir(testName) tmpFilePath = path.join(tmpDirPath, "unlink-socket-file") }) beforeEach(async () => { - spy = jest.spyOn(logger, "error") // NOTE:@jsjoeio // Be mindful when spying. // You can't spy on fs functions if you do import * as fs @@ -36,53 +38,39 @@ describe("createApp", () => { jest.clearAllMocks() }) - afterAll(() => { - jest.restoreAllMocks() - // Ensure directory was removed - rmdirSync(tmpDirPath, { recursive: true }) - }) - it("should return an Express app, a WebSockets Express app and an http server", async () => { const defaultArgs = await setDefaults({ port, - _: [], }) - const [app, wsApp, server] = await createApp(defaultArgs) + const app = await createApp(defaultArgs) // This doesn't check much, but it's a good sanity check // to ensure we actually get back values from createApp - expect(app).not.toBeNull() - expect(wsApp).not.toBeNull() - expect(server).toBeInstanceOf(http.Server) + expect(app.router).not.toBeNull() + expect(app.wsRouter).not.toBeNull() + expect(app.server).toBeInstanceOf(http.Server) // Cleanup - server.close() + app.dispose() }) it("should handle error events on the server", async () => { const defaultArgs = await setDefaults({ port, - _: [], }) - // This looks funky, but that's because createApp - // returns an array like [app, wsApp, server] - // We only need server which is at index 2 - // we do it this way so ESLint is happy that we're - // have no declared variables not being used const app = await createApp(defaultArgs) - const server = app[2] const testError = new Error("Test error") // We can easily test how the server handles errors // By emitting an error event // Ref: https://stackoverflow.com/a/33872506/3015595 - server.emit("error", testError) - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(`http server error: ${testError.message} ${testError.stack}`) + app.server.emit("error", testError) + expect(logger.error).toHaveBeenCalledTimes(1) + expect(logger.error).toHaveBeenCalledWith(`http server error: ${testError.message} ${testError.stack}`) // Cleanup - server.close() + app.dispose() }) it("should reject errors that happen before the server can listen", async () => { @@ -91,56 +79,44 @@ describe("createApp", () => { const port = 2 const defaultArgs = await setDefaults({ port, - _: [], }) async function masterBall() { const app = await createApp(defaultArgs) - const server = app[2] const testError = new Error("Test error") - server.emit("error", testError) + app.server.emit("error", testError) // Cleanup - server.close() + app.dispose() } - expect(() => masterBall()).rejects.toThrow(`listen EACCES: permission denied 127.0.0.1:${port}`) + expect(() => masterBall()).rejects.toThrow("listen EACCES: permission denied") }) it("should unlink a socket before listening on the socket", async () => { await promises.writeFile(tmpFilePath, "") const defaultArgs = await setDefaults({ - _: [], socket: tmpFilePath, }) const app = await createApp(defaultArgs) - const server = app[2] - expect(unlinkSpy).toHaveBeenCalledTimes(1) - server.close() + expect(unlinkSpy).toHaveBeenCalledWith(tmpFilePath) + app.dispose() }) - it("should catch errors thrown when unlinking a socket", async () => { - const tmpDir2 = await tmpdir("unlink-socket-error") - const tmpFile = path.join(tmpDir2, "unlink-socket-file") - // await promises.writeFile(tmpFile, "") - const socketPath = tmpFile + + it("should change the file mode of a socket", async () => { const defaultArgs = await setDefaults({ - _: [], - socket: socketPath, + socket: tmpFilePath, + "socket-mode": "777", }) const app = await createApp(defaultArgs) - const server = app[2] - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(`ENOENT: no such file or directory, unlink '${socketPath}'`) - - server.close() - // Ensure directory was removed - rmdirSync(tmpDir2, { recursive: true }) + expect((await promises.stat(tmpFilePath)).mode & 0o777).toBe(0o777) + app.dispose() }) it("should create an https server if args.cert exists", async () => { @@ -149,18 +125,16 @@ describe("createApp", () => { const defaultArgs = await setDefaults({ port, cert, - _: [], ["cert-key"]: testCertificate.certKey, }) const app = await createApp(defaultArgs) - const server = app[2] // This doesn't check much, but it's a good sanity check // to ensure we actually get an https.Server - expect(server).toBeInstanceOf(https.Server) + expect(app.server).toBeInstanceOf(https.Server) // Cleanup - server.close() + app.dispose() }) }) @@ -176,100 +150,62 @@ describe("ensureAddress", () => { }) it("should throw and error if no address", () => { - expect(() => ensureAddress(mockServer)).toThrow("server has no address") + expect(() => ensureAddress(mockServer, "http")).toThrow("Server has no address") }) - it("should return the address if it exists and not a string", async () => { - const port = await getAvailablePort() - mockServer.listen(port) - const address = ensureAddress(mockServer) - expect(address).toBe(`http://:::${port}`) + it("should return the address if it's a string", async () => { + mockServer.address = () => "/path/to/unix.sock" + const address = ensureAddress(mockServer, "http") + expect(address.toString()).toBe(`/path/to/unix.sock`) }) - it("should return the address if it exists", async () => { - mockServer.address = () => "http://localhost:8080" - const address = ensureAddress(mockServer) - expect(address).toBe(`http://localhost:8080`) + it("should construct URL with an IPv4 address", async () => { + mockServer.address = () => ({ address: "1.2.3.4", port: 5678, family: "IPv4" }) + const address = ensureAddress(mockServer, "http") + expect(address.toString()).toBe(`http://1.2.3.4:5678/`) }) -}) - -describe("handleServerError", () => { - let spy: jest.SpyInstance - - beforeEach(() => { - spy = jest.spyOn(logger, "error") - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - afterAll(() => { - jest.restoreAllMocks() - }) - - it("should call reject if resolved is false", async () => { - const resolved = false - const reject = jest.fn((err: Error) => undefined) - const error = new Error("handleServerError Error") - - handleServerError(resolved, error, reject) - - expect(reject).toHaveBeenCalledTimes(1) - expect(reject).toHaveBeenCalledWith(error) - }) - - it("should log an error if resolved is true", async () => { - const resolved = true - const reject = jest.fn((err: Error) => undefined) - const error = new Error("handleServerError Error") - - handleServerError(resolved, error, reject) - - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toThrowErrorMatchingSnapshot() + it("should construct URL with an IPv6 address", async () => { + mockServer.address = () => ({ address: "a:b:c:d::1234", port: 5678, family: "IPv6" }) + const address = ensureAddress(mockServer, "http") + expect(address.toString()).toBe(`http://[a:b:c:d::1234]:5678/`) }) }) describe("handleArgsSocketCatchError", () => { - let spy: jest.SpyInstance - - beforeEach(() => { - spy = jest.spyOn(logger, "error") + beforeAll(() => { + mockLogger() }) afterEach(() => { jest.clearAllMocks() }) - afterAll(() => { - jest.restoreAllMocks() - }) - it("should log an error if its not an NodeJS.ErrnoException", () => { - const error = new Error() - - handleArgsSocketCatchError(error) + const message = "other message" + const error = new Error(message) - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(error) + expect(() => { + handleArgsSocketCatchError(error) + }).toThrowError(error) }) it("should log an error if its not an NodeJS.ErrnoException (and the error has a message)", () => { const errorMessage = "handleArgsSocketCatchError Error" const error = new Error(errorMessage) - handleArgsSocketCatchError(error) - - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(errorMessage) + expect(() => { + handleArgsSocketCatchError(error) + }).toThrowError(error) }) - it("should not log an error if its a iNodeJS.ErrnoException", () => { - const error: NodeJS.ErrnoException = new Error() - error.code = "ENOENT" + it("should not log an error if its a NodeJS.ErrnoException", () => { + const code = "ENOENT" + const error: NodeJS.ErrnoException = new Error(code) + error.code = code handleArgsSocketCatchError(error) - expect(spy).toHaveBeenCalledTimes(0) + expect(() => { + handleArgsSocketCatchError(error) + }).not.toThrowError() }) it("should log an error if the code is not ENOENT (and the error has a message)", () => { @@ -278,19 +214,50 @@ describe("handleArgsSocketCatchError", () => { error.code = "EACCESS" error.message = errorMessage - handleArgsSocketCatchError(error) - - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(errorMessage) + expect(() => { + handleArgsSocketCatchError(error) + }).toThrowError(error) }) it("should log an error if the code is not ENOENT", () => { - const error: NodeJS.ErrnoException = new Error() - error.code = "EACCESS" + const code = "EACCESS" + const error: NodeJS.ErrnoException = new Error(code) + error.code = code - handleArgsSocketCatchError(error) + expect(() => { + handleArgsSocketCatchError(error) + }).toThrowError(error) + }) +}) - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(error) +describe("listen", () => { + let tmpDirPath: string + let mockServer: http.Server + + const testName = "listen" + + beforeEach(async () => { + await clean(testName) + mockLogger() + tmpDirPath = await tmpdir(testName) + mockServer = http.createServer() + }) + + afterEach(() => { + mockServer.close() + jest.clearAllMocks() + }) + + it("should throw an error if a directory is passed in instead of a file", async () => { + const errorMessage = "EISDIR: illegal operation on a directory, unlink" + const port = await getAvailablePort() + const mockArgs = { port, host: "0.0.0.0", socket: tmpDirPath } + + try { + await listen(mockServer, mockArgs) + } catch (error) { + expect(error).toBeInstanceOf(Error) + expect((error as any).message).toMatch(errorMessage) + } }) }) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index e92cb9758c85..85e16f38ca99 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -1,121 +1,167 @@ import { Level, logger } from "@coder/logger" import { promises as fs } from "fs" -import * as net from "net" -import * as os from "os" import * as path from "path" import { - Args, + UserProvidedArgs, bindAddrFromArgs, + defaultConfigFile, parse, + parseConfigFile, setDefaults, shouldOpenInExistingInstance, - shouldRunVsCodeCli, - splitOnFirstEquals, + toCodeArgs, + optionDescriptions, + options, + Options, + AuthType, + OptionalString, } from "../../../src/node/cli" -import { tmpdir } from "../../../src/node/constants" -import { paths } from "../../../src/node/util" -import { useEnv } from "../../utils/helpers" - -type Mutable = { - -readonly [P in keyof T]: T[P] +import { shouldSpawnCliProcess } from "../../../src/node/main" +import { generatePassword, paths } from "../../../src/node/util" +import { + EditorSessionManager, + EditorSessionManagerClient, + makeEditorSessionManagerServer, +} from "../../../src/node/vscodeSocket" +import { clean, useEnv, tmpdir, listenOn } from "../../utils/helpers" + +// The parser should not set any defaults so the caller can determine what +// values the user actually set. These are only set after explicitly calling +// `setDefaults`. +const defaults = { + auth: "password", + host: "localhost", + port: 8080, + "proxy-domain": [], + usingEnvPassword: false, + usingEnvHashedPassword: false, + "extensions-dir": path.join(paths.data, "extensions"), + "user-data-dir": paths.data, + "session-socket": path.join(paths.data, "code-server-ipc.sock"), + "app-name": "code-server", + _: [], } describe("parser", () => { beforeEach(() => { delete process.env.LOG_LEVEL delete process.env.PASSWORD + delete process.env.CS_DISABLE_FILE_DOWNLOADS + delete process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE + delete process.env.CODE_SERVER_RECONNECTION_GRACE_TIME + delete process.env.VSCODE_PROXY_URI + delete process.env.CS_DISABLE_PROXY console.log = jest.fn() }) - // The parser should not set any defaults so the caller can determine what - // values the user actually set. These are only set after explicitly calling - // `setDefaults`. - const defaults = { - auth: "password", - host: "localhost", - port: 8080, - "proxy-domain": [], - usingEnvPassword: false, - usingEnvHashedPassword: false, - "extensions-dir": path.join(paths.data, "extensions"), - "user-data-dir": paths.data, - } - - it("should parse nothing", () => { - expect(parse([])).toStrictEqual({ _: [] }) + it("should parse nothing", async () => { + expect(parse([])).toStrictEqual({}) }) - it("should parse all available options", () => { + it("should parse all available options", async () => { expect( - parse([ - "--enable", - "feature1", - "--enable", - "feature2", - "--bind-addr=192.169.0.1:8080", - "--auth", - "none", - "--extensions-dir", - "foo", - "--builtin-extensions-dir", - "foobar", - "--extra-extensions-dir", - "nozzle", - "1", - "--extra-builtin-extensions-dir", - "bazzle", - "--verbose", - "2", - "--log", - "error", - "--help", - "--open", - "--socket=mumble", - "3", - "--user-data-dir", - "bar", - "--cert=baz", - "--cert-key", - "qux", - "--version", - "--json", - "--port=8081", - "--host", - "0.0.0.0", - "4", - "--", - "-5", - "--6", - ]), + parse( + [ + ["--enable", "feature1"], + ["--enable", "feature2"], + + "--bind-addr=192.169.0.1:8080", + + ["--auth", "none"], + + ["--extensions-dir", "path/to/ext/dir"], + + ["--builtin-extensions-dir", "path/to/builtin/ext/dir"], + + "1", + "--verbose", + ["--app-name", "custom instance name"], + ["--welcome-text", "welcome to code"], + ["--i18n", "path/to/custom-strings.json"], + "2", + + ["--locale", "ja"], + + ["--log", "error"], + + "--help", + + "--open", + + "--socket=mumble", + + "--socket-mode=777", + + "3", + + ["--user-data-dir", "path/to/user/dir"], + + ["--cert=path/to/cert", "--cert-key", "path/to/cert/key"], + + "--version", + + "--json", + + "--port=8081", + + "--disable-file-downloads", + + "--disable-getting-started-override", + + "--disable-proxy", + + ["--abs-proxy-base-path", "/codeserver/app1"], + + "--skip-auth-preflight", + + ["--session-socket", "/tmp/override-code-server-ipc-socket"], + + ["--reconnection-grace-time", "86400"], + + ["--host", "0.0.0.0"], + "4", + "--", + "--5", + ].flat(), + ), ).toEqual({ - _: ["1", "2", "3", "4", "-5", "--6"], + _: ["1", "2", "3", "4", "--5"], auth: "none", - "builtin-extensions-dir": path.resolve("foobar"), - "cert-key": path.resolve("qux"), + "builtin-extensions-dir": path.resolve("path/to/builtin/ext/dir"), + "extensions-dir": path.resolve("path/to/ext/dir"), + "user-data-dir": path.resolve("path/to/user/dir"), + "cert-key": path.resolve("path/to/cert/key"), cert: { - value: path.resolve("baz"), + value: path.resolve("path/to/cert"), }, + "disable-file-downloads": true, + "disable-getting-started-override": true, + "disable-proxy": true, enable: ["feature1", "feature2"], - "extensions-dir": path.resolve("foo"), - "extra-builtin-extensions-dir": [path.resolve("bazzle")], - "extra-extensions-dir": [path.resolve("nozzle")], help: true, host: "0.0.0.0", json: true, + locale: "ja", log: "error", open: true, port: 8081, socket: path.resolve("mumble"), - "user-data-dir": path.resolve("bar"), + "socket-mode": "777", verbose: true, + "app-name": "custom instance name", + "welcome-text": "welcome to code", + i18n: path.resolve("path/to/custom-strings.json"), version: true, "bind-addr": "192.169.0.1:8080", + "session-socket": "/tmp/override-code-server-ipc-socket", + "reconnection-grace-time": "86400", + "abs-proxy-base-path": "/codeserver/app1", + "skip-auth-preflight": true, }) }) - it("should work with short options", () => { + it("should work with short options", async () => { expect(parse(["-vvv", "-v"])).toEqual({ - _: [], verbose: true, version: true, }) @@ -123,13 +169,12 @@ describe("parser", () => { it("should use log level env var", async () => { const args = parse([]) - expect(args).toEqual({ _: [] }) + expect(args).toEqual({}) process.env.LOG_LEVEL = "debug" const defaults = await setDefaults(args) expect(defaults).toStrictEqual({ ...defaults, - _: [], log: "debug", verbose: false, }) @@ -140,7 +185,6 @@ describe("parser", () => { const updated = await setDefaults(args) expect(updated).toStrictEqual({ ...updated, - _: [], log: "trace", verbose: true, }) @@ -151,7 +195,6 @@ describe("parser", () => { it("should prefer --log to env var and --verbose to --log", async () => { let args = parse(["--log", "info"]) expect(args).toEqual({ - _: [], log: "info", }) @@ -159,7 +202,6 @@ describe("parser", () => { const defaults = await setDefaults(args) expect(defaults).toEqual({ ...defaults, - _: [], log: "info", verbose: false, }) @@ -170,7 +212,6 @@ describe("parser", () => { const updated = await setDefaults(args) expect(updated).toEqual({ ...defaults, - _: [], log: "info", verbose: false, }) @@ -179,7 +220,6 @@ describe("parser", () => { args = parse(["--log", "info", "--verbose"]) expect(args).toEqual({ - _: [], log: "info", verbose: true, }) @@ -188,7 +228,6 @@ describe("parser", () => { const updatedAgain = await setDefaults(args) expect(updatedAgain).toEqual({ ...defaults, - _: [], log: "trace", verbose: true, }) @@ -196,12 +235,20 @@ describe("parser", () => { expect(logger.level).toEqual(Level.Trace) }) + it("should set valid log level env var", async () => { + process.env.LOG_LEVEL = "error" + const defaults = await setDefaults(parse([])) + expect(defaults).toEqual({ + ...defaults, + log: "error", + }) + }) + it("should ignore invalid log level env var", async () => { process.env.LOG_LEVEL = "bogus" const defaults = await setDefaults(parse([])) expect(defaults).toEqual({ ...defaults, - _: [], }) }) @@ -223,9 +270,8 @@ describe("parser", () => { expect(() => parse(["--foo"])).toThrowError(/Unknown option --foo/) }) - it("should not error if the value is optional", () => { + it("should not error if the value is optional", async () => { expect(parse(["--cert"])).toEqual({ - _: [], cert: { value: undefined, }, @@ -236,34 +282,35 @@ describe("parser", () => { expect(() => parse(["--socket", "--socket-path-value"])).toThrowError(/--socket requires a value/) // If you actually had a path like this you would do this instead: expect(parse(["--socket", "./--socket-path-value"])).toEqual({ - _: [], socket: path.resolve("--socket-path-value"), }) expect(() => parse(["--cert", "--socket-path-value"])).toThrowError(/Unknown option --socket-path-value/) }) - it("should allow positional arguments before options", () => { - expect(parse(["foo", "test", "--auth", "none"])).toEqual({ - _: ["foo", "test"], + it("should allow positional arguments before options", async () => { + expect(parse(["test", "--auth", "none"])).toEqual({ + _: ["test"], auth: "none", }) }) - it("should support repeatable flags", () => { + it("should support repeatable flags", async () => { + expect(() => parse(["--proxy-domain", ""])).toThrowError(/--proxy-domain requires a value/) expect(parse(["--proxy-domain", "*.coder.com"])).toEqual({ - _: [], "proxy-domain": ["*.coder.com"], }) expect(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"])).toEqual({ - _: [], "proxy-domain": ["*.coder.com", "test.com"], }) + // Commas are literal, at the moment. + expect(parse(["--proxy-domain", "*.coder.com,test.com"])).toEqual({ + "proxy-domain": ["*.coder.com,test.com"], + }) }) it("should enforce cert-key with cert value or otherwise generate one", async () => { const args = parse(["--cert"]) expect(args).toEqual({ - _: [], cert: { value: undefined, }, @@ -271,7 +318,6 @@ describe("parser", () => { expect(() => parse(["--cert", "test"])).toThrowError(/--cert-key is missing/) const defaultArgs = await setDefaults(args) expect(defaultArgs).toEqual({ - _: [], ...defaults, cert: { value: path.join(paths.data, "localhost.crt"), @@ -280,35 +326,14 @@ describe("parser", () => { }) }) - it("should override with --link", async () => { - const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" ")) - const defaultArgs = await setDefaults(args) - expect(defaultArgs).toEqual({ - _: [], - ...defaults, - auth: "none", - host: "localhost", - link: { - value: "test", - }, - port: 0, - cert: undefined, - "cert-key": path.resolve("test"), - socket: undefined, - }) - }) - it("should use env var password", async () => { process.env.PASSWORD = "test" const args = parse([]) - expect(args).toEqual({ - _: [], - }) + expect(args).toEqual({}) const defaultArgs = await setDefaults(args) expect(defaultArgs).toEqual({ ...defaults, - _: [], password: "test", usingEnvPassword: true, }) @@ -318,44 +343,175 @@ describe("parser", () => { process.env.HASHED_PASSWORD = "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY" // test const args = parse([]) - expect(args).toEqual({ - _: [], - }) + expect(args).toEqual({}) const defaultArgs = await setDefaults(args) expect(defaultArgs).toEqual({ ...defaults, - _: [], "hashed-password": "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY", usingEnvHashedPassword: true, }) }) + it("should parse i18n flag with file path", async () => { + // Test with file path (no validation at CLI parsing level) + const args = parse(["--i18n", "/path/to/custom-strings.json"]) + expect(args).toEqual({ + i18n: "/path/to/custom-strings.json", + }) + }) + + it("should parse i18n flag with relative file path", async () => { + // Test with relative file path + expect(() => parse(["--i18n", "./custom-strings.json"])).not.toThrow() + expect(() => parse(["--i18n", "strings.json"])).not.toThrow() + }) + + it("should support app-name and deprecated welcome-text flags", async () => { + const args = parse(["--app-name", "My App", "--welcome-text", "Welcome!"]) + expect(args).toEqual({ + "app-name": "My App", + "welcome-text": "Welcome!", + }) + }) + + it("should use env var github token", async () => { + process.env.GITHUB_TOKEN = "ga-foo" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "github-auth": "ga-foo", + }) + expect(process.env.GITHUB_TOKEN).toBe(undefined) + }) + + it("should use env var CS_DISABLE_FILE_DOWNLOADS", async () => { + process.env.CS_DISABLE_FILE_DOWNLOADS = "1" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-file-downloads": true, + }) + }) + + it("should use env var CS_DISABLE_FILE_DOWNLOADS set to true", async () => { + process.env.CS_DISABLE_FILE_DOWNLOADS = "true" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-file-downloads": true, + }) + }) + + it("should use env var CS_DISABLE_GETTING_STARTED_OVERRIDE", async () => { + process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE = "1" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-getting-started-override": true, + }) + }) + + it("should use env var CS_DISABLE_GETTING_STARTED_OVERRIDE set to true", async () => { + process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE = "true" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-getting-started-override": true, + }) + }) + + it("should use env var CS_DISABLE_PROXY", async () => { + process.env.CS_DISABLE_PROXY = "1" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-proxy": true, + }) + }) + + it("should use env var CS_DISABLE_PROXY set to true", async () => { + process.env.CS_DISABLE_PROXY = "true" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-proxy": true, + }) + }) + + it("should use env var CODE_SERVER_RECONNECTION_GRACE_TIME for reconnection grace time", async () => { + process.env.CODE_SERVER_RECONNECTION_GRACE_TIME = "86400" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "reconnection-grace-time": "86400", + }) + delete process.env.CODE_SERVER_RECONNECTION_GRACE_TIME + }) + + it("should error if password passed in", () => { + expect(() => parse(["--password", "supersecret123"])).toThrowError( + "--password can only be set in the config file or passed in via $PASSWORD", + ) + }) + + it("should error if hashed-password passed in", () => { + expect(() => parse(["--hashed-password", "fdas423fs8a"])).toThrowError( + "--hashed-password can only be set in the config file or passed in via $HASHED_PASSWORD", + ) + }) + + it("should error if github-auth passed in", () => { + expect(() => parse(["--github-auth", "fdas423fs8a"])).toThrowError( + "--github-auth can only be set in the config file or passed in via $GITHUB_TOKEN", + ) + }) + it("should filter proxy domains", async () => { const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"]) expect(args).toEqual({ - _: [], "proxy-domain": ["*.coder.com", "coder.com", "coder.org"], }) const defaultArgs = await setDefaults(args) expect(defaultArgs).toEqual({ ...defaults, - _: [], - "proxy-domain": ["coder.com", "coder.org"], + "proxy-domain": ["{{port}}.coder.com", "{{port}}.coder.org"], }) }) it("should allow '=,$/' in strings", async () => { const args = parse([ - "--enable-proposed-api", + "--disable-update-check", "$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy", ]) expect(args).toEqual({ - _: [], - "enable-proposed-api": [ - "$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy", - ], + "disable-update-check": true, + _: ["$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy"], }) }) it("should parse options with double-dash and multiple equal signs ", async () => { @@ -368,150 +524,280 @@ describe("parser", () => { }, ) expect(args).toEqual({ - _: [], "hashed-password": "$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy", }) }) + it("should throw an error for invalid config values", async () => { + const fakePath = "/fake-config-path" + const expectedErrMsg = `error reading ${fakePath}: ` + + expect(() => + parse(["--foo"], { + configFile: fakePath, + }), + ).toThrowError(expectedErrMsg) + }) + it("should fail to parse invalid config", () => { + expect(() => parseConfigFile("test", "/fake-config-path")).toThrowError("invalid config: test") + }) + it("should parse repeatable options", () => { + const configContents = ` + install-extension: + - extension.number1 + - extension.number2 + ` + expect(parseConfigFile(configContents, "/fake-config-path")).toEqual({ + config: "/fake-config-path", + "install-extension": ["extension.number1", "extension.number2"], + }) + }) + it("should ignore optional strings set to false", async () => { + expect(parse(["--cert=false"])).toEqual({}) + }) + it("should use last flag", async () => { + expect(parse(["--port", "8081", "--port", "8082"])).toEqual({ + port: 8082, + }) + }) + + it("should not set proxy uri", async () => { + await setDefaults(parse([])) + expect(process.env.VSCODE_PROXY_URI).toBeUndefined() + }) + + it("should set proxy uri", async () => { + await setDefaults(parse(["--proxy-domain", "coder.org"])) + expect(process.env.VSCODE_PROXY_URI).toEqual("//{{port}}.coder.org") + }) + + it("should set proxy uri to first domain", async () => { + await setDefaults( + parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"]), + ) + expect(process.env.VSCODE_PROXY_URI).toEqual("//{{port}}.coder.com") + }) + + it("should not override existing proxy uri", async () => { + process.env.VSCODE_PROXY_URI = "foo" + await setDefaults( + parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"]), + ) + expect(process.env.VSCODE_PROXY_URI).toEqual("foo") + }) }) describe("cli", () => { - let args: Mutable = { _: [] } - const testDir = path.join(tmpdir, "tests/cli") - const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc") + const testName = "cli" + let tmpDirPath: string beforeAll(async () => { - await fs.rmdir(testDir, { recursive: true }) - await fs.mkdir(testDir, { recursive: true }) + await clean(testName) }) beforeEach(async () => { delete process.env.VSCODE_IPC_HOOK_CLI - args = { _: [] } - await fs.rmdir(vscodeIpcPath, { recursive: true }) + tmpDirPath = await tmpdir(testName) }) it("should use existing if inside code-server", async () => { process.env.VSCODE_IPC_HOOK_CLI = "test" - expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test") + const args: UserProvidedArgs = {} + expect(await shouldOpenInExistingInstance(args, "")).toStrictEqual("test") args.port = 8081 - args._.push("./file") - expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test") + args._ = ["./file"] + expect(await shouldOpenInExistingInstance(args, "")).toStrictEqual("test") }) it("should use existing if --reuse-window is set", async () => { + const sessionSocket = path.join(tmpDirPath, "session-socket") + const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager()) + + const args: UserProvidedArgs = {} args["reuse-window"] = true - await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined) + await expect(shouldOpenInExistingInstance(args, sessionSocket)).rejects.toThrow() + + const socketPath = path.join(tmpDirPath, "socket") + const client = new EditorSessionManagerClient(sessionSocket) + await client.addSession({ + entry: { + workspace: { + id: "aaa", + folders: [ + { + uri: { + path: "/aaa", + }, + }, + ], + }, + socketPath, + }, + }) + const vscodeSockets = listenOn(socketPath) - await fs.writeFile(vscodeIpcPath, "test") - await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual("test") + await expect(shouldOpenInExistingInstance(args, sessionSocket)).resolves.toStrictEqual(socketPath) args.port = 8081 - await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual("test") + await expect(shouldOpenInExistingInstance(args, sessionSocket)).resolves.toStrictEqual(socketPath) + + server.close() + vscodeSockets.close() }) it("should use existing if --new-window is set", async () => { + const sessionSocket = path.join(tmpDirPath, "session-socket") + const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager()) + + const args: UserProvidedArgs = {} args["new-window"] = true - expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) + await expect(shouldOpenInExistingInstance(args, sessionSocket)).rejects.toThrow() + + const socketPath = path.join(tmpDirPath, "socket") + const client = new EditorSessionManagerClient(sessionSocket) + await client.addSession({ + entry: { + workspace: { + id: "aaa", + folders: [ + { + uri: { + path: "/aaa", + }, + }, + ], + }, + socketPath, + }, + }) + const vscodeSockets = listenOn(socketPath) - await fs.writeFile(vscodeIpcPath, "test") - expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test") + expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(socketPath) args.port = 8081 - expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test") + expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(socketPath) + + server.close() + vscodeSockets.close() }) it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => { - expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) - - args._.push("./file") - expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) - - const socketPath = path.join(testDir, "socket") - await fs.writeFile(vscodeIpcPath, socketPath) - expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) - - await new Promise((resolve) => { - const server = net.createServer(() => { - // Close after getting the first connection. - server.close() - }) - server.once("listening", () => resolve(server)) - server.listen(socketPath) + const sessionSocket = path.join(tmpDirPath, "session-socket") + const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager()) + + const args: UserProvidedArgs = {} + expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(undefined) + + args._ = ["./file"] + expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(undefined) + + const client = new EditorSessionManagerClient(sessionSocket) + const socketPath = path.join(tmpDirPath, "socket") + await client.addSession({ + entry: { + workspace: { + id: "aaa", + folders: [ + { + uri: { + path: "/aaa", + }, + }, + ], + }, + socketPath, + }, }) + const vscodeSockets = listenOn(socketPath) - expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath) + expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(socketPath) args.port = 8081 - expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) + expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(undefined) + + server.close() + vscodeSockets.close() }) -}) -describe("splitOnFirstEquals", () => { - it("should split on the first equals", () => { - const testStr = "enabled-proposed-api=test=value" - const actual = splitOnFirstEquals(testStr) - const expected = ["enabled-proposed-api", "test=value"] - expect(actual).toEqual(expect.arrayContaining(expected)) - }) - it("should split on first equals regardless of multiple equals signs", () => { - const testStr = - "hashed-password=$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY" - const actual = splitOnFirstEquals(testStr) - const expected = [ - "hashed-password", - "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY", - ] - expect(actual).toEqual(expect.arrayContaining(expected)) - }) - it("should always return the first element before an equals", () => { - const testStr = "auth=" - const actual = splitOnFirstEquals(testStr) - const expected = ["auth"] - expect(actual).toEqual(expect.arrayContaining(expected)) + it("should prefer matching sessions for only the first path", async () => { + const sessionSocket = path.join(tmpDirPath, "session-socket") + const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager()) + const client = new EditorSessionManagerClient(sessionSocket) + await client.addSession({ + entry: { + workspace: { + id: "aaa", + folders: [ + { + uri: { + path: "/aaa", + }, + }, + ], + }, + socketPath: `${tmpDirPath}/vscode-ipc-aaa.sock`, + }, + }) + await client.addSession({ + entry: { + workspace: { + id: "bbb", + folders: [ + { + uri: { + path: "/bbb", + }, + }, + ], + }, + socketPath: `${tmpDirPath}/vscode-ipc-bbb.sock`, + }, + }) + listenOn(`${tmpDirPath}/vscode-ipc-aaa.sock`, `${tmpDirPath}/vscode-ipc-bbb.sock`) + + const args: UserProvidedArgs = {} + args._ = ["/aaa/file", "/bbb/file"] + expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(`${tmpDirPath}/vscode-ipc-aaa.sock`) + + server.close() }) }) -describe("shouldRunVsCodeCli", () => { - it("should return false if no 'extension' related args passed in", () => { - const args = { - _: [], - } - const actual = shouldRunVsCodeCli(args) +describe("shouldSpawnCliProcess", () => { + it("should return false if no 'extension' related args passed in", async () => { + const args = {} + const actual = await shouldSpawnCliProcess(args) const expected = false expect(actual).toBe(expected) }) - it("should return true if 'list-extensions' passed in", () => { + it("should return true if 'list-extensions' passed in", async () => { const args = { - _: [], ["list-extensions"]: true, } - const actual = shouldRunVsCodeCli(args) + const actual = await shouldSpawnCliProcess(args) const expected = true expect(actual).toBe(expected) }) - it("should return true if 'install-extension' passed in", () => { + it("should return true if 'install-extension' passed in", async () => { const args = { - _: [], ["install-extension"]: ["hello.world"], } - const actual = shouldRunVsCodeCli(args) + const actual = await shouldSpawnCliProcess(args) const expected = true expect(actual).toBe(expected) }) - it("should return true if 'uninstall-extension' passed in", () => { - const args = { - _: [], + it("should return true if 'uninstall-extension' passed in", async () => { + const args: UserProvidedArgs = { ["uninstall-extension"]: ["hello.world"], } - const actual = shouldRunVsCodeCli(args) + const actual = await shouldSpawnCliProcess(args) const expected = true expect(actual).toBe(expected) @@ -520,9 +806,7 @@ describe("shouldRunVsCodeCli", () => { describe("bindAddrFromArgs", () => { it("should return the bind address", () => { - const args = { - _: [], - } + const args: UserProvidedArgs = {} const addr = { host: "localhost", @@ -536,8 +820,7 @@ describe("bindAddrFromArgs", () => { }) it("should use the bind-address if set in args", () => { - const args = { - _: [], + const args: UserProvidedArgs = { ["bind-addr"]: "localhost:3000", } @@ -556,8 +839,7 @@ describe("bindAddrFromArgs", () => { }) it("should use the host if set in args", () => { - const args = { - _: [], + const args: UserProvidedArgs = { ["host"]: "coder", } @@ -575,13 +857,55 @@ describe("bindAddrFromArgs", () => { expect(actual).toStrictEqual(expected) }) + it("should use process.env.CODE_SERVER_HOST if set", () => { + const [setValue, resetValue] = useEnv("CODE_SERVER_HOST") + setValue("coder") + + const args: UserProvidedArgs = {} + + const addr = { + host: "localhost", + port: 8080, + } + + const actual = bindAddrFromArgs(addr, args) + const expected = { + host: "coder", + port: 8080, + } + + expect(actual).toStrictEqual(expected) + resetValue() + }) + + it("should use the args.host over process.env.CODE_SERVER_HOST if both set", () => { + const [setValue, resetValue] = useEnv("CODE_SERVER_HOST") + setValue("coder") + + const args: UserProvidedArgs = { + host: "123.123.123.123", + } + + const addr = { + host: "localhost", + port: 8080, + } + + const actual = bindAddrFromArgs(addr, args) + const expected = { + host: "123.123.123.123", + port: 8080, + } + + expect(actual).toStrictEqual(expected) + resetValue() + }) + it("should use process.env.PORT if set", () => { const [setValue, resetValue] = useEnv("PORT") setValue("8000") - const args = { - _: [], - } + const args: UserProvidedArgs = {} const addr = { host: "localhost", @@ -599,8 +923,7 @@ describe("bindAddrFromArgs", () => { }) it("should set port if in args", () => { - const args = { - _: [], + const args: UserProvidedArgs = { port: 3000, } @@ -622,8 +945,7 @@ describe("bindAddrFromArgs", () => { const [setValue, resetValue] = useEnv("PORT") setValue("8000") - const args = { - _: [], + const args: UserProvidedArgs = { port: 3000, } @@ -642,3 +964,132 @@ describe("bindAddrFromArgs", () => { resetValue() }) }) + +describe("defaultConfigFile", () => { + it("should return the default config file as a string", async () => { + const password = await generatePassword() + const actual = defaultConfigFile(password) + + expect(actual).toMatch(`bind-addr: 127.0.0.1:8080 +auth: password +password: ${password} +cert: false`) + }) +}) + +describe("toCodeArgs", () => { + const vscodeDefaults = { + ...defaults, + help: false, + port: "8080", + version: false, + log: undefined, + } + + const testName = "vscode-args" + beforeAll(async () => { + // Clean up temporary directories from the previous run. + await clean(testName) + }) + + it("should convert empty args", async () => { + expect(await toCodeArgs(await setDefaults(parse([])))).toStrictEqual({ + ...vscodeDefaults, + }) + }) + + it("should ignore regular file", async () => { + const file = path.join(await tmpdir(testName), "file") + await fs.writeFile(file, "foobar") + expect(await toCodeArgs(await setDefaults(parse([file])))).toStrictEqual({ + ...vscodeDefaults, + _: [file], + }) + }) +}) + +describe("optionDescriptions", () => { + it("should return the descriptions of all the available options", () => { + const expectedOptionDescriptions = Object.entries(options) + .flat() + .filter((item: any) => { + if (item.description) { + return item.description + } + }) + .map((item: any) => item.description) + const actualOptionDescriptions = optionDescriptions() + // We need both the expected and the actual + // Both of these are string[] + // We then loop through the expectedOptionDescriptions + // and check that this expectedDescription exists in the + // actualOptionDescriptions + + // To do that we need to loop through actualOptionDescriptions + // and make sure we have a substring match + expectedOptionDescriptions.forEach((expectedDescription) => { + const exists = actualOptionDescriptions.find((desc) => { + if ( + desc.replace(/\n/g, " ").replace(/ /g, "").includes(expectedDescription.replace(/\n/g, " ").replace(/ /g, "")) + ) { + return true + } + return false + }) + expect(exists).toBeTruthy() + }) + }) + it("should visually align multiple options", () => { + const opts: Partial>> = { + "cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." }, + "cert-host": { + type: "string", + description: "Hostname to use when generating a self signed certificate.", + }, + "disable-update-check": { + type: "boolean", + description: + "Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" + + "then notifies you once every week that a new release is available.", + }, + } + expect(optionDescriptions(opts)).toStrictEqual([ + " --cert-key Path to certificate key when using non-generated cert.", + " --cert-host Hostname to use when generating a self signed certificate.", + ` --disable-update-check Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and + then notifies you once every week that a new release is available.`, + ]) + }) + it("should add all valid options for enumerated types", () => { + const opts: Partial>> = { + auth: { type: AuthType, description: "The type of authentication to use." }, + } + expect(optionDescriptions(opts)).toStrictEqual([" --auth The type of authentication to use. [password, none]"]) + }) + + it("should show if an option is deprecated", () => { + const opts: Partial>> = { + cert: { + type: OptionalString, + description: "foo", + deprecated: true, + }, + } + expect(optionDescriptions(opts)).toStrictEqual([" --cert (deprecated) foo"]) + }) + + it("should show newlines in description", () => { + const opts: Partial>> = { + "install-extension": { + type: "string[]", + description: + "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" + + "To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", + }, + } + expect(optionDescriptions(opts)).toStrictEqual([ + ` --install-extension Install or update a VS Code extension by id or vsix. The identifier of an extension is \`\${publisher}.\${name}\`. + To install a specific version provide \`@\${version}\`. For example: 'vscode.csharp@1.2.3'.`, + ]) + }) +}) diff --git a/test/unit/node/constants.test.ts b/test/unit/node/constants.test.ts index 5b9a8d87a712..2ed963f87ba3 100644 --- a/test/unit/node/constants.test.ts +++ b/test/unit/node/constants.test.ts @@ -1,26 +1,36 @@ -import { createLoggerMock } from "../../utils/helpers" +import { logger } from "@coder/logger" +import path from "path" +import * as semver from "semver" +import { mockLogger } from "../../utils/helpers" describe("constants", () => { let constants: typeof import("../../../src/node/constants") describe("with package.json defined", () => { - const loggerModule = createLoggerMock() const mockPackageJson = { name: "mock-code-server", description: "Run VS Code on a remote server.", - repository: "https://github.com/cdr/code-server", + repository: "https://github.com/coder/code-server", version: "1.0.0", commit: "f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b", } + const mockCodePackageJson = { + name: "mock-vscode", + version: "1.2.3", + } + beforeAll(() => { - jest.mock("@coder/logger", () => loggerModule) - jest.mock("../../../package.json", () => mockPackageJson, { virtual: true }) + jest.clearAllMocks() + mockLogger() + jest.mock(path.resolve(__dirname, "../../../package.json"), () => mockPackageJson, { virtual: true }) + jest.mock(path.resolve(__dirname, "../../../lib/vscode/package.json"), () => mockCodePackageJson, { + virtual: true, + }) constants = require("../../../src/node/constants") }) afterAll(() => { - jest.clearAllMocks() jest.resetModules() }) @@ -30,6 +40,38 @@ describe("constants", () => { it("should return the package.json version", () => { expect(constants.version).toBe(mockPackageJson.version) + + // Ensure the version is parseable as semver and equal + const actual = semver.parse(constants.version) + const expected = semver.parse(mockPackageJson.version) + expect(actual).toBeTruthy() + expect(actual).toStrictEqual(expected) + }) + + it("should include embedded Code version information", () => { + expect(constants.codeVersion).toBe(mockCodePackageJson.version) + + // Ensure the version is parseable as semver and equal + const actual = semver.parse(constants.codeVersion) + const expected = semver.parse(mockCodePackageJson.version) + expect(actual).toBeTruthy() + expect(actual).toStrictEqual(expected) + }) + + it("should return a human-readable version string", () => { + expect(constants.getVersionString()).toStrictEqual( + `${mockPackageJson.version} ${mockPackageJson.commit} with Code ${mockCodePackageJson.version}`, + ) + }) + + it("should return a machine-readable version string", () => { + expect(constants.getVersionJsonString()).toStrictEqual( + JSON.stringify({ + codeServer: mockPackageJson.version, + commit: mockPackageJson.commit, + vscode: mockCodePackageJson.version, + }), + ) }) describe("getPackageJson", () => { @@ -38,8 +80,8 @@ describe("constants", () => { constants.getPackageJson("./package.json") - expect(loggerModule.logger.warn).toHaveBeenCalled() - expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage) + expect(logger.warn).toHaveBeenCalled() + expect(logger.warn).toHaveBeenCalledWith(expectedErrorMessage) }) it("should find the package.json", () => { @@ -47,6 +89,9 @@ describe("constants", () => { // so to get the root package.json we need to use ../../ const packageJson = constants.getPackageJson("../../package.json") expect(packageJson).toStrictEqual(mockPackageJson) + + const codePackageJson = constants.getPackageJson("../../lib/vscode/package.json") + expect(codePackageJson).toStrictEqual(mockCodePackageJson) }) }) }) @@ -55,22 +100,44 @@ describe("constants", () => { const mockPackageJson = { name: "mock-code-server", } + const mockCodePackageJson = { + name: "mock-vscode", + } beforeAll(() => { - jest.mock("../../../package.json", () => mockPackageJson, { virtual: true }) + jest.clearAllMocks() + jest.mock(path.resolve(__dirname, "../../../package.json"), () => mockPackageJson, { virtual: true }) + jest.mock(path.resolve(__dirname, "../../../lib/vscode/package.json"), () => mockCodePackageJson, { + virtual: true, + }) constants = require("../../../src/node/constants") }) afterAll(() => { - jest.clearAllMocks() jest.resetModules() }) it("version should return 'development'", () => { expect(constants.version).toBe("development") }) + it("commit should return 'development'", () => { expect(constants.commit).toBe("development") }) + + it("should return a human-readable version string", () => { + // this string is not super useful + expect(constants.getVersionString()).toStrictEqual("development development with Code development") + }) + + it("should return a machine-readable version string", () => { + expect(constants.getVersionJsonString()).toStrictEqual( + JSON.stringify({ + codeServer: "development", + commit: "development", + vscode: "development", + }), + ) + }) }) }) diff --git a/test/unit/node/heart.test.ts b/test/unit/node/heart.test.ts new file mode 100644 index 000000000000..85b1945e3760 --- /dev/null +++ b/test/unit/node/heart.test.ts @@ -0,0 +1,188 @@ +import { logger } from "@coder/logger" +import { readFile, writeFile, stat, utimes } from "fs/promises" +import { setImmediate } from "timers" +import { Heart } from "../../../src/node/heart" +import { clean, mockLogger, tmpdir } from "../../utils/helpers" + +describe("Heart", () => { + const testName = "heartTests" + let testDir = "" + let heart: Heart + + beforeAll(async () => { + mockLogger() + await clean(testName) + testDir = await tmpdir(testName) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + beforeEach(() => { + heart = new Heart(`${testDir}/shutdown.txt`, jest.fn().mockResolvedValue(true)) + }) + + afterEach(() => { + jest.resetAllMocks() + jest.useRealTimers() + if (heart) { + heart.dispose() + } + }) + + it("should write to a file when given a valid file path", async () => { + // Set up heartbeat file with contents + const text = "test" + const pathToFile = `${testDir}/file.txt` + await writeFile(pathToFile, text) + const fileContents = await readFile(pathToFile, { encoding: "utf8" }) + // Explicitly set the modified time to 0 so that we can check + // that the file was indeed modified after calling heart.beat(). + // This works around any potential race conditions. + // Docs: https://nodejs.org/api/fs.html#fspromisesutimespath-atime-mtime + await utimes(pathToFile, 0, 0) + + expect(fileContents).toBe(text) + + heart = new Heart(pathToFile, jest.fn().mockResolvedValue(true)) + await heart.beat() + // Check that the heart wrote to the heartbeatFilePath and overwrote our text + const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" }) + expect(fileContentsAfterBeat).not.toBe(text) + // Make sure the modified timestamp was updated. + const fileStatusAfterEdit = await stat(pathToFile) + expect(fileStatusAfterEdit.mtimeMs).toBeGreaterThan(0) + }) + + it("should log a warning when given an invalid file path", async () => { + heart = new Heart(`fakeDir/fake.txt`, jest.fn().mockResolvedValue(false)) + await heart.beat() + expect(logger.warn).toHaveBeenCalled() + }) + + it("should be active after calling beat", async () => { + await heart.beat() + + const isAlive = heart.alive() + expect(isAlive).toBe(true) + }) + + it("should not be active after dispose is called", () => { + heart.dispose() + + const isAlive = heart.alive() + expect(isAlive).toBe(false) + }) + + it("should beat twice without warnings", async () => { + heart = new Heart(`${testDir}/hello.txt`, jest.fn().mockResolvedValue(true)) + await heart.beat() + jest.runAllTimers() + expect(logger.warn).not.toHaveBeenCalled() + }) +}) + +describe("heartbeatTimer", () => { + const testName = "heartbeatTimer" + let testDir = "" + + beforeAll(async () => { + await clean(testName) + testDir = await tmpdir(testName) + mockLogger() + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.resetAllMocks() + jest.clearAllTimers() + jest.useRealTimers() + }) + + it("should call isActive when timeout expires", async () => { + const mockIsActive = jest.fn().mockResolvedValue(true) + const heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive) + await heart.beat() + jest.advanceTimersByTime(60 * 1000) + expect(mockIsActive).toHaveBeenCalled() + }) + + it("should log a warning when isActive rejects", async () => { + const error = new Error("oh no") + const mockIsActive = jest.fn().mockRejectedValue(error) + const heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive) + await heart.beat() + jest.advanceTimersByTime(60 * 1000) + expect(mockIsActive).toHaveBeenCalled() + + // The timer callback waits on mockIsActive, so we need to yield to let the + // callback finish. + await new Promise((resolve) => setImmediate(resolve)) + + expect(logger.warn).toHaveBeenCalledWith(error.message) + }) +}) + +describe("stateChange", () => { + const testName = "stateChange" + let testDir = "" + let heart: Heart + + beforeAll(async () => { + await clean(testName) + testDir = await tmpdir(testName) + mockLogger() + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.resetAllMocks() + jest.useRealTimers() + if (heart) { + heart.dispose() + } + }) + + it("should change to alive after a beat", async () => { + const mockIsActive = jest.fn().mockResolvedValue(true) + heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive) + const mockOnChange = jest.fn() + heart.onChange(mockOnChange) + + await heart.beat() + + expect(mockOnChange.mock.calls[0][0]).toBe("alive") + }) + + it("should change to expired when not active", async () => { + const mockIsActive = jest.fn().mockResolvedValue(false) + heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive) + const mockOnChange = jest.fn() + heart.onChange(mockOnChange) + + await heart.beat() + jest.advanceTimersByTime(60 * 1000) + expect(mockIsActive).toHaveBeenCalled() + + // The timer callback waits on the isActive promise, so we need to yield to + // let the callback finish. + await new Promise((resolve) => setImmediate(resolve)) + + expect(mockOnChange.mock.calls[1][0]).toBe("expired") + }) +}) diff --git a/test/unit/node/http.test.ts b/test/unit/node/http.test.ts new file mode 100644 index 000000000000..26fe9be7940b --- /dev/null +++ b/test/unit/node/http.test.ts @@ -0,0 +1,169 @@ +import { getMockReq } from "@jest-mock/express" +import * as http from "../../../src/node/http" +import { mockLogger } from "../../utils/helpers" + +describe("http", () => { + beforeEach(() => { + mockLogger() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("should construct a relative path to the root", () => { + expect(http.relativeRoot("/")).toStrictEqual(".") + expect(http.relativeRoot("/foo")).toStrictEqual(".") + expect(http.relativeRoot("/foo/")).toStrictEqual("./..") + expect(http.relativeRoot("/foo/bar ")).toStrictEqual("./..") + expect(http.relativeRoot("/foo/bar/")).toStrictEqual("./../..") + }) + + describe("isTrustedOrigin", () => { + it("should match exact origins", () => { + expect(http.isTrustedOrigin("localhost:8080", ["localhost:8080"])).toBe(true) + expect(http.isTrustedOrigin("example.com", ["example.com"])).toBe(true) + expect(http.isTrustedOrigin("example.com", ["other.com"])).toBe(false) + }) + + it("should match the wildcard *", () => { + expect(http.isTrustedOrigin("anything.example.com", ["*"])).toBe(true) + expect(http.isTrustedOrigin("localhost:8080", ["*"])).toBe(true) + }) + + it("should match *.example.com wildcard (same style as --proxy-domain)", () => { + expect(http.isTrustedOrigin("sub.example.com", ["*.example.com"])).toBe(true) + expect(http.isTrustedOrigin("example.com", ["*.example.com"])).toBe(true) + expect(http.isTrustedOrigin("evil.com", ["*.example.com"])).toBe(false) + expect(http.isTrustedOrigin("example.com.evil.com", ["*.example.com"])).toBe(false) + }) + + it("should return false for an empty trusted origins list", () => { + expect(http.isTrustedOrigin("example.com", [])).toBe(false) + }) + }) + + describe("origin", () => { + ;[ + { + origin: "", + host: "", + }, + { + origin: "http://localhost:8080", + host: "", + expected: "no host headers", + }, + { + origin: "http://localhost:8080", + host: " ", + expected: "does not match", + }, + { + origin: "http://localhost:8080", + host: "localhost:8080", + }, + { + origin: "http://localhost:8080", + host: "localhost:8081", + expected: "does not match", + }, + { + origin: "localhost:8080", + host: "localhost:8080", + expected: "does not match", // Gets parsed as host: localhost and path: 8080. + }, + { + origin: "test.org", + host: "localhost:8080", + expected: "malformed", // Parsing fails completely. + }, + { + origin: "http://sub.example.com", + host: "other.com", + trustedOrigins: ["*.example.com"], + }, + { + origin: "http://evil.com", + host: "other.com", + trustedOrigins: ["*.example.com"], + expected: "does not match", + }, + { + origin: "http://sub.example.com", + host: "other.com", + trustedOrigins: ["*"], + }, + ].forEach((test) => { + ;[ + ["host", test.host], + ["x-forwarded-host", test.host], + ["x-forwarded-host", `${test.host}, ${test.host}`], + ["forwarded", `for=127.0.0.1, host=${test.host}, proto=http`], + ["forwarded", `for=127.0.0.1;proto=http;host=${test.host}`], + ["forwarded", `proto=http;host=${test.host}, for=127.0.0.1`], + ].forEach(([key, value]) => { + it(`${test.origin} -> [${key}: ${value}]`, () => { + const req = getMockReq({ + originalUrl: "localhost:8080", + headers: { + origin: test.origin, + [key]: value, + }, + args: { + "trusted-origins": (test as { trustedOrigins?: string[] }).trustedOrigins, + }, + }) + if (typeof test.expected === "string") { + expect(() => http.authenticateOrigin(req)).toThrow(test.expected) + } else { + expect(() => http.authenticateOrigin(req)).not.toThrow() + } + }) + }) + }) + }) + + describe("constructRedirectPath", () => { + it("should preserve slashes in queryString so they are human-readable", () => { + const mockReq = getMockReq({ + originalUrl: "localhost:8080", + }) + const mockQueryParams = { folder: "/Users/jp/dev/coder" } + const mockTo = "" + const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo) + const expected = "./?folder=/Users/jp/dev/coder" + expect(actual).toBe(expected) + }) + it("should use an empty string if no query params", () => { + const mockReq = getMockReq({ + originalUrl: "localhost:8080", + }) + const mockQueryParams = {} + const mockTo = "" + const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo) + const expected = "./" + expect(actual).toBe(expected) + }) + it("should append the 'to' path relative to the originalUrl", () => { + const mockReq = getMockReq({ + originalUrl: "localhost:8080", + }) + const mockQueryParams = {} + const mockTo = "vscode" + const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo) + const expected = "./vscode" + expect(actual).toBe(expected) + }) + it("should append append queryParams after 'to' path", () => { + const mockReq = getMockReq({ + originalUrl: "localhost:8080", + }) + const mockQueryParams = { folder: "/Users/jp/dev/coder" } + const mockTo = "vscode" + const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo) + const expected = "./vscode?folder=/Users/jp/dev/coder" + expect(actual).toBe(expected) + }) + }) +}) diff --git a/test/unit/node/i18n.test.ts b/test/unit/node/i18n.test.ts new file mode 100644 index 000000000000..90b10d04822c --- /dev/null +++ b/test/unit/node/i18n.test.ts @@ -0,0 +1,154 @@ +import { promises as fs } from "fs" +import * as os from "os" +import * as path from "path" +import { loadCustomStrings } from "../../../src/node/i18n" + +describe("i18n", () => { + let tempDir: string + let validJsonFile: string + let invalidJsonFile: string + let nonExistentFile: string + + beforeEach(async () => { + // Create temporary directory for test files + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "code-server-i18n-test-")) + + // Create test files + validJsonFile = path.join(tempDir, "valid.json") + invalidJsonFile = path.join(tempDir, "invalid.json") + nonExistentFile = path.join(tempDir, "does-not-exist.json") + + // Write valid JSON file + await fs.writeFile( + validJsonFile, + JSON.stringify({ + WELCOME: "Custom Welcome", + LOGIN_TITLE: "My Custom App", + LOGIN_BELOW: "Please log in to continue", + }), + ) + + // Write invalid JSON file + await fs.writeFile(invalidJsonFile, '{"WELCOME": "Missing closing quote}') + }) + + afterEach(async () => { + // Clean up temporary directory + await fs.rmdir(tempDir, { recursive: true }) + }) + + describe("loadCustomStrings", () => { + it("should load valid JSON file successfully", async () => { + // Should not throw an error + await expect(loadCustomStrings(validJsonFile)).resolves.toBeUndefined() + }) + + it("should throw clear error for non-existent file", async () => { + await expect(loadCustomStrings(nonExistentFile)).rejects.toThrow( + `Custom strings file not found: ${nonExistentFile}\nPlease ensure the file exists and is readable.`, + ) + }) + + it("should throw clear error for invalid JSON", async () => { + await expect(loadCustomStrings(invalidJsonFile)).rejects.toThrow( + `Invalid JSON in custom strings file: ${invalidJsonFile}`, + ) + }) + + it("should handle empty JSON object", async () => { + const emptyJsonFile = path.join(tempDir, "empty.json") + await fs.writeFile(emptyJsonFile, "{}") + + await expect(loadCustomStrings(emptyJsonFile)).resolves.toBeUndefined() + }) + + it("should handle nested JSON objects", async () => { + const nestedJsonFile = path.join(tempDir, "nested.json") + await fs.writeFile( + nestedJsonFile, + JSON.stringify({ + WELCOME: "Hello World", + NESTED: { + KEY: "Value", + }, + }), + ) + + await expect(loadCustomStrings(nestedJsonFile)).resolves.toBeUndefined() + }) + + it("should handle special characters and unicode", async () => { + const unicodeJsonFile = path.join(tempDir, "unicode.json") + await fs.writeFile( + unicodeJsonFile, + JSON.stringify({ + WELCOME: "欢迎来到 code-server", + LOGIN_TITLE: "Willkommen bei {{app}}", + SPECIAL: "Special chars: àáâãäåæçèéêë 🚀 ♠️ ∆", + }), + "utf8", + ) + + await expect(loadCustomStrings(unicodeJsonFile)).resolves.toBeUndefined() + }) + + it("should handle generic errors that are not ENOENT or SyntaxError", async () => { + const testFile = path.join(tempDir, "test.json") + await fs.writeFile(testFile, "{}") + + // Mock fs.readFile to throw a generic error + const originalReadFile = fs.readFile + const mockError = new Error("Permission denied") + fs.readFile = jest.fn().mockRejectedValue(mockError) + + await expect(loadCustomStrings(testFile)).rejects.toThrow( + `Failed to load custom strings from ${testFile}: Permission denied`, + ) + + // Restore original function + fs.readFile = originalReadFile + }) + + it("should handle errors that are not Error instances", async () => { + const testFile = path.join(tempDir, "test.json") + await fs.writeFile(testFile, "{}") + + // Mock fs.readFile to throw a non-Error object + const originalReadFile = fs.readFile + fs.readFile = jest.fn().mockRejectedValue("String error") + + await expect(loadCustomStrings(testFile)).rejects.toThrow( + `Failed to load custom strings from ${testFile}: String error`, + ) + + // Restore original function + fs.readFile = originalReadFile + }) + + it("should handle null/undefined errors", async () => { + const testFile = path.join(tempDir, "test.json") + await fs.writeFile(testFile, "{}") + + // Mock fs.readFile to throw null + const originalReadFile = fs.readFile + fs.readFile = jest.fn().mockRejectedValue(null) + + await expect(loadCustomStrings(testFile)).rejects.toThrow(`Failed to load custom strings from ${testFile}: null`) + + // Restore original function + fs.readFile = originalReadFile + }) + + it("should complete without errors for valid input", async () => { + const testFile = path.join(tempDir, "resource-test.json") + const customStrings = { + WELCOME: "Custom Welcome Message", + LOGIN_TITLE: "Custom Login Title", + } + await fs.writeFile(testFile, JSON.stringify(customStrings)) + + // Should not throw any errors + await expect(loadCustomStrings(testFile)).resolves.toBeUndefined() + }) + }) +}) diff --git a/test/unit/node/main.test.ts b/test/unit/node/main.test.ts new file mode 100644 index 000000000000..39ba2ca8bf9b --- /dev/null +++ b/test/unit/node/main.test.ts @@ -0,0 +1,176 @@ +import { promises as fs } from "fs" +import * as path from "path" +import { setDefaults, parse } from "../../../src/node/cli" +import { loadCustomStrings } from "../../../src/node/i18n" +import { tmpdir } from "../../utils/helpers" + +// Mock the i18n module +jest.mock("../../../src/node/i18n", () => ({ + loadCustomStrings: jest.fn(), +})) + +// Mock logger to avoid console output during tests +jest.mock("@coder/logger", () => ({ + logger: { + info: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + named: jest.fn(), + level: 0, + }, + field: jest.fn(), + Level: { + Trace: 0, + Debug: 1, + Info: 2, + Warn: 3, + Error: 4, + }, +})) + +const mockedLoadCustomStrings = loadCustomStrings as jest.MockedFunction + +describe("main", () => { + let tempDir: string + let mockServer: any + + beforeEach(async () => { + tempDir = await tmpdir("code-server-main-test") + + // Reset mocks + jest.clearAllMocks() + + // Mock the server creation to avoid actually starting a server + mockServer = { + server: { + listen: jest.fn(), + address: jest.fn(() => ({ address: "127.0.0.1", port: 8080 })), + close: jest.fn(), + }, + editorSessionManagerServer: { + address: jest.fn(() => null), + }, + dispose: jest.fn(), + } + }) + + afterEach(async () => { + // Clean up temp directory + try { + await fs.rmdir(tempDir, { recursive: true }) + } catch (error) { + // Ignore cleanup errors + } + }) + + describe("runCodeServer", () => { + it("should load custom strings when i18n flag is provided", async () => { + // Create a test custom strings file + const customStringsFile = path.join(tempDir, "custom-strings.json") + await fs.writeFile( + customStringsFile, + JSON.stringify({ + WELCOME: "Custom Welcome", + LOGIN_TITLE: "My App", + }), + ) + + // Create args with i18n flag + const cliArgs = parse([ + `--config=${path.join(tempDir, "config.yaml")}`, + `--user-data-dir=${tempDir}`, + "--bind-addr=localhost:0", + "--log=warn", + "--auth=none", + `--i18n=${customStringsFile}`, + ]) + const args = await setDefaults(cliArgs) + + // Mock the app module + jest.doMock("../../../src/node/app", () => ({ + createApp: jest.fn().mockResolvedValue(mockServer), + ensureAddress: jest.fn().mockReturnValue(new URL("http://localhost:8080")), + })) + + // Mock routes module + jest.doMock("../../../src/node/routes", () => ({ + register: jest.fn().mockResolvedValue({ disposeRoutes: jest.fn() }), + })) + + // Mock loadCustomStrings to succeed + mockedLoadCustomStrings.mockResolvedValue(undefined) + + // Import runCodeServer after mocking + const mainModule = await import("../../../src/node/main") + const result = await mainModule.runCodeServer(args) + + // Verify that loadCustomStrings was called with the correct file path + expect(mockedLoadCustomStrings).toHaveBeenCalledWith(customStringsFile) + expect(mockedLoadCustomStrings).toHaveBeenCalledTimes(1) + + // Clean up + await result.dispose() + }) + + it("should not load custom strings when i18n flag is not provided", async () => { + // Create args without i18n flag + const cliArgs = parse([ + `--config=${path.join(tempDir, "config.yaml")}`, + `--user-data-dir=${tempDir}`, + "--bind-addr=localhost:0", + "--log=warn", + "--auth=none", + ]) + const args = await setDefaults(cliArgs) + + // Mock the app module + jest.doMock("../../../src/node/app", () => ({ + createApp: jest.fn().mockResolvedValue(mockServer), + ensureAddress: jest.fn().mockReturnValue(new URL("http://localhost:8080")), + })) + + // Mock routes module + jest.doMock("../../../src/node/routes", () => ({ + register: jest.fn().mockResolvedValue({ disposeRoutes: jest.fn() }), + })) + + // Import runCodeServer after mocking + const mainModule = await import("../../../src/node/main") + const result = await mainModule.runCodeServer(args) + + // Verify that loadCustomStrings was NOT called + expect(mockedLoadCustomStrings).not.toHaveBeenCalled() + + // Clean up + await result.dispose() + }) + + it("should handle errors when loadCustomStrings fails", async () => { + // Create args with i18n flag pointing to non-existent file + const nonExistentFile = path.join(tempDir, "does-not-exist.json") + const cliArgs = parse([ + `--config=${path.join(tempDir, "config.yaml")}`, + `--user-data-dir=${tempDir}`, + "--bind-addr=localhost:0", + "--log=warn", + "--auth=none", + `--i18n=${nonExistentFile}`, + ]) + const args = await setDefaults(cliArgs) + + // Mock loadCustomStrings to throw an error + const mockError = new Error("Custom strings file not found") + mockedLoadCustomStrings.mockRejectedValue(mockError) + + // Import runCodeServer after mocking + const mainModule = await import("../../../src/node/main") + + // Verify that runCodeServer throws the error from loadCustomStrings + await expect(mainModule.runCodeServer(args)).rejects.toThrow("Custom strings file not found") + + // Verify that loadCustomStrings was called + expect(mockedLoadCustomStrings).toHaveBeenCalledWith(nonExistentFile) + }) + }) +}) diff --git a/test/unit/node/plugin.test.ts b/test/unit/node/plugin.test.ts deleted file mode 100644 index 5459db2c287f..000000000000 --- a/test/unit/node/plugin.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { logger } from "@coder/logger" -import * as express from "express" -import * as fs from "fs" -import * as path from "path" -import { HttpCode } from "../../../src/common/http" -import { AuthType } from "../../../src/node/cli" -import { codeServer, PluginAPI } from "../../../src/node/plugin" -import * as apps from "../../../src/node/routes/apps" -import * as httpserver from "../../utils/httpserver" -const fsp = fs.promises - -// Jest overrides `require` so our usual override doesn't work. -jest.mock("code-server", () => codeServer, { virtual: true }) - -/** - * Use $LOG_LEVEL=debug to see debug logs. - */ -describe("plugin", () => { - let papi: PluginAPI - let s: httpserver.HttpServer - - beforeAll(async () => { - // Only include the test plugin to avoid contaminating results with other - // plugins that might be on the filesystem. - papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`, "") - await papi.loadPlugins(false) - - const app = express.default() - const wsApp = express.default() - - const common: express.RequestHandler = (req, _, next) => { - // Routes might use these arguments. - req.args = { - _: [], - auth: AuthType.None, - host: "localhost", - port: 8080, - "proxy-domain": [], - config: "~/.config/code-server/config.yaml", - verbose: false, - usingEnvPassword: false, - usingEnvHashedPassword: false, - "extensions-dir": "", - "user-data-dir": "", - } - next() - } - - app.use(common) - wsApp.use(common) - - papi.mount(app, wsApp) - app.use("/api/applications", apps.router(papi)) - - s = new httpserver.HttpServer() - await s.listen(app) - s.listenUpgrade(wsApp) - }) - - afterAll(async () => { - await s.close() - }) - - it("/api/applications", async () => { - const resp = await s.fetch("/api/applications") - expect(resp.status).toBe(200) - const body = await resp.json() - logger.debug(`${JSON.stringify(body)}`) - expect(body).toStrictEqual([ - { - name: "Test App", - version: "4.0.0", - - description: "This app does XYZ.", - iconPath: "/test-plugin/test-app/icon.svg", - homepageURL: "https://example.com", - path: "/test-plugin/test-app", - - plugin: { - name: "test-plugin", - version: "1.0.0", - modulePath: path.join(__dirname, "test-plugin"), - - displayName: "Test Plugin", - description: "Plugin used in code-server tests.", - routerPath: "/test-plugin", - homepageURL: "https://example.com", - }, - }, - ]) - }) - - it("/test-plugin/test-app", async () => { - const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), { - encoding: "utf8", - }) - const resp = await s.fetch("/test-plugin/test-app") - expect(resp.status).toBe(200) - const body = await resp.text() - expect(body).toBe(indexHTML) - }) - - it("/test-plugin/test-app (websocket)", async () => { - const ws = s.ws("/test-plugin/test-app") - const message = await new Promise((resolve) => { - ws.once("message", (message) => resolve(message)) - }) - ws.terminate() - expect(message).toBe("hello") - }) - - it("/test-plugin/error", async () => { - const resp = await s.fetch("/test-plugin/error") - expect(resp.status).toBe(HttpCode.LargePayload) - }) -}) diff --git a/test/unit/node/proxy.test.ts b/test/unit/node/proxy.test.ts index fe349bddac0e..4c3320a07b93 100644 --- a/test/unit/node/proxy.test.ts +++ b/test/unit/node/proxy.test.ts @@ -1,41 +1,56 @@ -import bodyParser from "body-parser" import * as express from "express" -import * as http from "http" -import * as nodeFetch from "node-fetch" import { HttpCode } from "../../../src/common/http" -import { proxy } from "../../../src/node/proxy" -import { getAvailablePort } from "../../utils/helpers" +import { wss, Router as WsRouter } from "../../../src/node/wsRouter" +import { mockLogger } from "../../utils/helpers" import * as httpserver from "../../utils/httpserver" import * as integration from "../../utils/integration" describe("proxy", () => { - const nhooyrDevServer = new httpserver.HttpServer() + const proxyTarget = new httpserver.HttpServer() + const wsApp = express.default() + const wsRouter = WsRouter() let codeServer: httpserver.HttpServer | undefined let proxyPath: string let absProxyPath: string let e: express.Express beforeAll(async () => { - await nhooyrDevServer.listen((req, res) => { + wsApp.use("/", wsRouter.router) + await proxyTarget.listen((req, res) => { e(req, res) }) - proxyPath = `/proxy/${nhooyrDevServer.port()}/wsup` + proxyTarget.listenUpgrade(wsApp) + proxyPath = `/proxy/${proxyTarget.port()}/wsup` absProxyPath = proxyPath.replace("/proxy/", "/absproxy/") }) afterAll(async () => { - await nhooyrDevServer.close() + await proxyTarget.dispose() }) beforeEach(() => { e = express.default() + mockLogger() + delete process.env.PASSWORD }) afterEach(async () => { if (codeServer) { - await codeServer.close() + await codeServer.dispose() codeServer = undefined } + jest.clearAllMocks() + }) + + it("should return 403 Forbidden if proxy is disabled", async () => { + e.get("/wsup", (req, res) => { + res.json("you cannot see this") + }) + codeServer = await integration.setup(["--auth=none", "--disable-proxy"], "") + const resp = await codeServer.fetch(proxyPath) + expect(resp.status).toBe(403) + const json = await resp.json() + expect(json).toEqual({ error: "Forbidden" }) }) it("should rewrite the base path", async () => { @@ -92,7 +107,7 @@ describe("proxy", () => { }) it("should allow post bodies", async () => { - e.use(bodyParser.json({ strict: false })) + e.use(express.json({ strict: false })) e.post("/wsup", (req, res) => { res.json(req.body) }) @@ -109,7 +124,7 @@ describe("proxy", () => { }) it("should handle bad requests", async () => { - e.use(bodyParser.json({ strict: false })) + e.use(express.json({ strict: false })) e.post("/wsup", (req, res) => { res.json(req.body) }) @@ -136,7 +151,7 @@ describe("proxy", () => { }) it("should handle errors", async () => { - e.use(bodyParser.json({ strict: false })) + e.use(express.json({ strict: false })) e.post("/wsup", (req, res) => { throw new Error("BROKEN") }) @@ -151,65 +166,176 @@ describe("proxy", () => { expect(resp.status).toBe(500) expect(resp.statusText).toMatch("Internal Server Error") }) -}) -// NOTE@jsjoeio -// Both this test suite and the one above it are very similar -// The main difference is this one uses http and node-fetch -// and specifically tests the proxy in isolation vs. using -// the httpserver abstraction we've built. -// -// Leaving this as a separate test suite for now because -// we may consider refactoring the httpserver abstraction -// in the future. -// -// If you're writing a test specifically for code in -// src/node/proxy.ts, you should probably add it to -// this test suite. -describe("proxy (standalone)", () => { - let URL = "" - let PROXY_URL = "" - let testServer: http.Server - let proxyTarget: http.Server - - beforeEach(async () => { - const PORT = await getAvailablePort() - const PROXY_PORT = await getAvailablePort() - URL = `http://localhost:${PORT}` - PROXY_URL = `http://localhost:${PROXY_PORT}` - // Define server and a proxy server - testServer = http.createServer((req, res) => { - proxy.web(req, res, { - target: PROXY_URL, + it("should pass origin check", async () => { + wsRouter.ws("/wsup", async (req) => { + wss.handleUpgrade(req, req.ws, req.head, (ws) => { + ws.send("hello") + req.ws.resume() }) }) + codeServer = await integration.setup(["--auth=none"], "") + const ws = await codeServer.wsWait(proxyPath, { + headers: { + host: "localhost:8080", + origin: "https://localhost:8080", + }, + }) + ws.terminate() + }) - proxyTarget = http.createServer((req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }) + it("should fail origin check", async () => { + await expect(async () => { + codeServer = await integration.setup(["--auth=none"], "") + await codeServer.wsWait(proxyPath, { + headers: { + host: "localhost:8080", + origin: "https://evil.org", + }, + }) + }).rejects.toThrow() + }) + + it("should proxy non-ASCII", async () => { + e.get(/.*/, (req, res) => { + res.json("ほげ") + }) + codeServer = await integration.setup(["--auth=none"], "") + const resp = await codeServer.fetch(proxyPath.replace("wsup", "ほげ")) + expect(resp.status).toBe(200) + const json = await resp.json() + expect(json).toBe("ほげ") + }) + + it("should not double-encode query variables", async () => { + const spy = jest.fn() + e.get(/.*/, (req, res) => { + spy([req.originalUrl, req.query]) res.end() }) + codeServer = await integration.setup(["--auth=none"], "") + for (const test of [ + { + endpoint: proxyPath, + query: { foo: "bar with spaces" }, + expected: "/wsup?foo=bar+with+spaces", + }, + { + endpoint: absProxyPath, + query: { foo: "bar with spaces" }, + expected: absProxyPath + "?foo=bar+with+spaces", + }, + { + endpoint: proxyPath, + query: { foo: "with-&-ampersand" }, + expected: "/wsup?foo=with-%26-ampersand", + }, + { + endpoint: absProxyPath, + query: { foo: "with-&-ampersand" }, + expected: absProxyPath + "?foo=with-%26-ampersand", + }, + { + endpoint: absProxyPath, + query: { foo: "ほげ ほげ" }, + expected: absProxyPath + "?foo=%E3%81%BB%E3%81%92+%E3%81%BB%E3%81%92", + }, + { + endpoint: proxyPath, + query: { foo: "ほげ ほげ" }, + expected: "/wsup?foo=%E3%81%BB%E3%81%92+%E3%81%BB%E3%81%92", + }, + ]) { + spy.mockClear() + const resp = await codeServer.fetch(test.endpoint, undefined, test.query) + expect(resp.status).toBe(200) + await resp.text() + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith([test.expected, test.query]) + } + }) + + it("should allow specifying an absproxy path", async () => { + const prefixedPath = `/codeserver/app1${absProxyPath}` + e.get(prefixedPath, (req, res) => { + res.send("app being served behind a prefixed path") + }) + codeServer = await integration.setup(["--auth=none", "--abs-proxy-base-path=/codeserver/app1"], "") + const resp = await codeServer.fetch(absProxyPath) + expect(resp.status).toBe(200) + const text = await resp.text() + expect(text).toBe("app being served behind a prefixed path") + }) - // Start both servers - await proxyTarget.listen(PROXY_PORT) - await testServer.listen(PORT) + it("should not allow OPTIONS without authentication by default", async () => { + process.env.PASSWORD = "test" + codeServer = await integration.setup(["--auth=password"]) + const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" }) + expect(resp.status).toBe(401) }) - afterEach(async () => { - await testServer.close() - await proxyTarget.close() + it("should allow OPTIONS with `skip-auth-preflight` flag", async () => { + process.env.PASSWORD = "test" + codeServer = await integration.setup(["--auth=password", "--skip-auth-preflight"]) + e.post("/wsup", (req, res) => {}) + const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" }) + expect(resp.status).toBe(200) }) - it("should return a 500 when proxy target errors ", async () => { - // Close the proxy target so that proxy errors - await proxyTarget.close() - const errorResp = await nodeFetch.default(`${URL}/error`) - expect(errorResp.status).toBe(HttpCode.ServerError) - expect(errorResp.statusText).toBe("Internal Server Error") + it("should return a 500 when no target is running ", async () => { + const target = new httpserver.HttpServer() + await target.listen(() => {}) + const port = target.port() + target.dispose() + codeServer = await integration.setup(["--auth=none"], "") + const resp = await codeServer.fetch(`/proxy/${port}/wsup`) + expect(resp.status).toBe(HttpCode.ServerError) + expect(resp.statusText).toBe("Internal Server Error") + }) + + it("should strip token cookie", async () => { + const token = "my-super-secure-token" + process.env.HASHED_PASSWORD = token + codeServer = await integration.setup(["--auth=password"]) + + e.get("/wsup/cookies", (req, res) => { + res.writeHead(HttpCode.Ok, { "Content-Type": "text/plain" }) + res.end(req.headers.cookie) + }) + + const cookies = [ + "cookie2=hello", + // Cookies should pass through unchanged, neither encoded nor decoded. + `cookie1=${encodeURIComponent("hello=there")}`, + "cookie3=foo|bar", + `cookie4=${encodeURIComponent("foo|bar")}`, + `cookie5=${encodeURIComponent("bar;baz")}`, + ] + + // Test each slot to ensure the token is found anywhere. + for (let i = 0; i <= cookies.length; ++i) { + const left = cookies.slice(0, i) + const right = cookies.slice(i) + const resp = await codeServer.fetch(proxyPath + "/cookies", { + headers: { + cookie: [...left, `code-server-session=${token}`, ...right].join("; "), + }, + }) + expect(resp.status).toBe(200) + expect(await resp.text()).toBe(cookies.join("; ")) + } }) - it("should proxy correctly", async () => { - const resp = await nodeFetch.default(`${URL}/route`) + it("should proxy when no cookies", async () => { + codeServer = await integration.setup(["--auth=none"]) + + e.get("/wsup/cookies", (req, res) => { + res.writeHead(HttpCode.Ok, { "Content-Type": "text/plain" }) + res.end(req.headers.cookie || "no cookies") + }) + + const resp = await codeServer.fetch(proxyPath + "/cookies") expect(resp.status).toBe(200) - expect(resp.statusText).toBe("OK") + expect(await resp.text()).toBe("no cookies") }) }) diff --git a/test/unit/node/proxy_agent.test.ts b/test/unit/node/proxy_agent.test.ts deleted file mode 100644 index a2552b7f0399..000000000000 --- a/test/unit/node/proxy_agent.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { shouldEnableProxy } from "../../../src/node/proxy_agent" -import { useEnv } from "../../utils/helpers" - -describe("shouldEnableProxy", () => { - const [setHTTPProxy, resetHTTPProxy] = useEnv("HTTP_PROXY") - const [setHTTPSProxy, resetHTTPSProxy] = useEnv("HTTPS_PROXY") - const [setNoProxy, resetNoProxy] = useEnv("NO_PROXY") - - beforeEach(() => { - jest.resetModules() // Most important - it clears the cache - resetHTTPProxy() - resetNoProxy() - resetHTTPSProxy() - }) - - it("returns true when HTTP_PROXY is set", () => { - setHTTPProxy("http://proxy.example.com") - expect(shouldEnableProxy()).toBe(true) - }) - it("returns true when HTTPS_PROXY is set", () => { - setHTTPSProxy("https://proxy.example.com") - expect(shouldEnableProxy()).toBe(true) - }) - it("returns false when NO_PROXY is set", () => { - setNoProxy("*") - expect(shouldEnableProxy()).toBe(false) - }) - it("should return false when neither HTTP_PROXY nor HTTPS_PROXY is set", () => { - expect(shouldEnableProxy()).toBe(false) - }) - it("should return false when NO_PROXY is set to https://example.com", () => { - setNoProxy("https://example.com") - expect(shouldEnableProxy()).toBe(false) - }) - it("should return false when NO_PROXY is set to http://example.com", () => { - setNoProxy("http://example.com") - expect(shouldEnableProxy()).toBe(false) - }) -}) diff --git a/test/unit/node/routes/errors.test.ts b/test/unit/node/routes/errors.test.ts new file mode 100644 index 000000000000..94657abf9f92 --- /dev/null +++ b/test/unit/node/routes/errors.test.ts @@ -0,0 +1,69 @@ +import express from "express" +import { UserProvidedArgs, setDefaults } from "../../../../src/node/cli" +import { errorHandler } from "../../../../src/node/routes/errors" + +describe("error page is rendered for text/html requests", () => { + it("escapes any html in the error messages", async () => { + const next = jest.fn() + const err = { + code: "ENOENT", + statusCode: 404, + message: ";>hello", + } + const req = await createRequest() + const res = { + status: jest.fn().mockReturnValue(this), + send: jest.fn().mockReturnValue(this), + set: jest.fn().mockReturnValue(this), + } as unknown as express.Response + + await errorHandler(err, req, res, next) + expect(res.status).toHaveBeenCalledWith(404) + expect(res.send).toHaveBeenCalledWith(expect.not.stringContaining("