Format all files with prettier

This commit is contained in:
Andy Balaam 2022-12-15 11:00:58 +00:00
parent 040344eeab
commit 0faac52dae
67 changed files with 1760 additions and 1793 deletions

View file

@ -1,8 +1,6 @@
module.exports = { module.exports = {
plugins: ["matrix-org"], plugins: ["matrix-org"],
extends: [ extends: ["plugin:matrix-org/javascript"],
"plugin:matrix-org/javascript",
],
parserOptions: { parserOptions: {
ecmaVersion: 2021, ecmaVersion: 2021,
}, },
@ -20,19 +18,19 @@ module.exports = {
"prefer-promise-reject-errors": "off", "prefer-promise-reject-errors": "off",
"no-async-promise-executor": "off", "no-async-promise-executor": "off",
}, },
overrides: [{ overrides: [
files: ["{src,scripts,hak}/**/*.{ts,tsx}"], {
extends: [ files: ["{src,scripts,hak}/**/*.{ts,tsx}"],
"plugin:matrix-org/typescript", extends: ["plugin:matrix-org/typescript"],
], rules: {
rules: { // Things we do that break the ideal style
// Things we do that break the ideal style "prefer-promise-reject-errors": "off",
"prefer-promise-reject-errors": "off", "quotes": "off",
"quotes": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them // We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
},
}, },
}], ],
}; };

View file

@ -2,9 +2,9 @@
## Checklist ## Checklist
* [ ] Ensure your code works with manual testing - [ ] Ensure your code works with manual testing
* [ ] Linter and other CI checks pass - [ ] Linter and other CI checks pass
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/vector-im/element-desktop/blob/develop/CONTRIBUTING.md)) - [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/vector-im/element-desktop/blob/develop/CONTRIBUTING.md))
<!-- <!--
If you would like to specify text for the changelog entry other than your PR title, add the following: If you would like to specify text for the changelog entry other than your PR title, add the following:

View file

@ -1,6 +1,4 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["github>matrix-org/renovate-config-element-web"]
"github>matrix-org/renovate-config-element-web"
]
} }

View file

@ -1,30 +1,30 @@
name: Backport name: Backport
on: on:
pull_request_target: pull_request_target:
types: types:
- closed - closed
- labeled - labeled
branches: branches:
- develop - develop
jobs: jobs:
backport: backport:
name: Backport name: Backport
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only react to merged PRs for security reasons. # Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: > if: >
github.event.pull_request.merged github.event.pull_request.merged
&& ( && (
github.event.action == 'closed' github.event.action == 'closed'
|| ( || (
github.event.action == 'labeled' github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport') && contains(github.event.label.name, 'backport')
) )
) )
steps: steps:
- uses: tibdex/backport@v2 - uses: tibdex/backport@v2
with: with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>" labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR # We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }} github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -1,107 +1,107 @@
name: Build and Test name: Build and Test
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
fetch: fetch:
uses: ./.github/workflows/build_prepare.yaml uses: ./.github/workflows/build_prepare.yaml
with:
config: ${{ github.event.pull_request.base.ref == 'develop' && 'element.io/nightly' || 'element.io/release' }}
version: ${{ github.event.pull_request.base.ref == 'develop' && 'develop' || '' }}
windows:
needs: fetch
name: Windows
uses: ./.github/workflows/build_windows.yaml
strategy:
matrix:
arch: [ x64, x86 ]
with:
arch: ${{ matrix.arch }}
linux:
needs: fetch
name: Linux
uses: ./.github/workflows/build_linux.yaml
strategy:
matrix:
sqlcipher: [ system, static ]
with:
sqlcipher: ${{ matrix.sqlcipher }}
macos:
needs: fetch
name: macOS
uses: ./.github/workflows/build_macos.yaml
test:
needs:
- macos
- linux
- windows
strategy:
matrix:
include:
# Disable macOS tests for now, they fail to run in CI, needs investigation.
# - name: macOS Universal
# os: macos
# artifact: macos
# executable: "./dist/mac-universal/Element.app/Contents/MacOS/Element"
# prepare_cmd: "chmod +x ./dist/mac-universal/Element.app/Contents/MacOS/Element"
- name: 'Linux (sqlcipher: system)'
os: ubuntu
artifact: linux-sqlcipher-system
executable: "element-desktop"
prepare_cmd: "sudo apt install ./dist/*.deb"
- name: 'Linux (sqlcipher: static)'
os: ubuntu
artifact: linux-sqlcipher-static
executable: "element-desktop"
prepare_cmd: "sudo apt install ./dist/*.deb"
- name: Windows (x86)
os: windows
artifact: win-x86
executable: "./dist/win-ia32-unpacked/Element.exe"
- name: Windows (x64)
os: windows
artifact: win-x64
executable: "./dist/win-unpacked/Element.exe"
name: Test ${{ matrix.name }}
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: with:
cache: "yarn" config: ${{ github.event.pull_request.base.ref == 'develop' && 'element.io/nightly' || 'element.io/release' }}
version: ${{ github.event.pull_request.base.ref == 'develop' && 'develop' || '' }}
- name: Install Deps windows:
run: "yarn install --pure-lockfile" needs: fetch
name: Windows
- uses: actions/download-artifact@v3 uses: ./.github/workflows/build_windows.yaml
strategy:
matrix:
arch: [x64, x86]
with: with:
name: ${{ matrix.artifact }} arch: ${{ matrix.arch }}
path: dist
- name: Prepare for tests linux:
run: ${{ matrix.prepare_cmd }} needs: fetch
if: matrix.prepare_cmd name: Linux
uses: ./.github/workflows/build_linux.yaml
- name: Run tests strategy:
uses: GabrielBB/xvfb-action@v1 matrix:
timeout-minutes: 5 sqlcipher: [system, static]
with: with:
run: "yarn test" sqlcipher: ${{ matrix.sqlcipher }}
env:
ELEMENT_DESKTOP_EXECUTABLE: ${{ matrix.executable }}
- name: Upload Artifacts macos:
uses: actions/upload-artifact@v3 needs: fetch
with: name: macOS
name: ${{ matrix.artifact }} uses: ./.github/workflows/build_macos.yaml
path: test_artifacts
retention-days: 1 test:
needs:
- macos
- linux
- windows
strategy:
matrix:
include:
# Disable macOS tests for now, they fail to run in CI, needs investigation.
# - name: macOS Universal
# os: macos
# artifact: macos
# executable: "./dist/mac-universal/Element.app/Contents/MacOS/Element"
# prepare_cmd: "chmod +x ./dist/mac-universal/Element.app/Contents/MacOS/Element"
- name: "Linux (sqlcipher: system)"
os: ubuntu
artifact: linux-sqlcipher-system
executable: "element-desktop"
prepare_cmd: "sudo apt install ./dist/*.deb"
- name: "Linux (sqlcipher: static)"
os: ubuntu
artifact: linux-sqlcipher-static
executable: "element-desktop"
prepare_cmd: "sudo apt install ./dist/*.deb"
- name: Windows (x86)
os: windows
artifact: win-x86
executable: "./dist/win-ia32-unpacked/Element.exe"
- name: Windows (x64)
os: windows
artifact: win-x64
executable: "./dist/win-unpacked/Element.exe"
name: Test ${{ matrix.name }}
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --pure-lockfile"
- uses: actions/download-artifact@v3
with:
name: ${{ matrix.artifact }}
path: dist
- name: Prepare for tests
run: ${{ matrix.prepare_cmd }}
if: matrix.prepare_cmd
- name: Run tests
uses: GabrielBB/xvfb-action@v1
timeout-minutes: 5
with:
run: "yarn test"
env:
ELEMENT_DESKTOP_EXECUTABLE: ${{ matrix.executable }}
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.artifact }}
path: test_artifacts
retention-days: 1

View file

@ -2,63 +2,63 @@
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure # Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch. # the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
on: on:
workflow_call: workflow_call:
inputs: inputs:
sqlcipher: sqlcipher:
type: string type: string
required: true required: true
description: "How to link sqlcipher, one of 'system' | 'static'" description: "How to link sqlcipher, one of 'system' | 'static'"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: webapp name: webapp
- name: Cache .hak - name: Cache .hak
id: cache id: cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
key: ${{ runner.os }}-${{ hashFiles('hakDependencies.json', 'electronVersion') }} key: ${{ runner.os }}-${{ hashFiles('hakDependencies.json', 'electronVersion') }}
path: | path: |
./.hak ./.hak
- name: Install Rust - name: Install Rust
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
- name: Install libsqlcipher-dev - name: Install libsqlcipher-dev
if: steps.cache.outputs.cache-hit != 'true' && inputs.sqlcipher == 'system' if: steps.cache.outputs.cache-hit != 'true' && inputs.sqlcipher == 'system'
run: sudo apt-get install -y libsqlcipher-dev run: sudo apt-get install -y libsqlcipher-dev
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: "yarn" cache: "yarn"
# Does not need branch matching as only analyses this layer # Does not need branch matching as only analyses this layer
- name: Install Deps - name: Install Deps
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: Build Natives - name: Build Natives
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: "yarn build:native" run: "yarn build:native"
env: env:
SQLCIPHER_STATIC: ${{ inputs.sqlcipher == 'static' && '1' || '' }} SQLCIPHER_STATIC: ${{ inputs.sqlcipher == 'static' && '1' || '' }}
- name: Build App - name: Build App
run: "yarn build --publish never -l" run: "yarn build --publish never -l"
- name: Install .deb - name: Install .deb
run: "sudo apt install ./dist/*.deb" run: "sudo apt install ./dist/*.deb"
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: linux-sqlcipher-${{ inputs.sqlcipher }} name: linux-sqlcipher-${{ inputs.sqlcipher }}
path: dist path: dist
retention-days: 1 retention-days: 1

View file

@ -2,50 +2,50 @@
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure # Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch. # the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
on: on:
workflow_call: workflow_call:
jobs: jobs:
build: build:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: webapp name: webapp
- name: Cache .hak - name: Cache .hak
id: cache id: cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
key: ${{ runner.os }}-${{ hashFiles('hakDependencies.json', 'electronVersion') }} key: ${{ runner.os }}-${{ hashFiles('hakDependencies.json', 'electronVersion') }}
path: | path: |
./.hak ./.hak
- name: Install Rust - name: Install Rust
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
target: aarch64-apple-darwin target: aarch64-apple-darwin
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: "yarn" cache: "yarn"
# Does not need branch matching as only analyses this layer # Does not need branch matching as only analyses this layer
- name: Install Deps - name: Install Deps
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: Build Natives - name: Build Natives
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: "yarn build:native:universal" run: "yarn build:native:universal"
- name: Build App - name: Build App
run: "yarn build:universal --publish never" run: "yarn build:universal --publish never"
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: macos name: macos
path: dist path: dist
retention-days: 1 retention-days: 1

View file

@ -1,43 +1,43 @@
on: on:
workflow_call: workflow_call:
inputs: inputs:
config: config:
type: string type: string
required: true required: true
description: "The config directory to use" description: "The config directory to use"
version: version:
type: string type: string
required: false required: false
description: "The version tag to fetch, or 'develop', will pick automatically if not passed" description: "The version tag to fetch, or 'develop', will pick automatically if not passed"
jobs: jobs:
prepare: prepare:
name: Prepare name: Prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: "yarn" cache: "yarn"
- name: Install Deps - name: Install Deps
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: Fetch Element Web - name: Fetch Element Web
run: yarn run fetch --noverify -d ${{ inputs.config }} ${{ inputs.version }} run: yarn run fetch --noverify -d ${{ inputs.config }} ${{ inputs.version }}
# We split this out to save the build_* scripts having to do it to make use of `hashFiles` in the cache action # We split this out to save the build_* scripts having to do it to make use of `hashFiles` in the cache action
- name: Generate cache hash files - name: Generate cache hash files
run: | run: |
yarn run --silent electron --version > electronVersion yarn run --silent electron --version > electronVersion
cat package.json | jq -c .hakDependencies > hakDependencies.json cat package.json | jq -c .hakDependencies > hakDependencies.json
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: webapp name: webapp
retention-days: 1 retention-days: 1
path: | path: |
webapp.asar webapp.asar
package.json package.json
electronVersion electronVersion
hakDependencies.json hakDependencies.json

View file

@ -2,92 +2,92 @@
# Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure # Due to this extra care must be taken to only ever run all build_* scripts against the same branch to ensure
# the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch. # the correct cache scoping, and additional care must be taken to not run untrusted actions on the develop branch.
on: on:
workflow_call: workflow_call:
inputs: inputs:
arch: arch:
type: string type: string
required: true required: true
description: "The architecture to build for, one of 'x64' | 'x86'" description: "The architecture to build for, one of 'x64' | 'x86'"
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: kanga333/variable-mapper@master - uses: kanga333/variable-mapper@master
id: config id: config
with: with:
key: "${{ inputs.arch }}" key: "${{ inputs.arch }}"
export_to: output export_to: output
map: | map: |
{ {
"x64": { "x64": {
"target": "x86_64-pc-windows-msvc" "target": "x86_64-pc-windows-msvc"
}, },
"x86": { "x86": {
"target": "i686-pc-windows-msvc", "target": "i686-pc-windows-msvc",
"build-args": "--ia32" "build-args": "--ia32"
} }
} }
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: webapp name: webapp
- name: Cache .hak - name: Cache .hak
id: cache id: cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
key: ${{ runner.os }}-${{ hashFiles('hakDependencies.json', 'electronVersion') }} key: ${{ runner.os }}-${{ hashFiles('hakDependencies.json', 'electronVersion') }}
path: | path: |
./.hak ./.hak
- name: Set up build tools - name: Set up build tools
uses: ilammy/msvc-dev-cmd@v1 uses: ilammy/msvc-dev-cmd@v1
with: with:
arch: ${{ inputs.arch }} arch: ${{ inputs.arch }}
# ActiveTCL package on choco is from 2015, # ActiveTCL package on choco is from 2015,
# this one is newer but includes more than we need # this one is newer but includes more than we need
- name: Choco install tclsh - name: Choco install tclsh
shell: pwsh shell: pwsh
run: | run: |
choco install -y magicsplat-tcl-tk --no-progress choco install -y magicsplat-tcl-tk --no-progress
echo "${HOME}/AppData/Local/Apps/Tcl86/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append echo "${HOME}/AppData/Local/Apps/Tcl86/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Choco install NetWide Assembler - name: Choco install NetWide Assembler
shell: pwsh shell: pwsh
run: | run: |
choco install -y nasm --no-progress choco install -y nasm --no-progress
echo "C:/Program Files/NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append echo "C:/Program Files/NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install Rust - name: Install Rust
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
target: ${{ steps.config.outputs.target }} target: ${{ steps.config.outputs.target }}
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: "yarn" cache: "yarn"
# Does not need branch matching as only analyses this layer # Does not need branch matching as only analyses this layer
- name: Install Deps - name: Install Deps
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: Build Natives - name: Build Natives
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: | run: |
refreshenv refreshenv
yarn build:native --target ${{ steps.config.outputs.target }} yarn build:native --target ${{ steps.config.outputs.target }}
- name: Build App - name: Build App
run: "yarn build --publish never -w ${{ steps.config.outputs.build-args }}" run: "yarn build --publish never -w ${{ steps.config.outputs.build-args }}"
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: win-${{ inputs.arch }} name: win-${{ inputs.arch }}
path: dist path: dist
retention-days: 1 retention-days: 1

View file

@ -1,43 +1,43 @@
name: Generate packages.element.io directory indexes name: Generate packages.element.io directory indexes
on: on:
# Trigger a rebuild of all indexes if the template gets updated # Trigger a rebuild of all indexes if the template gets updated
push: push:
branches: [ develop ] branches: [develop]
paths: paths:
- 'packages.element.io/**' - "packages.element.io/**"
# Trigger a daily rebuild for nightlies # Trigger a daily rebuild for nightlies
schedule: schedule:
- cron: '0 11 * * *' - cron: "0 11 * * *"
# Manual trigger for rebuilding for releases # Manual trigger for rebuilding for releases
workflow_dispatch: { } workflow_dispatch: {}
jobs: jobs:
deploy: deploy:
name: "Deploy" name: "Deploy"
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: develop environment: develop
env:
R2_BUCKET: 'packages-element-io'
R2_URL: ${{ secrets.CF_R2_S3_API }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: Copy static files
if: github.event_name == 'push'
run: aws s3 cp --recursive packages.element.io/ s3://$R2_BUCKET/ --endpoint-url $R2_URL --region auto
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }} R2_BUCKET: "packages-element-io"
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }} R2_URL: ${{ secrets.CF_R2_S3_API }}
steps:
- uses: actions/checkout@v3
- name: Generate directory indexes - uses: actions/setup-node@v3
run: scripts/generate-packages-index.ts with:
env: cache: "yarn"
CF_R2_S3_API: ${{ secrets.CF_R2_S3_API }}
CF_R2_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }} - name: Install Deps
CF_R2_TOKEN: ${{ secrets.CF_R2_TOKEN }} run: "yarn install --pure-lockfile"
- name: Copy static files
if: github.event_name == 'push'
run: aws s3 cp --recursive packages.element.io/ s3://$R2_BUCKET/ --endpoint-url $R2_URL --region auto
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
- name: Generate directory indexes
run: scripts/generate-packages-index.ts
env:
CF_R2_S3_API: ${{ secrets.CF_R2_S3_API }}
CF_R2_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
CF_R2_TOKEN: ${{ secrets.CF_R2_TOKEN }}

View file

@ -1,12 +1,12 @@
name: Pull Request name: Pull Request
on: on:
pull_request_target: pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ] types: [opened, edited, labeled, unlabeled, synchronize]
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }} concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs: jobs:
action: action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
with: with:
labels: "T-Defect,T-Enhancement,T-Task" labels: "T-Defect,T-Enhancement,T-Task"
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -1,43 +1,43 @@
name: Static Analysis name: Static Analysis
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
jobs: jobs:
ts_lint: ts_lint:
name: "Typescript Syntax Check" name: "Typescript Syntax Check"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
# Does not need branch matching as only analyses this layer # Does not need branch matching as only analyses this layer
- name: Install Deps - name: Install Deps
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: Typecheck - name: Typecheck
run: "yarn run lint:types" run: "yarn run lint:types"
i18n_lint: i18n_lint:
name: "i18n Check" name: "i18n Check"
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
js_lint: js_lint:
name: "ESLint" name: "ESLint"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
# Does not need branch matching as only analyses this layer # Does not need branch matching as only analyses this layer
- name: Install Deps - name: Install Deps
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: Run Linter - name: Run Linter
run: "yarn run lint:js" run: "yarn run lint:js"

View file

@ -1,8 +1,8 @@
name: Upgrade Dependencies name: Upgrade Dependencies
on: on:
workflow_dispatch: { } workflow_dispatch: {}
jobs: jobs:
upgrade: upgrade:
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

19
.prettierignore Normal file
View file

@ -0,0 +1,19 @@
/build/
/dockerbuild/
/lib/
/node_modules/
/packages.elememt.io/
/webapp
/src/i18n/strings
/CHANGELOG.md
/package-lock.json
/yarn.lock
**/.idea
.vscode
.vscode/
.tmp
.env
/coverage
/.npmrc
/*.log

1
.prettierrc.js Normal file
View file

@ -0,0 +1 @@
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");

View file

@ -5,21 +5,20 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-desktop) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-desktop)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-desktop) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-desktop&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-desktop)
Element Desktop # Element Desktop
===============
Element Desktop is a Matrix client for desktop platforms with Element Web at its core. Element Desktop is a Matrix client for desktop platforms with Element Web at its core.
First Steps # First Steps
===========
Before you do anything else, fetch the dependencies: Before you do anything else, fetch the dependencies:
``` ```
yarn install yarn install
``` ```
Fetching Element # Fetching Element
================
Since this package is just the Electron wrapper for Element Web, it doesn't contain any of the Element Web code, Since this package is just the Electron wrapper for Element Web, it doesn't contain any of the Element Web code,
so the first step is to get a working copy of Element Web. There are a few ways of doing this: so the first step is to get a working copy of Element Web. There are a few ways of doing this:
@ -31,6 +30,7 @@ yarn run fetch --noverify --cfgdir ""
``` ```
...or if you'd like to use GPG to verify the downloaded package: ...or if you'd like to use GPG to verify the downloaded package:
``` ```
# Fetch the Element public key from the element.io web server over a secure connection and import # Fetch the Element public key from the element.io web server over a secure connection and import
# it into your local GPG keychain (you'll need GPG installed). You only need to to do this # it into your local GPG keychain (you'll need GPG installed). You only need to to do this
@ -41,6 +41,7 @@ yarn run fetch --cfgdir ""
``` ```
...or either of the above, but fetching a specific version of Element: ...or either of the above, but fetching a specific version of Element:
``` ```
# Fetch the prebuilt release Element package from the element-web GitHub releases page. The version # Fetch the prebuilt release Element package from the element-web GitHub releases page. The version
# fetched will be the same as the local element-desktop package. # fetched will be the same as the local element-desktop package.
@ -49,6 +50,7 @@ yarn run fetch --noverify --cfgdir "" v1.5.6
If you only want to run the app locally and don't need to build packages, you can If you only want to run the app locally and don't need to build packages, you can
provide the `webapp` directory directly: provide the `webapp` directory directly:
``` ```
# Assuming you've checked out and built a copy of element-web in ../element-web # Assuming you've checked out and built a copy of element-web in ../element-web
ln -s ../element-web/webapp ./ ln -s ../element-web/webapp ./
@ -56,29 +58,32 @@ ln -s ../element-web/webapp ./
[TODO: add support for fetching develop builds, arbitrary URLs and arbitrary paths] [TODO: add support for fetching develop builds, arbitrary URLs and arbitrary paths]
Building # Building
========
## Native Build ## Native Build
TODO: List native pre-requisites TODO: List native pre-requisites
Optionally, [build the native modules](https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md), Optionally, [build the native modules](https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md),
which include support for searching in encrypted rooms and secure storage. Skipping this step is fine, you just won't have those features. which include support for searching in encrypted rooms and secure storage. Skipping this step is fine, you just won't have those features.
Then, run Then, run
``` ```
yarn run build yarn run build
``` ```
This will do a couple of things: This will do a couple of things:
* Run the `setversion` script to set the local package version to match whatever
version of Element you installed above. - Run the `setversion` script to set the local package version to match whatever
* Run electron-builder to build a package. The package built will match the operating system version of Element you installed above.
you're running the build process on. - Run electron-builder to build a package. The package built will match the operating system
you're running the build process on.
## Docker ## Docker
Alternatively, you can also build using docker, which will always produce the linux package: Alternatively, you can also build using docker, which will always produce the linux package:
``` ```
# Run this once to make the docker image # Run this once to make the docker image
yarn run docker:setup yarn run docker:setup
@ -91,9 +96,10 @@ yarn run docker:build
After running, the packages should be in `dist/`. After running, the packages should be in `dist/`.
Starting # Starting
========
If you'd just like to run the electron app locally for development: If you'd just like to run the electron app locally for development:
``` ```
# Install electron - we don't normally need electron itself as it's provided # Install electron - we don't normally need electron itself as it's provided
# by electron-builder when building packages # by electron-builder when building packages
@ -101,21 +107,22 @@ yarn add electron
yarn start yarn start
``` ```
Config # Config
======
If you'd like the packaged Element to have a configuration file, you can create a If you'd like the packaged Element to have a configuration file, you can create a
config directory and place `config.json` in there, then specify this directory config directory and place `config.json` in there, then specify this directory
with the `--cfgdir` option to `yarn run fetch`, eg: with the `--cfgdir` option to `yarn run fetch`, eg:
``` ```
mkdir myconfig mkdir myconfig
cp /path/to/my/config.json myconfig/ cp /path/to/my/config.json myconfig/
yarn run fetch --cfgdir myconfig yarn run fetch --cfgdir myconfig
``` ```
The config dir for the official Element app is in `element.io`. If you use this, The config dir for the official Element app is in `element.io`. If you use this,
your app will auto-update itself using builds from element.io. your app will auto-update itself using builds from element.io.
Profiles # Profiles
========
To run multiple instances of the desktop app for different accounts, you can To run multiple instances of the desktop app for different accounts, you can
launch the executable with the `--profile` argument followed by a unique launch the executable with the `--profile` argument followed by a unique
@ -125,20 +132,18 @@ not interfere with the default one.
Alternatively, a custom location for the profile data can be specified using the Alternatively, a custom location for the profile data can be specified using the
`--profile-dir` flag followed by the desired path. `--profile-dir` flag followed by the desired path.
User-specified config.json # User-specified config.json
==========================
+ `%APPDATA%\$NAME\config.json` on Windows - `%APPDATA%\$NAME\config.json` on Windows
+ `$XDG_CONFIG_HOME/$NAME/config.json` or `~/.config/$NAME/config.json` on Linux - `$XDG_CONFIG_HOME/$NAME/config.json` or `~/.config/$NAME/config.json` on Linux
+ `~/Library/Application Support/$NAME/config.json` on macOS - `~/Library/Application Support/$NAME/config.json` on macOS
In the paths above, `$NAME` is typically `Element`, unless you use `--profile In the paths above, `$NAME` is typically `Element`, unless you use `--profile
$PROFILE` in which case it becomes `Element-$PROFILE`, or it is using one of $PROFILE` in which case it becomes `Element-$PROFILE`, or it is using one of
the above created by a pre-1.7 install, in which case it will be `Riot` or the above created by a pre-1.7 install, in which case it will be `Riot` or
`Riot-$PROFILE`. `Riot-$PROFILE`.
Translations # Translations
==========================
To add a new translation, head to the [translating doc](https://github.com/vector-im/element-web/blob/develop/docs/translating.md). To add a new translation, head to the [translating doc](https://github.com/vector-im/element-web/blob/develop/docs/translating.md).
@ -146,9 +151,8 @@ For a developer guide, see the [translating dev doc](https://github.com/vector-i
[<img src="https://translate.element.io/widgets/element-desktop/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.element.io/engage/element-desktop/?utm_source=widget) [<img src="https://translate.element.io/widgets/element-desktop/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.element.io/engage/element-desktop/?utm_source=widget)
Report bugs & give feedback # Report bugs & give feedback
==========================
If you run into any bugs or have feedback you'd like to share, please let us know on GitHub. If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.
To help avoid duplicate issues, please [view existing issues](https://github.com/vector-im/element-web/issues?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc) first (and add a +1) or [create a new issue](https://github.com/vector-im/element-web/issues/new/choose) if you can't find it. Please note that this issue tracker is associated with the [element-web](https://github.com/vector-im/element-web) repo, but is also applied to the code in this repo as well. To help avoid duplicate issues, please [view existing issues](https://github.com/vector-im/element-web/issues?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc) first (and add a +1) or [create a new issue](https://github.com/vector-im/element-web/issues/new/choose) if you can't find it. Please note that this issue tracker is associated with the [element-web](https://github.com/vector-im/element-web) repo, but is also applied to the code in this repo as well.

View file

@ -1,6 +1,3 @@
module.exports = { module.exports = {
presets: [ presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"],
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
}; };

View file

@ -17,13 +17,14 @@ when releasing.
Install the pre-requisites for your system: Install the pre-requisites for your system:
* [Windows pre-requisites](https://github.com/vector-im/element-desktop/blob/develop/docs/windows-requirements.md) - [Windows pre-requisites](https://github.com/vector-im/element-desktop/blob/develop/docs/windows-requirements.md)
* Linux: TODO - Linux: TODO
* OS X: TODO - OS X: TODO
Then optionally, [add seshat and dependencies to support search in E2E rooms](#adding-seshat-for-search-in-e2e-encrypted-rooms). Then optionally, [add seshat and dependencies to support search in E2E rooms](#adding-seshat-for-search-in-e2e-encrypted-rooms).
Then, to build for an architecture selected automatically based on your system (recommended), run: Then, to build for an architecture selected automatically based on your system (recommended), run:
``` ```
yarn run build:native yarn run build:native
``` ```
@ -70,7 +71,7 @@ as usual using:
On Windows & macOS we always statically link libsqlcipher for it is not generally available. On Windows & macOS we always statically link libsqlcipher for it is not generally available.
On Linux by default we will use a system package, on debian & ubuntu this is `libsqlcipher0`, On Linux by default we will use a system package, on debian & ubuntu this is `libsqlcipher0`,
but this is problematic for some other packages. but this is problematic for some other packages.
By including `SQLCIPHER_STATIC=1` in the build environment, the build scripts will statically link sqlcipher, By including `SQLCIPHER_STATIC=1` in the build environment, the build scripts will statically link sqlcipher,
note that this will want a `libcrypto1.1` shared library available in the system. note that this will want a `libcrypto1.1` shared library available in the system.
@ -82,15 +83,19 @@ and https://github.com/vector-im/element-web/issues/20926.
### macOS ### macOS
On macOS, you can build universal native modules too: On macOS, you can build universal native modules too:
``` ```
yarn run build:native:universal yarn run build:native:universal
``` ```
...or you can build for a specific architecture: ...or you can build for a specific architecture:
``` ```
yarn run build:native --target x86_64-apple-darwin yarn run build:native --target x86_64-apple-darwin
``` ```
or or
``` ```
yarn run build:native --target aarch64-apple-darwin yarn run build:native --target aarch64-apple-darwin
``` ```
@ -105,10 +110,13 @@ yarn run build:universal
### Windows ### Windows
If you're on Windows, you can choose to build specifically for 32 or 64 bit: If you're on Windows, you can choose to build specifically for 32 or 64 bit:
``` ```
yarn run build:32 yarn run build:32
``` ```
or or
``` ```
yarn run build:64 yarn run build:64
``` ```
@ -144,6 +152,6 @@ The current set of native modules are stored in `.hak/hakModules`,
so you can use this to check what architecture is currently in place, eg: so you can use this to check what architecture is currently in place, eg:
``` ```
$ lipo -info .hak/hakModules/keytar/build/Release/keytar.node $ lipo -info .hak/hakModules/keytar/build/Release/keytar.node
Architectures in the fat file: .hak/hakModules/keytar/build/Release/keytar.node are: x86_64 arm64 Architectures in the fat file: .hak/hakModules/keytar/build/Release/keytar.node are: x86_64 arm64
``` ```

View file

@ -13,4 +13,3 @@ and can be served by any compatible Squirrel server, such as https://github.com/
On macOS the update mechanism used is [Squirrel.Mac](https://github.com/Squirrel/Squirrel.Mac) On macOS the update mechanism used is [Squirrel.Mac](https://github.com/Squirrel/Squirrel.Mac)
using the newer JSON format as documented [here](https://github.com/Squirrel/Squirrel.Mac#update-file-json-format). using the newer JSON format as documented [here](https://github.com/Squirrel/Squirrel.Mac#update-file-json-format).

View file

@ -4,23 +4,24 @@
If you want to build native modules, make sure that the following tools are installed on your system. If you want to build native modules, make sure that the following tools are installed on your system.
- [Git for Windows](https://git-scm.com/download/win) - [Git for Windows](https://git-scm.com/download/win)
- [Node 14](https://nodejs.org) - [Node 14](https://nodejs.org)
- [Python 3](https://www.python.org/downloads/) (if you type 'python' into command prompt it will offer to install it from the windows store) - [Python 3](https://www.python.org/downloads/) (if you type 'python' into command prompt it will offer to install it from the windows store)
- [Strawberry Perl](https://strawberryperl.com/) - [Strawberry Perl](https://strawberryperl.com/)
- [Rustup](https://rustup.rs/) - [Rustup](https://rustup.rs/)
- [NASM](https://www.nasm.us/) - [NASM](https://www.nasm.us/)
- [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019) with the following configuration: - [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019) with the following configuration:
- On the Workloads tab: - On the Workloads tab:
- Desktop & Mobile -> C++ build tools - Desktop & Mobile -> C++ build tools
- On the Individual components tab: - On the Individual components tab:
- MSVC VS 2019 C++ build tools - MSVC VS 2019 C++ build tools
- Windows 10 SDK (latest version available) - Windows 10 SDK (latest version available)
- C++ CMake tools for Windows - C++ CMake tools for Windows
Once installed make sure all those utilities are accessible in your `PATH`. Once installed make sure all those utilities are accessible in your `PATH`.
If you want to be able to build x86 targets from an x64 host install the right toolchain: If you want to be able to build x86 targets from an x64 host install the right toolchain:
```cmd ```cmd
rustup toolchain install stable-i686-pc-windows-msvc rustup toolchain install stable-i686-pc-windows-msvc
rustup target add i686-pc-windows-msvc rustup target add i686-pc-windows-msvc

View file

@ -16,11 +16,7 @@
"uisi_autorageshake_app": "element-auto-uisi", "uisi_autorageshake_app": "element-auto-uisi",
"showLabsSettings": true, "showLabsSettings": true,
"roomDirectory": { "roomDirectory": {
"servers": [ "servers": ["matrix.org", "gitter.im", "libera.chat"]
"matrix.org",
"gitter.im",
"libera.chat"
]
}, },
"enable_presence_by_hs_url": { "enable_presence_by_hs_url": {
"https://matrix.org": false, "https://matrix.org": false,

View file

@ -15,11 +15,7 @@
"bug_report_endpoint_url": "https://element.io/bugreports/submit", "bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi", "uisi_autorageshake_app": "element-auto-uisi",
"roomDirectory": { "roomDirectory": {
"servers": [ "servers": ["matrix.org", "gitter.im", "libera.chat"]
"matrix.org",
"gitter.im",
"libera.chat"
]
}, },
"showLabsSettings": false, "showLabsSettings": false,
"enable_presence_by_hs_url": { "enable_presence_by_hs_url": {

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import childProcess from 'child_process'; import childProcess from "child_process";
import HakEnv from '../../scripts/hak/hakEnv'; import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from '../../scripts/hak/dep'; import { DependencyInfo } from "../../scripts/hak/dep";
export default async function buildKeytar(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function buildKeytar(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const env = hakEnv.makeGypEnv(); const env = hakEnv.makeGypEnv();
@ -26,15 +26,15 @@ export default async function buildKeytar(hakEnv: HakEnv, moduleInfo: Dependency
console.log("Running yarn with env", env); console.log("Running yarn with env", env);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn(
path.join(moduleInfo.nodeModuleBinDir, 'node-gyp' + (hakEnv.isWin() ? '.cmd' : '')), path.join(moduleInfo.nodeModuleBinDir, "node-gyp" + (hakEnv.isWin() ? ".cmd" : "")),
['rebuild'], ["rebuild"],
{ {
cwd: moduleInfo.moduleBuildDir, cwd: moduleInfo.moduleBuildDir,
env, env,
stdio: 'inherit', stdio: "inherit",
}, },
); );
proc.on('exit', (code) => { proc.on("exit", (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });

View file

@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import childProcess from 'child_process'; import childProcess from "child_process";
import HakEnv from '../../scripts/hak/hakEnv'; import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from '../../scripts/hak/dep'; import { DependencyInfo } from "../../scripts/hak/dep";
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const tools = [['python', '--version']]; // node-gyp uses python for reasons beyond comprehension const tools = [["python", "--version"]]; // node-gyp uses python for reasons beyond comprehension
for (const tool of tools) { for (const tool of tools) {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(tool[0], tool.slice(1), { const proc = childProcess.spawn(tool[0], tool.slice(1), {
stdio: ['ignore'], stdio: ["ignore"],
}); });
proc.on('exit', (code) => { proc.on("exit", (code) => {
if (code !== 0) { if (code !== 0) {
reject("Can't find " + tool); reject("Can't find " + tool);
} else { } else {

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import childProcess from 'child_process'; import childProcess from "child_process";
import mkdirp from 'mkdirp'; import mkdirp from "mkdirp";
import fsExtra from 'fs-extra'; import fsExtra from "fs-extra";
import HakEnv from '../../scripts/hak/hakEnv'; import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from '../../scripts/hak/dep'; import { DependencyInfo } from "../../scripts/hak/dep";
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (hakEnv.isWin()) { if (hakEnv.isWin()) {
await buildOpenSslWin(hakEnv, moduleInfo); await buildOpenSslWin(hakEnv, moduleInfo);
await buildSqlCipherWin(hakEnv, moduleInfo); await buildSqlCipherWin(hakEnv, moduleInfo);
@ -36,99 +36,91 @@ async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Prom
const version = moduleInfo.cfg.dependencies.openssl; const version = moduleInfo.cfg.dependencies.openssl;
const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`); const openSslDir = path.join(moduleInfo.moduleTargetDotHakDir, `openssl-${version}`);
const openSslArch = hakEnv.getTargetArch() === 'x64' ? 'VC-WIN64A' : 'VC-WIN32'; const openSslArch = hakEnv.getTargetArch() === "x64" ? "VC-WIN64A" : "VC-WIN32";
console.log("Building openssl in " + openSslDir); console.log("Building openssl in " + openSslDir);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn(
'perl', "perl",
[ [
'Configure', "Configure",
'--prefix=' + moduleInfo.depPrefix, "--prefix=" + moduleInfo.depPrefix,
// sqlcipher only uses about a tiny part of openssl. We link statically // sqlcipher only uses about a tiny part of openssl. We link statically
// so will only pull in the symbols we use, but we may as well turn off // so will only pull in the symbols we use, but we may as well turn off
// as much as possible to save on build time. // as much as possible to save on build time.
'no-afalgeng', "no-afalgeng",
'no-capieng', "no-capieng",
'no-cms', "no-cms",
'no-ct', "no-ct",
'no-deprecated', "no-deprecated",
'no-dgram', "no-dgram",
'no-dso', "no-dso",
'no-ec', "no-ec",
'no-ec2m', "no-ec2m",
'no-gost', "no-gost",
'no-nextprotoneg', "no-nextprotoneg",
'no-ocsp', "no-ocsp",
'no-sock', "no-sock",
'no-srp', "no-srp",
'no-srtp', "no-srtp",
'no-tests', "no-tests",
'no-ssl', "no-ssl",
'no-tls', "no-tls",
'no-dtls', "no-dtls",
'no-shared', "no-shared",
'no-aria', "no-aria",
'no-camellia', "no-camellia",
'no-cast', "no-cast",
'no-chacha', "no-chacha",
'no-cmac', "no-cmac",
'no-des', "no-des",
'no-dh', "no-dh",
'no-dsa', "no-dsa",
'no-ecdh', "no-ecdh",
'no-ecdsa', "no-ecdsa",
'no-idea', "no-idea",
'no-md4', "no-md4",
'no-mdc2', "no-mdc2",
'no-ocb', "no-ocb",
'no-poly1305', "no-poly1305",
'no-rc2', "no-rc2",
'no-rc4', "no-rc4",
'no-rmd160', "no-rmd160",
'no-scrypt', "no-scrypt",
'no-seed', "no-seed",
'no-siphash', "no-siphash",
'no-sm2', "no-sm2",
'no-sm3', "no-sm3",
'no-sm4', "no-sm4",
'no-whirlpool', "no-whirlpool",
openSslArch, openSslArch,
], ],
{ {
cwd: openSslDir, cwd: openSslDir,
stdio: 'inherit', stdio: "inherit",
}, },
); );
proc.on('exit', (code) => { proc.on("exit", (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn("nmake", ["build_libs"], {
'nmake', cwd: openSslDir,
['build_libs'], stdio: "inherit",
{ });
cwd: openSslDir, proc.on("exit", (code) => {
stdio: 'inherit',
},
);
proc.on('exit', (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn("nmake", ["install_dev"], {
'nmake', cwd: openSslDir,
['install_dev'], stdio: "inherit",
{ });
cwd: openSslDir, proc.on("exit", (code) => {
stdio: 'inherit',
},
);
proc.on('exit', (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
@ -137,38 +129,28 @@ async function buildOpenSslWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Prom
async function buildSqlCipherWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { async function buildSqlCipherWin(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const version = moduleInfo.cfg.dependencies.sqlcipher; const version = moduleInfo.cfg.dependencies.sqlcipher;
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`); const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
const buildDir = path.join(sqlCipherDir, 'bld'); const buildDir = path.join(sqlCipherDir, "bld");
await mkdirp(buildDir); await mkdirp(buildDir);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn("nmake", ["/f", path.join("..", "Makefile.msc"), "libsqlite3.lib", "TOP=.."], {
'nmake', cwd: buildDir,
['/f', path.join('..', 'Makefile.msc'), 'libsqlite3.lib', 'TOP=..'], stdio: "inherit",
{ env: Object.assign({}, process.env, {
cwd: buildDir, CCOPTS: "-DSQLITE_HAS_CODEC -I" + path.join(moduleInfo.depPrefix, "include"),
stdio: 'inherit', LTLIBPATHS: "/LIBPATH:" + path.join(moduleInfo.depPrefix, "lib"),
env: Object.assign({}, process.env, { LTLIBS: "libcrypto.lib",
CCOPTS: "-DSQLITE_HAS_CODEC -I" + path.join(moduleInfo.depPrefix, 'include'), }),
LTLIBPATHS: "/LIBPATH:" + path.join(moduleInfo.depPrefix, 'lib'), });
LTLIBS: "libcrypto.lib", proc.on("exit", (code) => {
}),
},
);
proc.on('exit', (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
await fsExtra.copy( await fsExtra.copy(path.join(buildDir, "libsqlite3.lib"), path.join(moduleInfo.depPrefix, "lib", "sqlcipher.lib"));
path.join(buildDir, 'libsqlite3.lib'),
path.join(moduleInfo.depPrefix, 'lib', 'sqlcipher.lib'),
);
await fsExtra.copy( await fsExtra.copy(path.join(buildDir, "sqlite3.h"), path.join(moduleInfo.depPrefix, "include", "sqlcipher.h"));
path.join(buildDir, 'sqlite3.h'),
path.join(moduleInfo.depPrefix, 'include', 'sqlcipher.h'),
);
} }
async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
@ -176,21 +158,21 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo): P
const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`); const sqlCipherDir = path.join(moduleInfo.moduleTargetDotHakDir, `sqlcipher-${version}`);
const args = [ const args = [
'--prefix=' + moduleInfo.depPrefix + '', "--prefix=" + moduleInfo.depPrefix + "",
'--enable-tempstore=yes', "--enable-tempstore=yes",
'--enable-shared=no', "--enable-shared=no",
'--enable-tcl=no', "--enable-tcl=no",
]; ];
if (hakEnv.isMac()) { if (hakEnv.isMac()) {
args.push('--with-crypto-lib=commoncrypto'); args.push("--with-crypto-lib=commoncrypto");
} }
if (hakEnv.wantsStaticSqlCipherUnix()) { if (hakEnv.wantsStaticSqlCipherUnix()) {
args.push('--enable-tcl=no'); args.push("--enable-tcl=no");
if (hakEnv.isLinux()) { if (hakEnv.isLinux()) {
args.push('--with-pic=yes'); args.push("--with-pic=yes");
} }
} }
@ -201,9 +183,7 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo): P
args.push(`--host=${hakEnv.getTargetId()}`); args.push(`--host=${hakEnv.getTargetId()}`);
} }
const cflags = [ const cflags = ["-DSQLITE_HAS_CODEC"];
'-DSQLITE_HAS_CODEC',
];
if (!hakEnv.isHost()) { if (!hakEnv.isHost()) {
// `clang` uses more logical option naming. // `clang` uses more logical option naming.
@ -211,58 +191,46 @@ async function buildSqlCipherUnix(hakEnv: HakEnv, moduleInfo: DependencyInfo): P
} }
if (cflags.length) { if (cflags.length) {
args.push(`CFLAGS=${cflags.join(' ')}`); args.push(`CFLAGS=${cflags.join(" ")}`);
} }
const ldflags: string[] = []; const ldflags: string[] = [];
if (hakEnv.isMac()) { if (hakEnv.isMac()) {
ldflags.push('-framework Security'); ldflags.push("-framework Security");
ldflags.push('-framework Foundation'); ldflags.push("-framework Foundation");
} }
if (ldflags.length) { if (ldflags.length) {
args.push(`LDFLAGS=${ldflags.join(' ')}`); args.push(`LDFLAGS=${ldflags.join(" ")}`);
} }
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn(path.join(sqlCipherDir, "configure"), args, {
path.join(sqlCipherDir, 'configure'), cwd: sqlCipherDir,
args, stdio: "inherit",
{ });
cwd: sqlCipherDir, proc.on("exit", (code) => {
stdio: 'inherit',
},
);
proc.on('exit', (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn("make", [], {
'make', cwd: sqlCipherDir,
[], stdio: "inherit",
{ });
cwd: sqlCipherDir, proc.on("exit", (code) => {
stdio: 'inherit',
},
);
proc.on('exit', (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn("make", ["install"], {
'make', cwd: sqlCipherDir,
['install'], stdio: "inherit",
{ });
cwd: sqlCipherDir, proc.on("exit", (code) => {
stdio: 'inherit',
},
);
proc.on('exit', (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
@ -277,8 +245,8 @@ async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo): Pr
if (!hakEnv.isLinux() || hakEnv.wantsStaticSqlCipherUnix()) { if (!hakEnv.isLinux() || hakEnv.wantsStaticSqlCipherUnix()) {
Object.assign(env, { Object.assign(env, {
SQLCIPHER_STATIC: 1, SQLCIPHER_STATIC: 1,
SQLCIPHER_LIB_DIR: path.join(moduleInfo.depPrefix, 'lib'), SQLCIPHER_LIB_DIR: path.join(moduleInfo.depPrefix, "lib"),
SQLCIPHER_INCLUDE_DIR: path.join(moduleInfo.depPrefix, 'include'), SQLCIPHER_INCLUDE_DIR: path.join(moduleInfo.depPrefix, "include"),
}); });
} }
@ -298,11 +266,11 @@ async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo): Pr
// --exclude-libs ALL // --exclude-libs ALL
// Prevent symbols from being exported by any archive libraries. // Prevent symbols from being exported by any archive libraries.
// Reduces output filesize and prevents being dynamically linked against. // Reduces output filesize and prevents being dynamically linked against.
env.RUSTFLAGS = '-Clink-arg=-Wl,-Bsymbolic -Clink-arg=-Wl,--exclude-libs,ALL'; env.RUSTFLAGS = "-Clink-arg=-Wl,-Bsymbolic -Clink-arg=-Wl,--exclude-libs,ALL";
} }
if (hakEnv.isWin()) { if (hakEnv.isWin()) {
env.RUSTFLAGS = '-Ctarget-feature=+crt-static -Clink-args=libcrypto.lib'; env.RUSTFLAGS = "-Ctarget-feature=+crt-static -Clink-args=libcrypto.lib";
// Note that in general, you can specify targets in Rust without having to have // Note that in general, you can specify targets in Rust without having to have
// the matching toolchain, however for this, cargo gets confused when building // the matching toolchain, however for this, cargo gets confused when building
// the build scripts since they run on the host, but vcvarsall.bat sets the c // the build scripts since they run on the host, but vcvarsall.bat sets the c
@ -318,15 +286,15 @@ async function buildMatrixSeshat(hakEnv: HakEnv, moduleInfo: DependencyInfo): Pr
console.log("Running neon with env", env); console.log("Running neon with env", env);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn(
path.join(moduleInfo.nodeModuleBinDir, 'neon' + (hakEnv.isWin() ? '.cmd' : '')), path.join(moduleInfo.nodeModuleBinDir, "neon" + (hakEnv.isWin() ? ".cmd" : "")),
['build', '--release'], ["build", "--release"],
{ {
cwd: moduleInfo.moduleBuildDir, cwd: moduleInfo.moduleBuildDir,
env, env,
stdio: 'inherit', stdio: "inherit",
}, },
); );
proc.on('exit', (code) => { proc.on("exit", (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });

View file

@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import childProcess from 'child_process'; import childProcess from "child_process";
import fsProm from 'fs/promises'; import fsProm from "fs/promises";
import HakEnv from '../../scripts/hak/hakEnv'; import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from '../../scripts/hak/dep'; import { DependencyInfo } from "../../scripts/hak/dep";
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (hakEnv.wantsStaticSqlCipher()) { if (hakEnv.wantsStaticSqlCipher()) {
// of course tcl doesn't have a --version // of course tcl doesn't have a --version
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn('tclsh', [], { const proc = childProcess.spawn("tclsh", [], {
stdio: ['pipe', 'ignore', 'ignore'], stdio: ["pipe", "ignore", "ignore"],
}); });
proc.on('exit', (code) => { proc.on("exit", (code) => {
if (code !== 0) { if (code !== 0) {
reject("Can't find tclsh - have you installed TCL?"); reject("Can't find tclsh - have you installed TCL?");
} else { } else {
@ -39,24 +39,24 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
} }
const tools = [ const tools = [
['rustc', '--version'], ["rustc", "--version"],
['python', '--version'], // node-gyp uses python for reasons beyond comprehension ["python", "--version"], // node-gyp uses python for reasons beyond comprehension
]; ];
if (hakEnv.isWin()) { if (hakEnv.isWin()) {
tools.push(['perl', '--version']); // for openssl configure tools.push(["perl", "--version"]); // for openssl configure
tools.push(['nasm', '-v']); // for openssl building tools.push(["nasm", "-v"]); // for openssl building
tools.push(['patch', '--version']); // to patch sqlcipher Makefile.msc tools.push(["patch", "--version"]); // to patch sqlcipher Makefile.msc
tools.push(['nmake', '/?']); tools.push(["nmake", "/?"]);
} else { } else {
tools.push(['make', '--version']); tools.push(["make", "--version"]);
} }
for (const tool of tools) { for (const tool of tools) {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(tool[0], tool.slice(1), { const proc = childProcess.spawn(tool[0], tool.slice(1), {
stdio: ['ignore'], stdio: ["ignore"],
}); });
proc.on('exit', (code) => { proc.on("exit", (code) => {
if (code !== 0) { if (code !== 0) {
reject("Can't find " + tool); reject("Can't find " + tool);
} else { } else {
@ -68,19 +68,24 @@ export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promi
// Ensure Rust target exists (nb. we avoid depending on rustup) // Ensure Rust target exists (nb. we avoid depending on rustup)
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const rustc = childProcess.execFile('rustc', [ const rustc = childProcess.execFile(
'--target', hakEnv.getTargetId(), '-o', 'tmp', '-', "rustc",
], (err, out) => { ["--target", hakEnv.getTargetId(), "-o", "tmp", "-"],
if (err) { (err, out) => {
reject( if (err) {
"rustc can't build for target " + hakEnv.getTargetId() + reject(
": ensure target is installed via `rustup target add " + hakEnv.getTargetId() + "` " + "rustc can't build for target " +
"or your package manager if not using `rustup`", hakEnv.getTargetId() +
); ": ensure target is installed via `rustup target add " +
} hakEnv.getTargetId() +
fsProm.unlink('tmp').then(resolve); "` " +
}); "or your package manager if not using `rustup`",
rustc.stdin!.write('fn main() {}'); );
}
fsProm.unlink("tmp").then(resolve);
},
);
rustc.stdin!.write("fn main() {}");
rustc.stdin!.end(); rustc.stdin!.end();
}); });
} }

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import childProcess from 'child_process'; import childProcess from "child_process";
import fs from 'fs'; import fs from "fs";
import fsProm from 'fs/promises'; import fsProm from "fs/promises";
import tar from 'tar'; import tar from "tar";
import fetch from 'node-fetch'; import fetch from "node-fetch";
import { promises as stream } from "stream"; import { promises as stream } from "stream";
import HakEnv from '../../scripts/hak/hakEnv'; import HakEnv from "../../scripts/hak/hakEnv";
import { DependencyInfo } from '../../scripts/hak/dep'; import { DependencyInfo } from "../../scripts/hak/dep";
async function download(url: string, filename: string): Promise<void> { async function download(url: string, filename: string): Promise<void> {
const resp = await fetch(url); const resp = await fetch(url);
@ -32,7 +32,7 @@ async function download(url: string, filename: string): Promise<void> {
await stream.pipeline(resp.body, fs.createWriteStream(filename)); await stream.pipeline(resp.body, fs.createWriteStream(filename));
} }
export default async function(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (hakEnv.wantsStaticSqlCipher()) { if (hakEnv.wantsStaticSqlCipher()) {
await getSqlCipher(hakEnv, moduleInfo); await getSqlCipher(hakEnv, moduleInfo);
} }
@ -83,15 +83,11 @@ async function getSqlCipher(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const readStream = fs.createReadStream(patchFile); const readStream = fs.createReadStream(patchFile);
const proc = childProcess.spawn( const proc = childProcess.spawn("patch", ["-p1"], {
'patch', cwd: sqlCipherDir,
['-p1'], stdio: ["pipe", "inherit", "inherit"],
{ });
cwd: sqlCipherDir, proc.on("exit", (code) => {
stdio: ['pipe', 'inherit', 'inherit'],
},
);
proc.on('exit', (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
readStream.pipe(proc.stdin); readStream.pipe(proc.stdin);

View file

@ -1,19 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"target": "es2016", "target": "es2016",
"sourceMap": false, "sourceMap": false,
"strict": true, "strict": true,
"lib": [ "lib": ["es2019"]
"es2019", },
] "include": ["../scripts/@types/*.d.ts", "./**/*.ts"],
}, "ts-node": {
"include": [ "transpileOnly": true
"../scripts/@types/*.d.ts", }
"./**/*.ts"
],
"ts-node": {
"transpileOnly": true
}
} }

View file

@ -79,8 +79,9 @@
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"eslint": "^8.26.0", "eslint": "^8.26.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.8.0", "eslint-plugin-matrix-org": "^0.9.0",
"eslint-plugin-unicorn": "^45.0.0", "eslint-plugin-unicorn": "^45.0.0",
"expect-playwright": "^0.8.0", "expect-playwright": "^0.8.0",
"find-npm-prefix": "^1.0.2", "find-npm-prefix": "^1.0.2",

View file

@ -1,154 +1,139 @@
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 100; font-weight: 100;
font-display: swap; font-display: swap;
src: url("Inter-Thin.woff2?v=3.19") format("woff2"), src: url("Inter-Thin.woff2?v=3.19") format("woff2"), url("Inter-Thin.woff?v=3.19") format("woff");
url("Inter-Thin.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 100; font-weight: 100;
font-display: swap; font-display: swap;
src: url("Inter-ThinItalic.woff2?v=3.19") format("woff2"), src: url("Inter-ThinItalic.woff2?v=3.19") format("woff2"), url("Inter-ThinItalic.woff?v=3.19") format("woff");
url("Inter-ThinItalic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 200; font-weight: 200;
font-display: swap; font-display: swap;
src: url("Inter-ExtraLight.woff2?v=3.19") format("woff2"), src: url("Inter-ExtraLight.woff2?v=3.19") format("woff2"), url("Inter-ExtraLight.woff?v=3.19") format("woff");
url("Inter-ExtraLight.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 200; font-weight: 200;
font-display: swap; font-display: swap;
src: url("Inter-ExtraLightItalic.woff2?v=3.19") format("woff2"), src: url("Inter-ExtraLightItalic.woff2?v=3.19") format("woff2"),
url("Inter-ExtraLightItalic.woff?v=3.19") format("woff"); url("Inter-ExtraLightItalic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url("Inter-Light.woff2?v=3.19") format("woff2"), src: url("Inter-Light.woff2?v=3.19") format("woff2"), url("Inter-Light.woff?v=3.19") format("woff");
url("Inter-Light.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url("Inter-LightItalic.woff2?v=3.19") format("woff2"), src: url("Inter-LightItalic.woff2?v=3.19") format("woff2"), url("Inter-LightItalic.woff?v=3.19") format("woff");
url("Inter-LightItalic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url("Inter-Regular.woff2?v=3.19") format("woff2"), src: url("Inter-Regular.woff2?v=3.19") format("woff2"), url("Inter-Regular.woff?v=3.19") format("woff");
url("Inter-Regular.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url("Inter-Italic.woff2?v=3.19") format("woff2"), src: url("Inter-Italic.woff2?v=3.19") format("woff2"), url("Inter-Italic.woff?v=3.19") format("woff");
url("Inter-Italic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url("Inter-Medium.woff2?v=3.19") format("woff2"), src: url("Inter-Medium.woff2?v=3.19") format("woff2"), url("Inter-Medium.woff?v=3.19") format("woff");
url("Inter-Medium.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url("Inter-MediumItalic.woff2?v=3.19") format("woff2"), src: url("Inter-MediumItalic.woff2?v=3.19") format("woff2"), url("Inter-MediumItalic.woff?v=3.19") format("woff");
url("Inter-MediumItalic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url("Inter-SemiBold.woff2?v=3.19") format("woff2"), src: url("Inter-SemiBold.woff2?v=3.19") format("woff2"), url("Inter-SemiBold.woff?v=3.19") format("woff");
url("Inter-SemiBold.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url("Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"), src: url("Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-SemiBoldItalic.woff?v=3.19") format("woff"); url("Inter-SemiBoldItalic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url("Inter-Bold.woff2?v=3.19") format("woff2"), src: url("Inter-Bold.woff2?v=3.19") format("woff2"), url("Inter-Bold.woff?v=3.19") format("woff");
url("Inter-Bold.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url("Inter-BoldItalic.woff2?v=3.19") format("woff2"), src: url("Inter-BoldItalic.woff2?v=3.19") format("woff2"), url("Inter-BoldItalic.woff?v=3.19") format("woff");
url("Inter-BoldItalic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
font-display: swap; font-display: swap;
src: url("Inter-ExtraBold.woff2?v=3.19") format("woff2"), src: url("Inter-ExtraBold.woff2?v=3.19") format("woff2"), url("Inter-ExtraBold.woff?v=3.19") format("woff");
url("Inter-ExtraBold.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
font-display: swap; font-display: swap;
src: url("Inter-ExtraBoldItalic.woff2?v=3.19") format("woff2"), src: url("Inter-ExtraBoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-ExtraBoldItalic.woff?v=3.19") format("woff"); url("Inter-ExtraBoldItalic.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: normal; font-style: normal;
font-weight: 900; font-weight: 900;
font-display: swap; font-display: swap;
src: url("Inter-Black.woff2?v=3.19") format("woff2"), src: url("Inter-Black.woff2?v=3.19") format("woff2"), url("Inter-Black.woff?v=3.19") format("woff");
url("Inter-Black.woff?v=3.19") format("woff");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: "Inter";
font-style: italic; font-style: italic;
font-weight: 900; font-weight: 900;
font-display: swap; font-display: swap;
src: url("Inter-BlackItalic.woff2?v=3.19") format("woff2"), src: url("Inter-BlackItalic.woff2?v=3.19") format("woff2"), url("Inter-BlackItalic.woff?v=3.19") format("woff");
url("Inter-BlackItalic.woff?v=3.19") format("woff");
} }
/* ------------------------------------------------------- /* -------------------------------------------------------
@ -161,21 +146,20 @@ Usage:
} }
*/ */
@font-face { @font-face {
font-family: 'Inter var'; font-family: "Inter var";
font-weight: 100 900; font-weight: 100 900;
font-display: swap; font-display: swap;
font-style: normal; font-style: normal;
src: url("Inter-roman.var.woff2?v=3.19") format("woff2"); src: url("Inter-roman.var.woff2?v=3.19") format("woff2");
} }
@font-face { @font-face {
font-family: 'Inter var'; font-family: "Inter var";
font-weight: 100 900; font-weight: 100 900;
font-display: swap; font-display: swap;
font-style: italic; font-style: italic;
src: url("Inter-italic.var.woff2?v=3.19") format("woff2"); src: url("Inter-italic.var.woff2?v=3.19") format("woff2");
} }
/* -------------------------------------------------------------------------- /* --------------------------------------------------------------------------
[EXPERIMENTAL] Multi-axis, single variable font. [EXPERIMENTAL] Multi-axis, single variable font.
@ -190,9 +174,9 @@ explicitly, e.g.
*/ */
@font-face { @font-face {
font-family: 'Inter var experimental'; font-family: "Inter var experimental";
font-weight: 100 900; font-weight: 100 900;
font-display: swap; font-display: swap;
font-style: oblique 0deg 10deg; font-style: oblique 0deg 10deg;
src: url("Inter.var.woff2?v=3.19") format("woff2"); src: url("Inter.var.woff2?v=3.19") format("woff2");
} }

View file

@ -5,13 +5,16 @@
* hosted on GitHub, https://GitHub.com/Naereen/Nginx-Fancyindex-Theme * hosted on GitHub, https://GitHub.com/Naereen/Nginx-Fancyindex-Theme
*/ */
@import url('./fonts/inter.css'); @import url("./fonts/inter.css");
* { font-family: 'Inter', sans-serif; } * {
@supports (font-variation-settings: normal) { font-family: "Inter", sans-serif;
* { font-family: 'Inter var', sans-serif; } }
@supports (font-variation-settings: normal) {
* {
font-family: "Inter var", sans-serif;
}
} }
* { * {
margin: 0; margin: 0;
@ -22,14 +25,14 @@
} }
html { html {
color: #17191C; color: #17191c;
font-weight: 400; font-weight: 400;
font-size: 1em; font-size: 1em;
line-height: 1.6em; line-height: 1.6em;
} }
body { body {
background-color: #F4F6FA; background-color: #f4f6fa;
margin: 0 auto; margin: 0 auto;
padding: 100px 20px 20px; padding: 100px 20px 20px;
max-width: 800px; max-width: 800px;
@ -52,9 +55,11 @@ a {
text-decoration: underline; text-decoration: underline;
} }
a:hover { a:hover {
color: #0DBD8B; color: #0dbd8b;
} }
a.clear, a.clear:link, a.clear:visited { a.clear,
a.clear:link,
a.clear:visited {
color: #666; color: #666;
padding: 2px 0; padding: 2px 0;
font-weight: 400; font-weight: 400;
@ -74,7 +79,7 @@ a.clear, a.clear:link, a.clear:visited {
input { input {
vertical-align: middle; vertical-align: middle;
*overflow: visible; *overflow: visible;
font-family: 'Open Sans', sans-serif; font-family: "Open Sans", sans-serif;
font-weight: 300; font-weight: 300;
display: inline-block; display: inline-block;
height: 20px; height: 20px;
@ -89,20 +94,20 @@ input {
width: 196px; width: 196px;
background-color: #fff; background-color: #fff;
border: 1px solid #ccc; border: 1px solid #ccc;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border linear .2s,box-shadow linear .2s; -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-moz-transition: border linear .2s,box-shadow linear .2s; -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
-o-transition: border linear .2s,box-shadow linear .2s; -o-transition: border linear 0.2s, box-shadow linear 0.2s;
transition: border linear .2s,box-shadow linear .2s; transition: border linear 0.2s, box-shadow linear 0.2s;
} }
input:focus { input:focus {
outline: 0; outline: 0;
border-color: rgba(0,0,0,0.8); border-color: rgba(0, 0, 0, 0.8);
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(0,0,0,0.6); -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 0, 0, 0.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(0,0,0,0.6); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 0, 0, 0.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(0,0,0,0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 0, 0, 0.6);
} }
input::-moz-focus-inner { input::-moz-focus-inner {
padding: 0; padding: 0;
@ -128,13 +133,12 @@ tr td:first-of-type {
padding-right: 10px; padding-right: 10px;
} }
tr.parent a { tr.parent a {
color: #9099A3; color: #9099a3;
} }
th { th {
text-align: left; text-align: left;
font-size: .75em; font-size: 0.75em;
padding-right: 20px; padding-right: 20px;
} }
th + th { th + th {
@ -157,7 +161,8 @@ td {
-o-transition: background 300ms ease-in; -o-transition: background 300ms ease-in;
transition: background 300ms ease-in; transition: background 300ms ease-in;
} }
td:last-child,th:last-child { td:last-child,
th:last-child {
text-align: right; text-align: right;
padding-right: 0; padding-right: 0;
} }
@ -187,11 +192,11 @@ td a {
padding: 0; padding: 0;
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
background-color: #FFF; background-color: #fff;
} }
.nav li a { .nav li a {
color: #17191C; color: #17191c;
display: block; display: block;
padding: 20px 20px; padding: 20px 20px;
text-decoration: none; text-decoration: none;
@ -199,7 +204,7 @@ td a {
.nav li a:hover, .nav li a:hover,
.nav .menu-btn:hover { .nav .menu-btn:hover {
color: #0DBD8B; color: #0dbd8b;
} }
.nav .logo { .nav .logo {
@ -216,7 +221,7 @@ td a {
.nav .menu { .nav .menu {
clear: both; clear: both;
max-height: 0; max-height: 0;
transition: max-height .2s ease-out; transition: max-height 0.2s ease-out;
} }
/* menu icon */ /* menu icon */
@ -237,18 +242,18 @@ td a {
margin-top: 6px; margin-top: 6px;
margin-bottom: 4px; margin-bottom: 4px;
position: relative; position: relative;
transition: background .2s ease-out; transition: background 0.2s ease-out;
width: 18px; width: 18px;
} }
.nav .menu-icon .navicon:before, .nav .menu-icon .navicon:before,
.nav .menu-icon .navicon:after { .nav .menu-icon .navicon:after {
background: #17191C; background: #17191c;
content: ''; content: "";
display: block; display: block;
height: 100%; height: 100%;
position: absolute; position: absolute;
transition: all .2s ease-out; transition: all 0.2s ease-out;
width: 100%; width: 100%;
} }
@ -303,13 +308,13 @@ td a {
margin-left: 20px; margin-left: 20px;
padding: 12px 20px; padding: 12px 20px;
border-radius: 100px; border-radius: 100px;
background-color: #0DBD8B; background-color: #0dbd8b;
color:#FFF; color: #fff;
} }
.nav .primary:hover { .nav .primary:hover {
background-color: #099970; background-color: #099970;
color:#FFF; color: #fff;
} }
.nav .menu { .nav .menu {
@ -324,10 +329,10 @@ td a {
footer { footer {
margin-top: 40px; margin-top: 40px;
font-size:0.8em; font-size: 0.8em;
text-align:center; text-align: center;
} }
footer a { footer a {
color:#03b381; color: #03b381;
} }

View file

@ -20,16 +20,16 @@ function errCheck(err?: Error): void {
} }
const I18N_BASE_PATH = "src/i18n/strings/"; const I18N_BASE_PATH = "src/i18n/strings/";
const INCLUDE_LANGS = fs.readdirSync(I18N_BASE_PATH).filter(fn => fn.endsWith(".json")); const INCLUDE_LANGS = fs.readdirSync(I18N_BASE_PATH).filter((fn) => fn.endsWith(".json"));
// Ensure lib, lib/i18n and lib/i18n/strings all exist // Ensure lib, lib/i18n and lib/i18n/strings all exist
fs.mkdirSync('lib/i18n/strings', { recursive: true }); fs.mkdirSync("lib/i18n/strings", { recursive: true });
type Translations = Record<string, Record<string, string> | string>; type Translations = Record<string, Record<string, string> | string>;
function genLangFile(file: string, dest: string): void { function genLangFile(file: string, dest: string): void {
const inTrs: Record<string, string> = {}; const inTrs: Record<string, string> = {};
[file].forEach(function(f) { [file].forEach(function (f) {
if (fs.existsSync(f)) { if (fs.existsSync(f)) {
try { try {
Object.assign(inTrs, JSON.parse(fs.readFileSync(f).toString())); Object.assign(inTrs, JSON.parse(fs.readFileSync(f).toString()));
@ -68,7 +68,7 @@ function weblateToCounterpart(inTrs: Record<string, string>): Translations {
const outTrs: Translations = {}; const outTrs: Translations = {};
for (const key of Object.keys(inTrs)) { for (const key of Object.keys(inTrs)) {
const keyParts = key.split('|', 2); const keyParts = key.split("|", 2);
if (keyParts.length === 2) { if (keyParts.length === 2) {
let obj = outTrs[keyParts[0]]; let obj = outTrs[keyParts[0]];
if (obj === undefined) { if (obj === undefined) {
@ -77,7 +77,7 @@ function weblateToCounterpart(inTrs: Record<string, string>): Translations {
// This is a transitional edge case if a string went from singular to pluralised and both still remain // This is a transitional edge case if a string went from singular to pluralised and both still remain
// in the translation json file. Use the singular translation as `other` and merge pluralisation atop. // in the translation json file. Use the singular translation as `other` and merge pluralisation atop.
obj = outTrs[keyParts[0]] = { obj = outTrs[keyParts[0]] = {
"other": inTrs[key], other: inTrs[key],
}; };
console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]); console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]);
} }
@ -108,10 +108,7 @@ function watchLanguage(file: string, dest: string): void {
}, 500); }, 500);
}; };
chokidar.watch(file) chokidar.watch(file).on("add", makeLang).on("change", makeLang).on("error", errCheck);
.on('add', makeLang)
.on('change', makeLang)
.on('error', errCheck);
} }
// language resources // language resources
@ -121,5 +118,5 @@ INCLUDE_LANGS.forEach((file): void => {
}, {}); }, {});
if (watch) { if (watch) {
INCLUDE_LANGS.forEach(file => watchLanguage(I18N_BASE_PATH + file, I18N_DEST)); INCLUDE_LANGS.forEach((file) => watchLanguage(I18N_BASE_PATH + file, I18N_DEST));
} }

View file

@ -1,7 +1,7 @@
const fsProm = require('fs').promises; const fsProm = require("fs").promises;
const path = require('path'); const path = require("path");
exports.default = async function(context) { exports.default = async function (context) {
const { electronPlatformName, appOutDir } = context; const { electronPlatformName, appOutDir } = context;
// Squirrel windows will try to relaunch the app using an executable of the same name as // Squirrel windows will try to relaunch the app using an executable of the same name as
@ -9,7 +9,7 @@ exports.default = async function(context) {
// We add a fake Riot.exe that it can run which runs the real one. // We add a fake Riot.exe that it can run which runs the real one.
// This also gets signed automatically, presumably because electron-build just looks for all // This also gets signed automatically, presumably because electron-build just looks for all
// exe files and signs them all... // exe files and signs them all...
if (electronPlatformName === 'win32') { if (electronPlatformName === "win32") {
await fsProm.copyFile('build/rebrand_stub/rebrand_stub.exe', path.join(appOutDir, "Riot.exe")); await fsProm.copyFile("build/rebrand_stub/rebrand_stub.exe", path.join(appOutDir, "Riot.exe"));
} }
}; };

View file

@ -1,11 +1,11 @@
const { notarize } = require('@electron/notarize'); const { notarize } = require("@electron/notarize");
let warned = false; let warned = false;
exports.default = async function(context) { exports.default = async function (context) {
const { electronPlatformName, appOutDir } = context; const { electronPlatformName, appOutDir } = context;
const appId = context.packager.info.appInfo.id; const appId = context.packager.info.appInfo.id;
if (electronPlatformName === 'darwin') { if (electronPlatformName === "darwin") {
const appName = context.packager.appInfo.productFilename; const appName = context.packager.appInfo.productFilename;
const keychainProfile = process.env.NOTARIZE_KEYCHAIN_PROFILE; const keychainProfile = process.env.NOTARIZE_KEYCHAIN_PROFILE;

View file

@ -1,4 +1,4 @@
const { execFile } = require('child_process'); const { execFile } = require("child_process");
// Loosely based on computeSignToolArgs from app-builder-lib/src/codeSign/windowsCodeSign.ts // Loosely based on computeSignToolArgs from app-builder-lib/src/codeSign/windowsCodeSign.ts
function computeSignToolArgs(options, keyContainer) { function computeSignToolArgs(options, keyContainer) {
@ -8,15 +8,15 @@ function computeSignToolArgs(options, keyContainer) {
const timestampingServiceUrl = options.options.timeStampServer || "http://timestamp.digicert.com"; const timestampingServiceUrl = options.options.timeStampServer || "http://timestamp.digicert.com";
args.push( args.push(
options.isNest || options.hash === "sha256" ? "/tr" : "/t", options.isNest || options.hash === "sha256" ? "/tr" : "/t",
options.isNest || options.hash === "sha256" ? ( options.isNest || options.hash === "sha256"
options.options.rfc3161TimeStampServer || "http://timestamp.comodoca.com/rfc3161" ? options.options.rfc3161TimeStampServer || "http://timestamp.comodoca.com/rfc3161"
) : timestampingServiceUrl, : timestampingServiceUrl,
); );
} }
args.push('/kc', keyContainer); args.push("/kc", keyContainer);
// To use the hardware token (this should probably be less hardcoded) // To use the hardware token (this should probably be less hardcoded)
args.push('/csp', 'eToken Base Cryptographic Provider'); args.push("/csp", "eToken Base Cryptographic Provider");
// The certificate file. Somehow this appears to be the only way to specify // The certificate file. Somehow this appears to be the only way to specify
// the cert that works. If you specify the subject name or hash, it will // the cert that works. If you specify the subject name or hash, it will
// say it can't associate the private key to the certificate. // say it can't associate the private key to the certificate.
@ -24,7 +24,7 @@ function computeSignToolArgs(options, keyContainer) {
// so we don't have to hard-code this here // so we don't have to hard-code this here
// fwiw https://stackoverflow.com/questions/17927895/automate-extended-validation-ev-code-signing // fwiw https://stackoverflow.com/questions/17927895/automate-extended-validation-ev-code-signing
// is about the most useful resource on automating code signing... // is about the most useful resource on automating code signing...
args.push('/f', 'element.io\\New_Vector_Ltd.pem'); args.push("/f", "element.io\\New_Vector_Ltd.pem");
if (options.hash !== "sha1") { if (options.hash !== "sha1") {
args.push("/fd", options.hash); args.push("/fd", options.hash);
@ -35,7 +35,7 @@ function computeSignToolArgs(options, keyContainer) {
// msi does not support dual-signing // msi does not support dual-signing
if (options.isNest) { if (options.isNest) {
args.push("/as"); args.push("/as");
} }
// https://github.com/electron-userland/electron-builder/issues/2875#issuecomment-387233610 // https://github.com/electron-userland/electron-builder/issues/2875#issuecomment-387233610
@ -47,15 +47,15 @@ function computeSignToolArgs(options, keyContainer) {
} }
let warned = false; let warned = false;
exports.default = async function(options) { exports.default = async function (options) {
const keyContainer = process.env.SIGNING_KEY_CONTAINER; const keyContainer = process.env.SIGNING_KEY_CONTAINER;
if (keyContainer === undefined) { if (keyContainer === undefined) {
if (!warned) { if (!warned) {
console.warn( console.warn(
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" +
"! Skipping Windows signing. !\n" + "! Skipping Windows signing. !\n" +
"! SIGNING_KEY_CONTAINER not defined. !\n" + "! SIGNING_KEY_CONTAINER not defined. !\n" +
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
); );
warned = true; warned = true;
} }
@ -63,9 +63,9 @@ exports.default = async function(options) {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const args = ['sign'].concat(computeSignToolArgs(options, keyContainer)); const args = ["sign"].concat(computeSignToolArgs(options, keyContainer));
execFile('signtool', args, {}, (error, stdout) => { execFile("signtool", args, {}, (error, stdout) => {
if (error) { if (error) {
console.error("signtool failed with code " + error); console.error("signtool failed with code " + error);
reject("signtool failed with code " + error); reject("signtool failed with code " + error);

View file

@ -14,7 +14,7 @@ import { setPackageVersion } from "./set-version";
const PUB_KEY_URL = "https://packages.riot.im/element-release-key.asc"; const PUB_KEY_URL = "https://packages.riot.im/element-release-key.asc";
const PACKAGE_URL_PREFIX = "https://github.com/vector-im/element-web/releases/download/"; const PACKAGE_URL_PREFIX = "https://github.com/vector-im/element-web/releases/download/";
const DEVELOP_TGZ_URL = "https://develop.element.io/develop.tar.gz"; const DEVELOP_TGZ_URL = "https://develop.element.io/develop.tar.gz";
const ASAR_PATH = 'webapp.asar'; const ASAR_PATH = "webapp.asar";
async function downloadToFile(url: string, filename: string): Promise<void> { async function downloadToFile(url: string, filename: string): Promise<void> {
console.log("Downloading " + url + "..."); console.log("Downloading " + url + "...");
@ -35,7 +35,7 @@ async function downloadToFile(url: string, filename: string): Promise<void> {
async function verifyFile(filename: string): Promise<void> { async function verifyFile(filename: string): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
childProcess.execFile('gpg', ['--verify', filename + '.asc', filename], (error) => { childProcess.execFile("gpg", ["--verify", filename + ".asc", filename], (error) => {
if (error) { if (error) {
reject(error); reject(error);
} else { } else {
@ -48,8 +48,8 @@ async function verifyFile(filename: string): Promise<void> {
async function main(): Promise<number | undefined> { async function main(): Promise<number | undefined> {
let verify = true; let verify = true;
let importkey = false; let importkey = false;
let pkgDir = 'packages'; let pkgDir = "packages";
let deployDir = 'deploys'; let deployDir = "deploys";
let cfgDir: string | undefined; let cfgDir: string | undefined;
let targetVersion: string | undefined; let targetVersion: string | undefined;
let filename: string | undefined; let filename: string | undefined;
@ -58,22 +58,22 @@ async function main(): Promise<number | undefined> {
while (process.argv.length > 2) { while (process.argv.length > 2) {
switch (process.argv[2]) { switch (process.argv[2]) {
case '--noverify': case "--noverify":
verify = false; verify = false;
break; break;
case '--importkey': case "--importkey":
importkey = true; importkey = true;
break; break;
case '--packages': case "--packages":
process.argv.shift(); process.argv.shift();
pkgDir = process.argv[2]; pkgDir = process.argv[2];
break; break;
case '--deploys': case "--deploys":
process.argv.shift(); process.argv.shift();
deployDir = process.argv[2]; deployDir = process.argv[2];
break; break;
case '--cfgdir': case "--cfgdir":
case '-d': case "-d":
process.argv.shift(); process.argv.shift();
cfgDir = process.argv[2]; cfgDir = process.argv[2];
break; break;
@ -84,13 +84,13 @@ async function main(): Promise<number | undefined> {
} }
if (targetVersion === undefined) { if (targetVersion === undefined) {
targetVersion = 'v' + riotDesktopPackageJson.version; targetVersion = "v" + riotDesktopPackageJson.version;
} else if (targetVersion !== 'develop') { } else if (targetVersion !== "develop") {
setVersion = true; // version was specified setVersion = true; // version was specified
} }
if (targetVersion === 'develop') { if (targetVersion === "develop") {
filename = 'develop.tar.gz'; filename = "develop.tar.gz";
url = DEVELOP_TGZ_URL; url = DEVELOP_TGZ_URL;
verify = false; // develop builds aren't signed verify = false; // develop builds aren't signed
} else if (targetVersion.includes("://")) { } else if (targetVersion.includes("://")) {
@ -99,11 +99,11 @@ async function main(): Promise<number | undefined> {
verify = false; // manually verified verify = false; // manually verified
} else { } else {
filename = `element-${targetVersion}.tar.gz`; filename = `element-${targetVersion}.tar.gz`;
url = PACKAGE_URL_PREFIX + targetVersion + '/' + filename; url = PACKAGE_URL_PREFIX + targetVersion + "/" + filename;
} }
const haveGpg = await new Promise<boolean>((resolve) => { const haveGpg = await new Promise<boolean>((resolve) => {
childProcess.execFile('gpg', ['--version'], (error) => { childProcess.execFile("gpg", ["--version"], (error) => {
resolve(!error); resolve(!error);
}); });
}); });
@ -115,7 +115,7 @@ async function main(): Promise<number | undefined> {
} }
await new Promise<boolean>((resolve) => { await new Promise<boolean>((resolve) => {
const gpgProc = childProcess.execFile('gpg', ['--import'], (error) => { const gpgProc = childProcess.execFile("gpg", ["--import"], (error) => {
if (error) { if (error) {
console.log("Failed to import key", error); console.log("Failed to import key", error);
} else { } else {
@ -123,7 +123,7 @@ async function main(): Promise<number | undefined> {
} }
resolve(!error); resolve(!error);
}); });
fetch(PUB_KEY_URL).then(resp => { fetch(PUB_KEY_URL).then((resp) => {
stream.pipeline(resp.body, gpgProc.stdin!); stream.pipeline(resp.body, gpgProc.stdin!);
}); });
}); });
@ -143,13 +143,12 @@ async function main(): Promise<number | undefined> {
} }
let haveDeploy = false; let haveDeploy = false;
let expectedDeployDir = path.join(deployDir, path.basename(filename).replace(/\.tar\.gz/, '')); let expectedDeployDir = path.join(deployDir, path.basename(filename).replace(/\.tar\.gz/, ""));
try { try {
await fs.opendir(expectedDeployDir); await fs.opendir(expectedDeployDir);
console.log(expectedDeployDir + "already exists"); console.log(expectedDeployDir + "already exists");
haveDeploy = true; haveDeploy = true;
} catch (e) { } catch (e) {}
}
if (!haveDeploy) { if (!haveDeploy) {
const outPath = path.join(pkgDir, filename); const outPath = path.join(pkgDir, filename);
@ -167,11 +166,11 @@ async function main(): Promise<number | undefined> {
if (verify) { if (verify) {
try { try {
await fs.stat(outPath+'.asc'); await fs.stat(outPath + ".asc");
console.log("Already have " + filename + ".asc: not redownloading"); console.log("Already have " + filename + ".asc: not redownloading");
} catch (e) { } catch (e) {
try { try {
await downloadToFile(url + '.asc', outPath + '.asc'); await downloadToFile(url + ".asc", outPath + ".asc");
} catch (e) { } catch (e) {
console.log("Failed to download " + url, e); console.log("Failed to download " + url, e);
return 1; return 1;
@ -192,7 +191,7 @@ async function main(): Promise<number | undefined> {
await tar.x({ await tar.x({
file: outPath, file: outPath,
cwd: deployDir, cwd: deployDir,
onentry: entry => { onentry: (entry) => {
// Find the appropriate extraction path, only needed for `develop` where the dir name is unknown // Find the appropriate extraction path, only needed for `develop` where the dir name is unknown
if (entry.type === "Directory" && !path.join(deployDir, entry.path).startsWith(expectedDeployDir)) { if (entry.type === "Directory" && !path.join(deployDir, entry.path).startsWith(expectedDeployDir)) {
expectedDeployDir = path.join(deployDir, entry.path); expectedDeployDir = path.join(deployDir, entry.path);
@ -205,13 +204,12 @@ async function main(): Promise<number | undefined> {
await fs.stat(ASAR_PATH); await fs.stat(ASAR_PATH);
console.log(ASAR_PATH + " already present: removing"); console.log(ASAR_PATH + " already present: removing");
await fs.unlink(ASAR_PATH); await fs.unlink(ASAR_PATH);
} catch (e) { } catch (e) {}
}
if (cfgDir.length) { if (cfgDir.length) {
const configJsonSource = path.join(cfgDir, 'config.json'); const configJsonSource = path.join(cfgDir, "config.json");
const configJsonDest = path.join(expectedDeployDir, 'config.json'); const configJsonDest = path.join(expectedDeployDir, "config.json");
console.log(configJsonSource + ' -> ' + configJsonDest); console.log(configJsonSource + " -> " + configJsonDest);
await fs.copyFile(configJsonSource, configJsonDest); await fs.copyFile(configJsonSource, configJsonDest);
} else { } else {
console.log("Skipping config file"); console.log("Skipping config file");
@ -229,9 +227,11 @@ async function main(): Promise<number | undefined> {
console.log("Done!"); console.log("Done!");
} }
main().then((ret) => { main()
process.exit(ret); .then((ret) => {
}).catch(e => { process.exit(ret);
console.error(e); })
process.exit(1); .catch((e) => {
}); console.error(e);
process.exit(1);
});

View file

@ -84,21 +84,21 @@ function humanFileSize(bytes: number, si = false, dp = 1): string {
const thresh = si ? 1000 : 1024; const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) { if (Math.abs(bytes) < thresh) {
return bytes + ' B'; return bytes + " B";
} }
const units = si const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
let u = -1; let u = -1;
const r = 10**dp; const r = 10 ** dp;
do { do {
bytes /= thresh; bytes /= thresh;
++u; ++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u]; return bytes.toFixed(dp) + " " + units[u];
} }
const dateTimeOptions: Intl.DateTimeFormatOptions = { const dateTimeOptions: Intl.DateTimeFormatOptions = {
@ -122,7 +122,8 @@ function indexLayout(prefix: string, files: _Object[], dirs: string[]): string {
} }
for (const file of files) { for (const file of files) {
if (!file.Key || if (
!file.Key ||
HIDDEN_FILES.includes(`/${file.Key}`) || HIDDEN_FILES.includes(`/${file.Key}`) ||
HIDDEN_FILES.includes(file.Key.slice(file.Key.lastIndexOf("/") + 1)) HIDDEN_FILES.includes(file.Key.slice(file.Key.lastIndexOf("/") + 1))
) { ) {
@ -143,11 +144,15 @@ function indexLayout(prefix: string, files: _Object[], dirs: string[]): string {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
${rows.map(([link, name, size, date]) => `<tr> ${rows
.map(
([link, name, size, date]) => `<tr>
<td class="link"><a href="${link}">${name}</a></td> <td class="link"><a href="${link}">${name}</a></td>
<td class="size">${size ? humanFileSize(size) : "-"}</td> <td class="size">${size ? humanFileSize(size) : "-"}</td>
<td class="date">${date?.toLocaleString("en-GB", dateTimeOptions) ?? "-"}</td> <td class="date">${date?.toLocaleString("en-GB", dateTimeOptions) ?? "-"}</td>
</tr>`).join("")} </tr>`,
)
.join("")}
</tbody> </tbody>
</table> </table>
`); `);
@ -166,17 +171,20 @@ async function generateIndex(Prefix: string): Promise<{
const listResponse = await client.send(command); const listResponse = await client.send(command);
const files = listResponse.Contents ?? []; const files = listResponse.Contents ?? [];
const dirs = listResponse.CommonPrefixes const dirs =
?.map(p => p.Prefix?.slice(Prefix.length).split("/", 2)[0]) (listResponse.CommonPrefixes?.map((p) => p.Prefix?.slice(Prefix.length).split("/", 2)[0]).filter(
.filter(Boolean) as string[] ?? []; Boolean,
) as string[]) ?? [];
const Body = indexLayout(Prefix, files, dirs); const Body = indexLayout(Prefix, files, dirs);
await client.send(new PutObjectCommand({ await client.send(
Body, new PutObjectCommand({
Bucket, Body,
ContentType: "text/html", Bucket,
Key: Prefix + "index.html", ContentType: "text/html",
})); Key: Prefix + "index.html",
}),
);
return { files, dirs }; return { files, dirs };
} }

View file

@ -1,40 +1,40 @@
hak # hak
===
This tool builds native dependencies for element-desktop. Here follows some very minimal This tool builds native dependencies for element-desktop. Here follows some very minimal
documentation for it. documentation for it.
Goals: Goals:
* Must build compiled native node modules in a shippable state
(ie. only dynamically linked against libraries that will be on the - Must build compiled native node modules in a shippable state
target system, all unnecessary files removed). (ie. only dynamically linked against libraries that will be on the
* Must be able to build any native module, no matter what build system target system, all unnecessary files removed).
it uses (electron-rebuild is supposed to do this job but only works - Must be able to build any native module, no matter what build system
for modules that use gyp). it uses (electron-rebuild is supposed to do this job but only works
for modules that use gyp).
It's also loosely designed to be a general tool and agnostic to what it's It's also loosely designed to be a general tool and agnostic to what it's
actually building. It's used here to build modules for the electron app actually building. It's used here to build modules for the electron app
but should work equally well for building modules for normal node. but should work equally well for building modules for normal node.
Running # Running
=======
Hak is invoked with a command and a dependency, eg. `yarn run hak fetch matrix-seshat`. Hak is invoked with a command and a dependency, eg. `yarn run hak fetch matrix-seshat`.
If no dependencies are given, hak runs the command on all dependencies. If no dependencies are given, hak runs the command on all dependencies.
Files # Files
=====
There are a lot of files involved: There are a lot of files involved:
* scripts/hak/... - The tool itself - scripts/hak/... - The tool itself
* hak/[dependency] - Files provided by the app that tell hak how to build each of its native dependencies. - hak/[dependency] - Files provided by the app that tell hak how to build each of its native dependencies.
Contains a hak.json file and also some script files, each of which must be referenced in hak.json. Contains a hak.json file and also some script files, each of which must be referenced in hak.json.
* .hak/ - Files generated by hak in the course of doing its job. Includes the dependency module itself and - .hak/ - Files generated by hak in the course of doing its job. Includes the dependency module itself and
any of the native dependency's native dependencies. any of the native dependency's native dependencies.
* .hak/[dependency]/build - An extracted copy of the dependency's node module used to build it. - .hak/[dependency]/build - An extracted copy of the dependency's node module used to build it.
* .hak/[dependency]/out - Another extracted copy of the dependency, this one contains only what will be shipped. - .hak/[dependency]/out - Another extracted copy of the dependency, this one contains only what will be shipped.
# Workings
Workings
========
Hak works around native node modules that try to fetch or build their native component in Hak works around native node modules that try to fetch or build their native component in
the npm 'install' phase - modules that do this will typically end up with native components the npm 'install' phase - modules that do this will typically end up with native components
targeted to the build platform and the node that npm/yarn is using, which is no good for an targeted to the build platform and the node that npm/yarn is using, which is no good for an
@ -49,33 +49,34 @@ This also means that the dependencies cannot be listed in `dependencies` or
try to fetch their native parts. Instead, they are listed in `hakDependencies` which try to fetch their native parts. Instead, they are listed in `hakDependencies` which
hak reads to install them for you. hak reads to install them for you.
Hak will *not* install dependencies for the copy of the module it links into your Hak will _not_ install dependencies for the copy of the module it links into your
project, so if your native module has javascript dependencies that are actually needed at project, so if your native module has javascript dependencies that are actually needed at
runtime (and not just to fetch / build the native parts), it won't work. runtime (and not just to fetch / build the native parts), it won't work.
Hak will generate a `.yarnrc` in the project directory to set the link directory to its Hak will generate a `.yarnrc` in the project directory to set the link directory to its
own in the .hak directory (unless one already exists, in which case this is your problem). own in the .hak directory (unless one already exists, in which case this is your problem).
Lifecycle # Lifecycle
=========
Hak is divided into lifecycle stages, in order: Hak is divided into lifecycle stages, in order:
* fetch - Download and extract the source of the dependency
* link - Link the copy of the dependency into your node_modules directory - fetch - Download and extract the source of the dependency
* fetchDeps - Fetch & extract any native dependencies required to build the module. - link - Link the copy of the dependency into your node_modules directory
* build - The Good Stuff. Configure and build any native dependencies, then the module itself. - fetchDeps - Fetch & extract any native dependencies required to build the module.
* copy - Copy the built artifact from the module build directory to the module output directory. - build - The Good Stuff. Configure and build any native dependencies, then the module itself.
- copy - Copy the built artifact from the module build directory to the module output directory.
# hak.json
hak.json
========
The scripts section contains scripts used for lifecycle stages that need them (fetch, fetchDeps, build). The scripts section contains scripts used for lifecycle stages that need them (fetch, fetchDeps, build).
It also contains 'prune' and 'copy' which are globs of files to delete from the output module directory It also contains 'prune' and 'copy' which are globs of files to delete from the output module directory
and copy over from the module build directory to the output module directory, respectively. and copy over from the module build directory to the output module directory, respectively.
Shortcomings # Shortcomings
============
Hak doesn't know about dependencies between lifecycle stages, ie. it doesn't know that you need to Hak doesn't know about dependencies between lifecycle stages, ie. it doesn't know that you need to
'fetch' and 'fetchDeps' before you can 'build', etc. You get to run each individually, and remember 'fetch' and 'fetchDeps' before you can 'build', etc. You get to run each individually, and remember
the right order. the right order.
There is also a *lot* of duplication in the command execution: we should abstract away There is also a _lot_ of duplication in the command execution: we should abstract away
some of the boilerplate required to run commands & so forth. some of the boilerplate required to run commands & so forth.

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import rimraf from 'rimraf'; import rimraf from "rimraf";
import { DependencyInfo } from './dep'; import { DependencyInfo } from "./dep";
import HakEnv from './hakEnv'; import HakEnv from "./hakEnv";
export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
@ -32,7 +32,7 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
rimraf(path.join(hakEnv.dotHakDir, 'links', moduleInfo.name), (err?: Error | null) => { rimraf(path.join(hakEnv.dotHakDir, "links", moduleInfo.name), (err?: Error | null) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
@ -42,7 +42,7 @@ export default async function clean(hakEnv: HakEnv, moduleInfo: DependencyInfo):
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
rimraf(path.join(hakEnv.projectRoot, 'node_modules', moduleInfo.name), (err?: Error | null) => { rimraf(path.join(hakEnv.projectRoot, "node_modules", moduleInfo.name), (err?: Error | null) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import fsProm from 'fs/promises'; import fsProm from "fs/promises";
import childProcess from 'child_process'; import childProcess from "child_process";
import rimraf from 'rimraf'; import rimraf from "rimraf";
import glob from 'glob'; import glob from "glob";
import mkdirp from 'mkdirp'; import mkdirp from "mkdirp";
import HakEnv from './hakEnv'; import HakEnv from "./hakEnv";
import { DependencyInfo } from './dep'; import { DependencyInfo } from "./dep";
export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
if (moduleInfo.cfg.prune) { if (moduleInfo.cfg.prune) {
@ -34,7 +34,7 @@ export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo):
await mkdirp(moduleInfo.moduleOutDir); await mkdirp(moduleInfo.moduleOutDir);
process.chdir(moduleInfo.moduleOutDir); process.chdir(moduleInfo.moduleOutDir);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
rimraf(moduleInfo.cfg.prune, {}, err => { rimraf(moduleInfo.cfg.prune, {}, (err) => {
err ? reject(err) : resolve(); err ? reject(err) : resolve();
}); });
}); });
@ -48,46 +48,44 @@ export default async function copy(hakEnv: HakEnv, moduleInfo: DependencyInfo):
// is the same as moduleBuildDirs[0], so we're just listing the contents // is the same as moduleBuildDirs[0], so we're just listing the contents
// of the first one. // of the first one.
const files = await new Promise<string[]>((resolve, reject) => { const files = await new Promise<string[]>((resolve, reject) => {
glob(moduleInfo.cfg.copy, { glob(
nosort: true, moduleInfo.cfg.copy,
silent: true, {
cwd: moduleInfo.moduleBuildDir, nosort: true,
}, (err, files) => { silent: true,
err ? reject(err) : resolve(files); cwd: moduleInfo.moduleBuildDir,
}); },
(err, files) => {
err ? reject(err) : resolve(files);
},
);
}); });
if (moduleInfo.moduleBuildDirs.length > 1) { if (moduleInfo.moduleBuildDirs.length > 1) {
if (!hakEnv.isMac()) { if (!hakEnv.isMac()) {
console.error( console.error(
"You asked me to copy multiple targets but I've only been taught " + "You asked me to copy multiple targets but I've only been taught " + "how to do that on macOS.",
"how to do that on macOS.",
); );
throw new Error("Can't copy multiple targets on this platform"); throw new Error("Can't copy multiple targets on this platform");
} }
for (const f of files) { for (const f of files) {
const components = moduleInfo.moduleBuildDirs.map(dir => path.join(dir, f)); const components = moduleInfo.moduleBuildDirs.map((dir) => path.join(dir, f));
const dst = path.join(moduleInfo.moduleOutDir, f); const dst = path.join(moduleInfo.moduleOutDir, f);
await mkdirp(path.dirname(dst)); await mkdirp(path.dirname(dst));
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
childProcess.execFile('lipo', childProcess.execFile("lipo", ["-create", "-output", dst, ...components], (err) => {
['-create', '-output', dst, ...components], (err) => { if (err) {
if (err) { reject(err);
reject(err); } else {
} else { resolve();
resolve(); }
} });
},
);
}); });
} }
} else { } else {
console.log( console.log("Copying files from " + moduleInfo.moduleBuildDir + " to " + moduleInfo.moduleOutDir);
"Copying files from " +
moduleInfo.moduleBuildDir + " to " + moduleInfo.moduleOutDir,
);
for (const f of files) { for (const f of files) {
console.log("\t" + f); console.log("\t" + f);
const src = path.join(moduleInfo.moduleBuildDir, f); const src = path.join(moduleInfo.moduleBuildDir, f);

View file

@ -28,5 +28,5 @@ export interface DependencyInfo {
moduleOutDir: string; moduleOutDir: string;
nodeModuleBinDir: string; nodeModuleBinDir: string;
depPrefix: string; depPrefix: string;
scripts: Record<string, (hakEnv: HakEnv, moduleInfo: DependencyInfo) => Promise<void> >; scripts: Record<string, (hakEnv: HakEnv, moduleInfo: DependencyInfo) => Promise<void>>;
} }

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import fsProm from 'fs/promises'; import fsProm from "fs/promises";
import childProcess from 'child_process'; import childProcess from "child_process";
import pacote from 'pacote'; import pacote from "pacote";
import HakEnv from './hakEnv'; import HakEnv from "./hakEnv";
import { DependencyInfo } from './dep'; import { DependencyInfo } from "./dep";
export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
let haveModuleBuildDir; let haveModuleBuildDir;
@ -41,15 +41,11 @@ export default async function fetch(hakEnv: HakEnv, moduleInfo: DependencyInfo):
console.log("Running yarn install in " + moduleInfo.moduleBuildDir); console.log("Running yarn install in " + moduleInfo.moduleBuildDir);
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn( const proc = childProcess.spawn(hakEnv.isWin() ? "yarn.cmd" : "yarn", ["install", "--ignore-scripts"], {
hakEnv.isWin() ? 'yarn.cmd' : 'yarn', stdio: "inherit",
['install', '--ignore-scripts'], cwd: moduleInfo.moduleBuildDir,
{ });
stdio: 'inherit', proc.on("exit", (code) => {
cwd: moduleInfo.moduleBuildDir,
},
);
proc.on('exit', code => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import mkdirp from 'mkdirp'; import mkdirp from "mkdirp";
import { DependencyInfo } from './dep'; import { DependencyInfo } from "./dep";
import HakEnv from './hakEnv'; import HakEnv from "./hakEnv";
export default async function fetchDeps(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function fetchDeps(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
await mkdirp(moduleInfo.moduleDotHakDir); await mkdirp(moduleInfo.moduleDotHakDir);

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import os from 'os'; import os from "os";
import nodePreGypVersioning from "node-pre-gyp/lib/util/versioning"; import nodePreGypVersioning from "node-pre-gyp/lib/util/versioning";
import { getElectronVersion } from "app-builder-lib/out/electron/electronVersion"; import { getElectronVersion } from "app-builder-lib/out/electron/electronVersion";
import { Arch, Target, TARGETS, getHost, isHostId, TargetId } from './target'; import { Arch, Target, TARGETS, getHost, isHostId, TargetId } from "./target";
async function getRuntime(projectRoot: string): Promise<string> { async function getRuntime(projectRoot: string): Promise<string> {
const electronVersion = await getElectronVersion(projectRoot); const electronVersion = await getElectronVersion(projectRoot);
return electronVersion ? 'electron' : 'node-webkit'; return electronVersion ? "electron" : "node-webkit";
} }
async function getRuntimeVersion(projectRoot: string): Promise<string> { async function getRuntimeVersion(projectRoot: string): Promise<string> {
@ -48,7 +48,7 @@ export default class HakEnv {
throw new Error(`Unknown target ${targetId}!`); throw new Error(`Unknown target ${targetId}!`);
} }
this.target = target; this.target = target;
this.dotHakDir = path.join(this.projectRoot, '.hak'); this.dotHakDir = path.join(this.projectRoot, ".hak");
} }
public async init(): Promise<void> { public async init(): Promise<void> {
@ -62,7 +62,7 @@ export default class HakEnv {
// {node_abi}-{platform}-{arch} // {node_abi}-{platform}-{arch}
public getNodeTriple(): string { public getNodeTriple(): string {
return this.getRuntimeAbi() + '-' + this.target.platform + '-' + this.target.arch; return this.getRuntimeAbi() + "-" + this.target.platform + "-" + this.target.arch;
} }
public getTargetId(): TargetId { public getTargetId(): TargetId {
@ -70,15 +70,15 @@ export default class HakEnv {
} }
public isWin(): boolean { public isWin(): boolean {
return this.target.platform === 'win32'; return this.target.platform === "win32";
} }
public isMac(): boolean { public isMac(): boolean {
return this.target.platform === 'darwin'; return this.target.platform === "darwin";
} }
public isLinux(): boolean { public isLinux(): boolean {
return this.target.platform === 'linux'; return this.target.platform === "linux";
} }
public getTargetArch(): Arch { public getTargetArch(): Arch {
@ -93,7 +93,7 @@ export default class HakEnv {
return Object.assign({}, process.env, { return Object.assign({}, process.env, {
npm_config_arch: this.target.arch, npm_config_arch: this.target.arch,
npm_config_target_arch: this.target.arch, npm_config_target_arch: this.target.arch,
npm_config_disturl: 'https://electronjs.org/headers', npm_config_disturl: "https://electronjs.org/headers",
npm_config_runtime: this.runtime, npm_config_runtime: this.runtime,
npm_config_target: this.runtimeVersion, npm_config_target: this.runtimeVersion,
npm_config_build_from_source: true, npm_config_build_from_source: true,
@ -102,7 +102,7 @@ export default class HakEnv {
} }
public wantsStaticSqlCipherUnix(): boolean { public wantsStaticSqlCipherUnix(): boolean {
return this.isMac() || process.env.SQLCIPHER_STATIC == '1'; return this.isMac() || process.env.SQLCIPHER_STATIC == "1";
} }
public wantsStaticSqlCipher(): boolean { public wantsStaticSqlCipher(): boolean {

View file

@ -14,42 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import findNpmPrefix from 'find-npm-prefix'; import findNpmPrefix from "find-npm-prefix";
import HakEnv from './hakEnv'; import HakEnv from "./hakEnv";
import { TargetId } from './target'; import { TargetId } from "./target";
import { DependencyInfo } from './dep'; import { DependencyInfo } from "./dep";
const GENERALCOMMANDS = [ const GENERALCOMMANDS = ["target"];
'target',
];
// These can only be run on specific modules // These can only be run on specific modules
const MODULECOMMANDS = [ const MODULECOMMANDS = ["check", "fetch", "link", "fetchDeps", "build", "copy", "clean"];
'check',
'fetch',
'link',
'fetchDeps',
'build',
'copy',
'clean',
];
// Shortcuts for multiple commands at once (useful for building universal binaries // Shortcuts for multiple commands at once (useful for building universal binaries
// because you can run the fetch/fetchDeps/build for each arch and then copy/link once) // because you can run the fetch/fetchDeps/build for each arch and then copy/link once)
const METACOMMANDS: Record<string, string[]> = { const METACOMMANDS: Record<string, string[]> = {
'fetchandbuild': ['check', 'fetch', 'fetchDeps', 'build'], fetchandbuild: ["check", "fetch", "fetchDeps", "build"],
'copyandlink': ['copy', 'link'], copyandlink: ["copy", "link"],
}; };
// Scripts valid in a hak.json 'scripts' section // Scripts valid in a hak.json 'scripts' section
const HAKSCRIPTS = [ const HAKSCRIPTS = ["check", "fetch", "fetchDeps", "build"];
'check',
'fetch',
'fetchDeps',
'build',
];
async function main(): Promise<void> { async function main(): Promise<void> {
const prefix = await findNpmPrefix(process.cwd()); const prefix = await findNpmPrefix(process.cwd());
@ -65,11 +50,12 @@ async function main(): Promise<void> {
// Apply `--target <target>` option if specified // Apply `--target <target>` option if specified
// Can be specified multiple times for the copy command to bundle // Can be specified multiple times for the copy command to bundle
// multiple archs into a single universal output module) // multiple archs into a single universal output module)
while (true) { // eslint-disable-line no-constant-condition for (;;) {
const targetIndex = process.argv.indexOf('--target'); // eslint-disable-line no-constant-condition
const targetIndex = process.argv.indexOf("--target");
if (targetIndex === -1) break; if (targetIndex === -1) break;
if ((targetIndex + 1) >= process.argv.length) { if (targetIndex + 1 >= process.argv.length) {
console.error("--target option specified without a target"); console.error("--target option specified without a target");
process.exit(1); process.exit(1);
} }
@ -77,7 +63,7 @@ async function main(): Promise<void> {
targetIds.push(process.argv.splice(targetIndex, 2)[1] as TargetId); targetIds.push(process.argv.splice(targetIndex, 2)[1] as TargetId);
} }
const hakEnvs = targetIds.map(tid => new HakEnv(prefix, tid)); const hakEnvs = targetIds.map((tid) => new HakEnv(prefix, tid));
if (hakEnvs.length == 0) hakEnvs.push(new HakEnv(prefix, null)); if (hakEnvs.length == 0) hakEnvs.push(new HakEnv(prefix, null));
for (const h of hakEnvs) { for (const h of hakEnvs) {
await h.init(); await h.init();
@ -89,7 +75,7 @@ async function main(): Promise<void> {
const hakDepsCfg = packageJson.hakDependencies || {}; const hakDepsCfg = packageJson.hakDependencies || {};
for (const dep of Object.keys(hakDepsCfg)) { for (const dep of Object.keys(hakDepsCfg)) {
const hakJsonPath = path.join(prefix, 'hak', dep, 'hak.json'); const hakJsonPath = path.join(prefix, "hak", dep, "hak.json");
let hakJson: Record<string, any>; let hakJson: Record<string, any>;
try { try {
hakJson = await require(hakJsonPath); hakJson = await require(hakJsonPath);
@ -102,20 +88,20 @@ async function main(): Promise<void> {
name: dep, name: dep,
version: hakDepsCfg[dep], version: hakDepsCfg[dep],
cfg: hakJson, cfg: hakJson,
moduleHakDir: path.join(prefix, 'hak', dep), moduleHakDir: path.join(prefix, "hak", dep),
moduleDotHakDir: path.join(hakEnv.dotHakDir, dep), moduleDotHakDir: path.join(hakEnv.dotHakDir, dep),
moduleTargetDotHakDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId()), moduleTargetDotHakDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId()),
moduleBuildDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), 'build'), moduleBuildDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), "build"),
moduleBuildDirs: hakEnvs.map(h => path.join(h.dotHakDir, dep, h.getTargetId(), 'build')), moduleBuildDirs: hakEnvs.map((h) => path.join(h.dotHakDir, dep, h.getTargetId(), "build")),
moduleOutDir: path.join(hakEnv.dotHakDir, 'hakModules', dep), moduleOutDir: path.join(hakEnv.dotHakDir, "hakModules", dep),
nodeModuleBinDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), 'build', 'node_modules', '.bin'), nodeModuleBinDir: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), "build", "node_modules", ".bin"),
depPrefix: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), 'opt'), depPrefix: path.join(hakEnv.dotHakDir, dep, hakEnv.getTargetId(), "opt"),
scripts: {}, scripts: {},
}; };
for (const s of HAKSCRIPTS) { for (const s of HAKSCRIPTS) {
if (hakJson.scripts && hakJson.scripts[s]) { if (hakJson.scripts && hakJson.scripts[s]) {
const scriptModule = await import(path.join(prefix, 'hak', dep, hakJson.scripts[s])); const scriptModule = await import(path.join(prefix, "hak", dep, hakJson.scripts[s]));
if (scriptModule.__esModule) { if (scriptModule.__esModule) {
deps[dep].scripts[s] = scriptModule.default; deps[dep].scripts[s] = scriptModule.default;
} else { } else {
@ -127,14 +113,14 @@ async function main(): Promise<void> {
let cmds: string[]; let cmds: string[];
if (process.argv.length < 3) { if (process.argv.length < 3) {
cmds = ['check', 'fetch', 'fetchDeps', 'build', 'copy', 'link']; cmds = ["check", "fetch", "fetchDeps", "build", "copy", "link"];
} else if (METACOMMANDS[process.argv[2]]) { } else if (METACOMMANDS[process.argv[2]]) {
cmds = METACOMMANDS[process.argv[2]]; cmds = METACOMMANDS[process.argv[2]];
} else { } else {
cmds = [process.argv[2]]; cmds = [process.argv[2]];
} }
if (hakEnvs.length > 1 && cmds.some(c => !['copy', 'link'].includes(c))) { if (hakEnvs.length > 1 && cmds.some((c) => !["copy", "link"].includes(c))) {
// We allow link here too for convenience because it's completely arch independent // We allow link here too for convenience because it's completely arch independent
console.error("Multiple targets only supported with the copy command"); console.error("Multiple targets only supported with the copy command");
return; return;
@ -145,7 +131,7 @@ async function main(): Promise<void> {
for (const cmd of cmds) { for (const cmd of cmds) {
if (GENERALCOMMANDS.includes(cmd)) { if (GENERALCOMMANDS.includes(cmd)) {
if (cmd === 'target') { if (cmd === "target") {
console.log(hakEnv.getNodeTriple()); console.log(hakEnv.getNodeTriple());
} }
return; return;
@ -160,15 +146,12 @@ async function main(): Promise<void> {
process.exit(1); process.exit(1);
} }
const cmdFunc = (await import('./' + cmd)).default; const cmdFunc = (await import("./" + cmd)).default;
for (const mod of modules) { for (const mod of modules) {
const depInfo = deps[mod]; const depInfo = deps[mod];
if (depInfo === undefined) { if (depInfo === undefined) {
console.log( console.log("Module " + mod + " not found - is it in hakDependencies " + "in your package.json?");
"Module " + mod + " not found - is it in hakDependencies " +
"in your package.json?",
);
process.exit(1); process.exit(1);
} }
console.log("hak " + cmd + ": " + mod); console.log("hak " + cmd + ": " + mod);
@ -177,7 +160,7 @@ async function main(): Promise<void> {
} }
} }
main().catch(err => { main().catch((err) => {
console.error(err); console.error(err);
process.exit(1); process.exit(1);
}); });

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import path from 'path'; import path from "path";
import os from 'os'; import os from "os";
import fsProm from 'fs/promises'; import fsProm from "fs/promises";
import childProcess from 'child_process'; import childProcess from "child_process";
import HakEnv from './hakEnv'; import HakEnv from "./hakEnv";
import { DependencyInfo } from './dep'; import { DependencyInfo } from "./dep";
export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> { export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const yarnrc = path.join(hakEnv.projectRoot, '.yarnrc'); const yarnrc = path.join(hakEnv.projectRoot, ".yarnrc");
// this is fairly terrible but it's reasonably clunky to either parse a yarnrc // this is fairly terrible but it's reasonably clunky to either parse a yarnrc
// properly or get yarn to do it, so this will probably suffice for now. // properly or get yarn to do it, so this will probably suffice for now.
// We just check to see if there is a local .yarnrc at all, and assume that // We just check to see if there is a local .yarnrc at all, and assume that
@ -43,28 +43,28 @@ export default async function link(hakEnv: HakEnv, moduleInfo: DependencyInfo):
// (ie. Windows absolute paths) but strings in quotes get parsed as // (ie. Windows absolute paths) but strings in quotes get parsed as
// JSON so need to be valid JSON encoded strings (ie. have the // JSON so need to be valid JSON encoded strings (ie. have the
// backslashes escaped). JSON.stringify will add quotes and escape. // backslashes escaped). JSON.stringify will add quotes and escape.
'--link-folder ' + JSON.stringify(path.join(hakEnv.dotHakDir, 'links')) + os.EOL, "--link-folder " + JSON.stringify(path.join(hakEnv.dotHakDir, "links")) + os.EOL,
); );
} }
const yarnCmd = 'yarn' + (hakEnv.isWin() ? '.cmd' : ''); const yarnCmd = "yarn" + (hakEnv.isWin() ? ".cmd" : "");
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(yarnCmd, ['link'], { const proc = childProcess.spawn(yarnCmd, ["link"], {
cwd: moduleInfo.moduleOutDir, cwd: moduleInfo.moduleOutDir,
stdio: 'inherit', stdio: "inherit",
}); });
proc.on('exit', code => { proc.on("exit", (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = childProcess.spawn(yarnCmd, ['link', moduleInfo.name], { const proc = childProcess.spawn(yarnCmd, ["link", moduleInfo.name], {
cwd: hakEnv.projectRoot, cwd: hakEnv.projectRoot,
stdio: 'inherit', stdio: "inherit",
}); });
proc.on('exit', code => { proc.on("exit", (code) => {
code ? reject(code) : resolve(); code ? reject(code) : resolve();
}); });
}); });

View file

@ -20,29 +20,29 @@ import { GLIBC, MUSL, family as processLibC } from "detect-libc";
// details in a single string. // details in a single string.
// See https://doc.rust-lang.org/rustc/platform-support.html. // See https://doc.rust-lang.org/rustc/platform-support.html.
export type TargetId = export type TargetId =
'aarch64-apple-darwin' | | "aarch64-apple-darwin"
'x86_64-apple-darwin' | | "x86_64-apple-darwin"
'universal-apple-darwin' | | "universal-apple-darwin"
'i686-pc-windows-msvc' | | "i686-pc-windows-msvc"
'x86_64-pc-windows-msvc' | | "x86_64-pc-windows-msvc"
'i686-unknown-linux-musl' | | "i686-unknown-linux-musl"
'i686-unknown-linux-gnu' | | "i686-unknown-linux-gnu"
'x86_64-unknown-linux-musl' | | "x86_64-unknown-linux-musl"
'x86_64-unknown-linux-gnu' | | "x86_64-unknown-linux-gnu"
'aarch64-unknown-linux-musl' | | "aarch64-unknown-linux-musl"
'aarch64-unknown-linux-gnu' | | "aarch64-unknown-linux-gnu"
'powerpc64le-unknown-linux-musl' | | "powerpc64le-unknown-linux-musl"
'powerpc64le-unknown-linux-gnu'; | "powerpc64le-unknown-linux-gnu";
// Values are expected to match those used in `process.platform`. // Values are expected to match those used in `process.platform`.
export type Platform = 'darwin' | 'linux' | 'win32'; export type Platform = "darwin" | "linux" | "win32";
// Values are expected to match those used in `process.arch`. // Values are expected to match those used in `process.arch`.
export type Arch = 'arm64' | 'ia32' | 'x64' | 'ppc64' | 'universal'; export type Arch = "arm64" | "ia32" | "x64" | "ppc64" | "universal";
// Values are expected to match those used by Visual Studio's `vcvarsall.bat`. // Values are expected to match those used by Visual Studio's `vcvarsall.bat`.
// See https://docs.microsoft.com/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax // See https://docs.microsoft.com/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax
export type VcVarsArch = 'amd64' | 'arm64' | 'x86'; export type VcVarsArch = "amd64" | "arm64" | "x86";
export type Target = { export type Target = {
id: TargetId; id: TargetId;
@ -51,140 +51,135 @@ export type Target = {
}; };
export type WindowsTarget = Target & { export type WindowsTarget = Target & {
platform: 'win32'; platform: "win32";
vcVarsArch: VcVarsArch; vcVarsArch: VcVarsArch;
}; };
export type LinuxTarget = Target & { export type LinuxTarget = Target & {
platform: 'linux'; platform: "linux";
libC: typeof processLibC; libC: typeof processLibC;
}; };
export type UniversalTarget = Target & { export type UniversalTarget = Target & {
arch: 'universal'; arch: "universal";
subtargets: Target[]; subtargets: Target[];
}; };
const aarch64AppleDarwin: Target = { const aarch64AppleDarwin: Target = {
id: 'aarch64-apple-darwin', id: "aarch64-apple-darwin",
platform: 'darwin', platform: "darwin",
arch: 'arm64', arch: "arm64",
}; };
const x8664AppleDarwin: Target = { const x8664AppleDarwin: Target = {
id: 'x86_64-apple-darwin', id: "x86_64-apple-darwin",
platform: 'darwin', platform: "darwin",
arch: 'x64', arch: "x64",
}; };
const universalAppleDarwin: UniversalTarget = { const universalAppleDarwin: UniversalTarget = {
id: 'universal-apple-darwin', id: "universal-apple-darwin",
platform: 'darwin', platform: "darwin",
arch: 'universal', arch: "universal",
subtargets: [ subtargets: [aarch64AppleDarwin, x8664AppleDarwin],
aarch64AppleDarwin,
x8664AppleDarwin,
],
}; };
const i686PcWindowsMsvc: WindowsTarget = { const i686PcWindowsMsvc: WindowsTarget = {
id: 'i686-pc-windows-msvc', id: "i686-pc-windows-msvc",
platform: 'win32', platform: "win32",
arch: 'ia32', arch: "ia32",
vcVarsArch: 'x86', vcVarsArch: "x86",
}; };
const x8664PcWindowsMsvc: WindowsTarget = { const x8664PcWindowsMsvc: WindowsTarget = {
id: 'x86_64-pc-windows-msvc', id: "x86_64-pc-windows-msvc",
platform: 'win32', platform: "win32",
arch: 'x64', arch: "x64",
vcVarsArch: 'amd64', vcVarsArch: "amd64",
}; };
const x8664UnknownLinuxGnu: LinuxTarget = { const x8664UnknownLinuxGnu: LinuxTarget = {
id: 'x86_64-unknown-linux-gnu', id: "x86_64-unknown-linux-gnu",
platform: 'linux', platform: "linux",
arch: 'x64', arch: "x64",
libC: GLIBC, libC: GLIBC,
}; };
const x8664UnknownLinuxMusl: LinuxTarget = { const x8664UnknownLinuxMusl: LinuxTarget = {
id: 'x86_64-unknown-linux-musl', id: "x86_64-unknown-linux-musl",
platform: 'linux', platform: "linux",
arch: 'x64', arch: "x64",
libC: MUSL, libC: MUSL,
}; };
const i686UnknownLinuxGnu: LinuxTarget = { const i686UnknownLinuxGnu: LinuxTarget = {
id: 'i686-unknown-linux-gnu', id: "i686-unknown-linux-gnu",
platform: 'linux', platform: "linux",
arch: 'ia32', arch: "ia32",
libC: GLIBC, libC: GLIBC,
}; };
const i686UnknownLinuxMusl: LinuxTarget = { const i686UnknownLinuxMusl: LinuxTarget = {
id: 'i686-unknown-linux-musl', id: "i686-unknown-linux-musl",
platform: 'linux', platform: "linux",
arch: 'ia32', arch: "ia32",
libC: MUSL, libC: MUSL,
}; };
const aarch64UnknownLinuxGnu: LinuxTarget = { const aarch64UnknownLinuxGnu: LinuxTarget = {
id: 'aarch64-unknown-linux-gnu', id: "aarch64-unknown-linux-gnu",
platform: 'linux', platform: "linux",
arch: 'arm64', arch: "arm64",
libC: GLIBC, libC: GLIBC,
}; };
const aarch64UnknownLinuxMusl: LinuxTarget = { const aarch64UnknownLinuxMusl: LinuxTarget = {
id: 'aarch64-unknown-linux-musl', id: "aarch64-unknown-linux-musl",
platform: 'linux', platform: "linux",
arch: 'arm64', arch: "arm64",
libC: MUSL, libC: MUSL,
}; };
const powerpc64leUnknownLinuxGnu: LinuxTarget = { const powerpc64leUnknownLinuxGnu: LinuxTarget = {
id: 'powerpc64le-unknown-linux-gnu', id: "powerpc64le-unknown-linux-gnu",
platform: 'linux', platform: "linux",
arch: 'ppc64', arch: "ppc64",
libC: GLIBC, libC: GLIBC,
}; };
const powerpc64leUnknownLinuxMusl: LinuxTarget = { const powerpc64leUnknownLinuxMusl: LinuxTarget = {
id: 'powerpc64le-unknown-linux-musl', id: "powerpc64le-unknown-linux-musl",
platform: 'linux', platform: "linux",
arch: 'ppc64', arch: "ppc64",
libC: MUSL, libC: MUSL,
}; };
export const TARGETS: Record<TargetId, Target> = { export const TARGETS: Record<TargetId, Target> = {
// macOS // macOS
'aarch64-apple-darwin': aarch64AppleDarwin, "aarch64-apple-darwin": aarch64AppleDarwin,
'x86_64-apple-darwin': x8664AppleDarwin, "x86_64-apple-darwin": x8664AppleDarwin,
'universal-apple-darwin': universalAppleDarwin, "universal-apple-darwin": universalAppleDarwin,
// Windows // Windows
'i686-pc-windows-msvc': i686PcWindowsMsvc, "i686-pc-windows-msvc": i686PcWindowsMsvc,
'x86_64-pc-windows-msvc': x8664PcWindowsMsvc, "x86_64-pc-windows-msvc": x8664PcWindowsMsvc,
// Linux // Linux
'i686-unknown-linux-musl': i686UnknownLinuxMusl, "i686-unknown-linux-musl": i686UnknownLinuxMusl,
'i686-unknown-linux-gnu': i686UnknownLinuxGnu, "i686-unknown-linux-gnu": i686UnknownLinuxGnu,
'x86_64-unknown-linux-musl': x8664UnknownLinuxMusl, "x86_64-unknown-linux-musl": x8664UnknownLinuxMusl,
'x86_64-unknown-linux-gnu': x8664UnknownLinuxGnu, "x86_64-unknown-linux-gnu": x8664UnknownLinuxGnu,
'aarch64-unknown-linux-musl': aarch64UnknownLinuxMusl, "aarch64-unknown-linux-musl": aarch64UnknownLinuxMusl,
'aarch64-unknown-linux-gnu': aarch64UnknownLinuxGnu, "aarch64-unknown-linux-gnu": aarch64UnknownLinuxGnu,
'powerpc64le-unknown-linux-musl': powerpc64leUnknownLinuxMusl, "powerpc64le-unknown-linux-musl": powerpc64leUnknownLinuxMusl,
'powerpc64le-unknown-linux-gnu': powerpc64leUnknownLinuxGnu, "powerpc64le-unknown-linux-gnu": powerpc64leUnknownLinuxGnu,
}; };
export function getHost(): Target | undefined { export function getHost(): Target | undefined {
return Object.values(TARGETS).find(target => ( return Object.values(TARGETS).find(
target.platform === process.platform && (target) =>
target.arch === process.arch && target.platform === process.platform &&
( target.arch === process.arch &&
process.platform !== 'linux' || (process.platform !== "linux" || (target as LinuxTarget).libC === processLibC),
(target as LinuxTarget).libC === processLibC );
)
));
} }
export function isHostId(id: TargetId): boolean { export function isHostId(id: TargetId): boolean {

View file

@ -11,31 +11,35 @@ import * as childProcess from "child_process";
export async function versionFromAsar(): Promise<string> { export async function versionFromAsar(): Promise<string> {
try { try {
await fs.stat('webapp.asar'); await fs.stat("webapp.asar");
} catch (e) { } catch (e) {
throw new Error("No 'webapp.asar' found. Run 'yarn run fetch'"); throw new Error("No 'webapp.asar' found. Run 'yarn run fetch'");
} }
return asar.extractFile('webapp.asar', 'version').toString().trim(); return asar.extractFile("webapp.asar", "version").toString().trim();
} }
export async function setPackageVersion(ver: string): Promise<void> { export async function setPackageVersion(ver: string): Promise<void> {
// set version in package.json: electron-builder will use this to populate // set version in package.json: electron-builder will use this to populate
// all the various version fields // all the various version fields
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
childProcess.execFile(process.platform === 'win32' ? 'yarn.cmd' : 'yarn', [ childProcess.execFile(
'version', process.platform === "win32" ? "yarn.cmd" : "yarn",
'-s', [
'--no-git-tag-version', // This also means "don't commit to git" as it turns out "version",
'--new-version', "-s",
ver, "--no-git-tag-version", // This also means "don't commit to git" as it turns out
], (err) => { "--new-version",
if (err) { ver,
reject(err); ],
} else { (err) => {
resolve(); if (err) {
} reject(err);
}); } else {
resolve();
}
},
);
}); });
} }
@ -49,10 +53,12 @@ async function main(args: string[]): Promise<number> {
} }
if (require.main === module) { if (require.main === module) {
main(process.argv.slice(2)).then((ret) => { main(process.argv.slice(2))
process.exit(ret); .then((ret) => {
}).catch(e => { process.exit(ret);
console.error(e); })
process.exit(1); .catch((e) => {
}); console.error(e);
process.exit(1);
});
} }

View file

@ -1,21 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true, "resolveJsonModule": true,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"target": "es2017", "target": "es2017",
"module": "commonjs", "module": "commonjs",
"sourceMap": false, "sourceMap": false,
"strict": true, "strict": true,
"lib": [ "lib": ["es2019", "dom"]
"es2019", },
"dom" "include": ["./**/*.ts"],
] "ts-node": {
}, "transpileOnly": true
"include": [ }
"./**/*.ts"
],
"ts-node": {
"transpileOnly": true
}
} }

View file

@ -50,5 +50,5 @@ declare module "keytar" {
* *
* @returns A promise for the array of found credentials. * @returns A promise for the array of found credentials.
*/ */
export function findCredentials(service: string): Promise<Array<{ account: string, password: string}>>; export function findCredentials(service: string): Promise<Array<{ account: string; password: string }>>;
} }

View file

@ -19,18 +19,11 @@ limitations under the License.
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc. // Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
import "./squirrelhooks"; import "./squirrelhooks";
import { import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog } from "electron";
app,
BrowserWindow,
Menu,
autoUpdater,
protocol,
dialog,
} from "electron";
import AutoLaunch from "auto-launch"; import AutoLaunch from "auto-launch";
import path from "path"; import path from "path";
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from "electron-window-state";
import Store from 'electron-store'; import Store from "electron-store";
import fs, { promises as afs } from "fs"; import fs, { promises as afs } from "fs";
import { URL } from "url"; import { URL } from "url";
import minimist from "minimist"; import minimist from "minimist";
@ -40,11 +33,11 @@ import "./keytar";
import "./seshat"; import "./seshat";
import "./settings"; import "./settings";
import * as tray from "./tray"; import * as tray from "./tray";
import { buildMenuTemplate } from './vectormenu'; import { buildMenuTemplate } from "./vectormenu";
import webContentsHandler from './webcontents-handler'; import webContentsHandler from "./webcontents-handler";
import * as updater from './updater'; import * as updater from "./updater";
import { getProfileFromDeeplink, protocolInit } from './protocol'; import { getProfileFromDeeplink, protocolInit } from "./protocol";
import { _t, AppLocalization } from './language-helper'; import { _t, AppLocalization } from "./language-helper";
import Input = Electron.Input; import Input = Electron.Input;
const argv = minimist(process.argv, { const argv = minimist(process.argv, {
@ -65,8 +58,7 @@ if (argv["help"]) {
console.log(" --no-update: Disable automatic updating."); console.log(" --no-update: Disable automatic updating.");
console.log(" --hidden: Start the application hidden in the system tray."); console.log(" --hidden: Start the application hidden in the system tray.");
console.log(" --help: Displays this help message."); console.log(" --help: Displays this help message.");
console.log("And more such as --proxy, see:" + console.log("And more such as --proxy, see:" + "https://electronjs.org/docs/api/command-line-switches");
"https://electronjs.org/docs/api/command-line-switches");
app.exit(); app.exit();
} }
@ -74,7 +66,7 @@ if (argv["help"]) {
// as soon as the app path is set, so pick a random path in it that must exist if it's a // as soon as the app path is set, so pick a random path in it that must exist if it's a
// real user data directory. // real user data directory.
function isRealUserDataDir(d: string): boolean { function isRealUserDataDir(d: string): boolean {
return fs.existsSync(path.join(d, 'IndexedDB')); return fs.existsSync(path.join(d, "IndexedDB"));
} }
// check if we are passed a profile in the SSO callback url // check if we are passed a profile in the SSO callback url
@ -83,22 +75,22 @@ let userDataPath: string;
const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]); const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]);
if (userDataPathInProtocol) { if (userDataPathInProtocol) {
userDataPath = userDataPathInProtocol; userDataPath = userDataPathInProtocol;
} else if (argv['profile-dir']) { } else if (argv["profile-dir"]) {
userDataPath = argv['profile-dir']; userDataPath = argv["profile-dir"];
} else { } else {
let newUserDataPath = app.getPath('userData'); let newUserDataPath = app.getPath("userData");
if (argv['profile']) { if (argv["profile"]) {
newUserDataPath += '-' + argv['profile']; newUserDataPath += "-" + argv["profile"];
} }
const newUserDataPathExists = isRealUserDataDir(newUserDataPath); const newUserDataPathExists = isRealUserDataDir(newUserDataPath);
let oldUserDataPath = path.join(app.getPath('appData'), app.getName().replace('Element', 'Riot')); let oldUserDataPath = path.join(app.getPath("appData"), app.getName().replace("Element", "Riot"));
if (argv['profile']) { if (argv["profile"]) {
oldUserDataPath += '-' + argv['profile']; oldUserDataPath += "-" + argv["profile"];
} }
const oldUserDataPathExists = isRealUserDataDir(oldUserDataPath); const oldUserDataPathExists = isRealUserDataDir(oldUserDataPath);
console.log(newUserDataPath + " exists: " + (newUserDataPathExists ? 'yes' : 'no')); console.log(newUserDataPath + " exists: " + (newUserDataPathExists ? "yes" : "no"));
console.log(oldUserDataPath + " exists: " + (oldUserDataPathExists ? 'yes' : 'no')); console.log(oldUserDataPath + " exists: " + (oldUserDataPathExists ? "yes" : "no"));
if (!newUserDataPathExists && oldUserDataPathExists) { if (!newUserDataPathExists && oldUserDataPathExists) {
console.log("Using legacy user data path: " + oldUserDataPath); console.log("Using legacy user data path: " + oldUserDataPath);
userDataPath = oldUserDataPath; userDataPath = oldUserDataPath;
@ -106,54 +98,53 @@ if (userDataPathInProtocol) {
userDataPath = newUserDataPath; userDataPath = newUserDataPath;
} }
} }
app.setPath('userData', userDataPath); app.setPath("userData", userDataPath);
async function tryPaths(name: string, root: string, rawPaths: string[]): Promise<string> { async function tryPaths(name: string, root: string, rawPaths: string[]): Promise<string> {
// Make everything relative to root // Make everything relative to root
const paths = rawPaths.map(p => path.join(root, p)); const paths = rawPaths.map((p) => path.join(root, p));
for (const p of paths) { for (const p of paths) {
try { try {
await afs.stat(p); await afs.stat(p);
return p + '/'; return p + "/";
} catch (e) { } catch (e) {}
}
} }
console.log(`Couldn't find ${name} files in any of: `); console.log(`Couldn't find ${name} files in any of: `);
for (const p of paths) { for (const p of paths) {
console.log("\t"+path.resolve(p)); console.log("\t" + path.resolve(p));
} }
throw new Error(`Failed to find ${name} files`); throw new Error(`Failed to find ${name} files`);
} }
const homeserverProps = ['default_is_url', 'default_hs_url', 'default_server_name', 'default_server_config'] as const; const homeserverProps = ["default_is_url", "default_hs_url", "default_server_name", "default_server_config"] as const;
// Find the webapp resources and set up things that require them // Find the webapp resources and set up things that require them
async function setupGlobals(): Promise<void> { async function setupGlobals(): Promise<void> {
// find the webapp asar. // find the webapp asar.
asarPath = await tryPaths("webapp", __dirname, [ asarPath = await tryPaths("webapp", __dirname, [
// If run from the source checkout, this will be in the directory above // If run from the source checkout, this will be in the directory above
'../webapp.asar', "../webapp.asar",
// but if run from a packaged application, electron-main.js will be in // but if run from a packaged application, electron-main.js will be in
// a different asar file so it will be two levels above // a different asar file so it will be two levels above
'../../webapp.asar', "../../webapp.asar",
// also try without the 'asar' suffix to allow symlinking in a directory // also try without the 'asar' suffix to allow symlinking in a directory
'../webapp', "../webapp",
// from a packaged application // from a packaged application
'../../webapp', "../../webapp",
]); ]);
// we assume the resources path is in the same place as the asar // we assume the resources path is in the same place as the asar
resPath = await tryPaths("res", path.dirname(asarPath), [ resPath = await tryPaths("res", path.dirname(asarPath), [
// If run from the source checkout // If run from the source checkout
'res', "res",
// if run from packaged application // if run from packaged application
'', "",
]); ]);
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
global.vectorConfig = require(asarPath + 'config.json'); global.vectorConfig = require(asarPath + "config.json");
} catch (e) { } catch (e) {
// it would be nice to check the error code here and bail if the config // it would be nice to check the error code here and bail if the config
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing // is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
@ -165,15 +156,15 @@ async function setupGlobals(): Promise<void> {
try { try {
// Load local config and use it to override values from the one baked with the build // Load local config and use it to override values from the one baked with the build
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const localConfig = require(path.join(app.getPath('userData'), 'config.json')); const localConfig = require(path.join(app.getPath("userData"), "config.json"));
// If the local config has a homeserver defined, don't use the homeserver from the build // If the local config has a homeserver defined, don't use the homeserver from the build
// config. This is to avoid a problem where Riot thinks there are multiple homeservers // config. This is to avoid a problem where Riot thinks there are multiple homeservers
// defined, and panics as a result. // defined, and panics as a result.
if (Object.keys(localConfig).find(k => homeserverProps.includes(<any>k))) { if (Object.keys(localConfig).find((k) => homeserverProps.includes(<any>k))) {
// Rip out all the homeserver options from the vector config // Rip out all the homeserver options from the vector config
global.vectorConfig = Object.keys(global.vectorConfig) global.vectorConfig = Object.keys(global.vectorConfig)
.filter(k => !homeserverProps.includes(<any>k)) .filter((k) => !homeserverProps.includes(<any>k))
.reduce((obj, key) => { .reduce((obj, key) => {
obj[key] = global.vectorConfig[key]; obj[key] = global.vectorConfig[key];
return obj; return obj;
@ -185,9 +176,10 @@ async function setupGlobals(): Promise<void> {
if (e instanceof SyntaxError) { if (e instanceof SyntaxError) {
dialog.showMessageBox({ dialog.showMessageBox({
type: "error", type: "error",
title: `Your ${global.vectorConfig.brand || 'Element'} is misconfigured`, title: `Your ${global.vectorConfig.brand || "Element"} is misconfigured`,
message: `Your custom ${global.vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` + message:
`Please correct the problem and reopen ${global.vectorConfig.brand || 'Element'}.`, `Your custom ${global.vectorConfig.brand || "Element"} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${global.vectorConfig.brand || "Element"}.`,
detail: e.message || "", detail: e.message || "",
}); });
} }
@ -197,16 +189,16 @@ async function setupGlobals(): Promise<void> {
// The tray icon // The tray icon
// It's important to call `path.join` so we don't end up with the packaged asar in the final path. // It's important to call `path.join` so we don't end up with the packaged asar in the final path.
const iconFile = `element.${process.platform === 'win32' ? 'ico' : 'png'}`; const iconFile = `element.${process.platform === "win32" ? "ico" : "png"}`;
iconPath = path.join(resPath, "img", iconFile); iconPath = path.join(resPath, "img", iconFile);
global.trayConfig = { global.trayConfig = {
icon_path: iconPath, icon_path: iconPath,
brand: global.vectorConfig.brand || 'Element', brand: global.vectorConfig.brand || "Element",
}; };
// launcher // launcher
global.launcher = new AutoLaunch({ global.launcher = new AutoLaunch({
name: global.vectorConfig.brand || 'Element', name: global.vectorConfig.brand || "Element",
isHidden: true, isHidden: true,
mac: { mac: {
useLaunchAgent: true, useLaunchAgent: true,
@ -217,9 +209,9 @@ async function setupGlobals(): Promise<void> {
async function moveAutoLauncher(): Promise<void> { async function moveAutoLauncher(): Promise<void> {
// Look for an auto-launcher under 'Riot' and if we find one, port it's // Look for an auto-launcher under 'Riot' and if we find one, port it's
// enabled/disabled-ness over to the new 'Element' launcher // enabled/disabled-ness over to the new 'Element' launcher
if (!global.vectorConfig.brand || global.vectorConfig.brand === 'Element') { if (!global.vectorConfig.brand || global.vectorConfig.brand === "Element") {
const oldLauncher = new AutoLaunch({ const oldLauncher = new AutoLaunch({
name: 'Riot', name: "Riot",
isHidden: true, isHidden: true,
mac: { mac: {
useLaunchAgent: true, useLaunchAgent: true,
@ -238,26 +230,30 @@ global.store = new Store({ name: "electron-config" });
global.appQuitting = false; global.appQuitting = false;
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [ const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
(input, platform): boolean => platform !== 'darwin' && input.alt && input.key.toUpperCase() === 'F4', (input, platform): boolean => platform !== "darwin" && input.alt && input.key.toUpperCase() === "F4",
(input, platform): boolean => platform !== 'darwin' && input.control && input.key.toUpperCase() === 'Q', (input, platform): boolean => platform !== "darwin" && input.control && input.key.toUpperCase() === "Q",
(input, platform): boolean => platform === 'darwin' && input.meta && input.key.toUpperCase() === 'Q', (input, platform): boolean => platform === "darwin" && input.meta && input.key.toUpperCase() === "Q",
]; ];
const warnBeforeExit = (event: Event, input: Input): void => { const warnBeforeExit = (event: Event, input: Input): void => {
const shouldWarnBeforeExit = global.store.get('warnBeforeExit', true); const shouldWarnBeforeExit = global.store.get("warnBeforeExit", true);
const exitShortcutPressed = const exitShortcutPressed =
input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform)); input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));
if (shouldWarnBeforeExit && exitShortcutPressed && global.mainWindow) { if (shouldWarnBeforeExit && exitShortcutPressed && global.mainWindow) {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(global.mainWindow, { const shouldCancelCloseRequest =
type: "question", dialog.showMessageBoxSync(global.mainWindow, {
buttons: [_t("Cancel"), _t("Close %(brand)s", { type: "question",
brand: global.vectorConfig.brand || 'Element', buttons: [
})], _t("Cancel"),
message: _t("Are you sure you want to quit?"), _t("Close %(brand)s", {
defaultId: 1, brand: global.vectorConfig.brand || "Element",
cancelId: 0, }),
}) === 0; ],
message: _t("Are you sure you want to quit?"),
defaultId: 1,
cancelId: 0,
}) === 0;
if (shouldCancelCloseRequest) { if (shouldCancelCloseRequest) {
event.preventDefault(); event.preventDefault();
@ -271,18 +267,18 @@ const warnBeforeExit = (event: Event, input: Input): void => {
// no other way to catch this error). // no other way to catch this error).
// Assuming we generally run from the console when developing, // Assuming we generally run from the console when developing,
// this is far preferable. // this is far preferable.
process.on('uncaughtException', function(error: Error): void { process.on("uncaughtException", function (error: Error): void {
console.log('Unhandled exception', error); console.log("Unhandled exception", error);
}); });
app.commandLine.appendSwitch('--enable-usermedia-screen-capturing'); app.commandLine.appendSwitch("--enable-usermedia-screen-capturing");
if (!app.commandLine.hasSwitch('enable-features')) { if (!app.commandLine.hasSwitch("enable-features")) {
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer'); app.commandLine.appendSwitch("enable-features", "WebRTCPipeWireCapturer");
} }
const gotLock = app.requestSingleInstanceLock(); const gotLock = app.requestSingleInstanceLock();
if (!gotLock) { if (!gotLock) {
console.log('Other instance detected: exiting'); console.log("Other instance detected: exiting");
app.exit(); app.exit();
} }
@ -294,14 +290,16 @@ protocolInit();
// work. // work.
// Also mark it as secure (ie. accessing resources from this // Also mark it as secure (ie. accessing resources from this
// protocol and HTTPS won't trigger mixed content warnings). // protocol and HTTPS won't trigger mixed content warnings).
protocol.registerSchemesAsPrivileged([{ protocol.registerSchemesAsPrivileged([
scheme: 'vector', {
privileges: { scheme: "vector",
standard: true, privileges: {
secure: true, standard: true,
supportFetchAPI: true, secure: true,
supportFetchAPI: true,
},
}, },
}]); ]);
// Turn the sandbox on for *all* windows we might generate. Doing this means we don't // Turn the sandbox on for *all* windows we might generate. Doing this means we don't
// have to specify a `sandbox: true` to each BrowserWindow. // have to specify a `sandbox: true` to each BrowserWindow.
@ -315,15 +313,15 @@ protocol.registerSchemesAsPrivileged([{
app.enableSandbox(); app.enableSandbox();
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704 // We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService'); app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
// Disable hardware acceleration if the setting has been set. // Disable hardware acceleration if the setting has been set.
if (global.store.get('disableHardwareAcceleration', false) === true) { if (global.store.get("disableHardwareAcceleration", false) === true) {
console.log("Disabling hardware acceleration."); console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
app.on('ready', async () => { app.on("ready", async () => {
try { try {
await setupGlobals(); await setupGlobals();
await moveAutoLauncher(); await moveAutoLauncher();
@ -337,51 +335,51 @@ app.on('ready', async () => {
return; return;
} }
if (argv['devtools']) { if (argv["devtools"]) {
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const { default: installExt, REACT_DEVELOPER_TOOLS, REACT_PERF } = require('electron-devtools-installer'); const { default: installExt, REACT_DEVELOPER_TOOLS, REACT_PERF } = require("electron-devtools-installer");
installExt(REACT_DEVELOPER_TOOLS) installExt(REACT_DEVELOPER_TOOLS)
.then((name: string) => console.log(`Added Extension: ${name}`)) .then((name: string) => console.log(`Added Extension: ${name}`))
.catch((err: unknown) => console.log('An error occurred: ', err)); .catch((err: unknown) => console.log("An error occurred: ", err));
installExt(REACT_PERF) installExt(REACT_PERF)
.then((name: string) => console.log(`Added Extension: ${name}`)) .then((name: string) => console.log(`Added Extension: ${name}`))
.catch((err: unknown) => console.log('An error occurred: ', err)); .catch((err: unknown) => console.log("An error occurred: ", err));
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
} }
protocol.registerFileProtocol('vector', (request, callback) => { protocol.registerFileProtocol("vector", (request, callback) => {
if (request.method !== 'GET') { if (request.method !== "GET") {
callback({ error: -322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h callback({ error: -322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
return null; return null;
} }
const parsedUrl = new URL(request.url); const parsedUrl = new URL(request.url);
if (parsedUrl.protocol !== 'vector:') { if (parsedUrl.protocol !== "vector:") {
callback({ error: -302 }); // UNKNOWN_URL_SCHEME callback({ error: -302 }); // UNKNOWN_URL_SCHEME
return; return;
} }
if (parsedUrl.host !== 'vector') { if (parsedUrl.host !== "vector") {
callback({ error: -105 }); // NAME_NOT_RESOLVED callback({ error: -105 }); // NAME_NOT_RESOLVED
return; return;
} }
const target = parsedUrl.pathname.split('/'); const target = parsedUrl.pathname.split("/");
// path starts with a '/' // path starts with a '/'
if (target[0] !== '') { if (target[0] !== "") {
callback({ error: -6 }); // FILE_NOT_FOUND callback({ error: -6 }); // FILE_NOT_FOUND
return; return;
} }
if (target[target.length - 1] == '') { if (target[target.length - 1] == "") {
target[target.length - 1] = 'index.html'; target[target.length - 1] = "index.html";
} }
let baseDir: string; let baseDir: string;
if (target[1] === 'webapp') { if (target[1] === "webapp") {
baseDir = asarPath; baseDir = asarPath;
} else { } else {
callback({ error: -6 }); // FILE_NOT_FOUND callback({ error: -6 }); // FILE_NOT_FOUND
@ -393,7 +391,7 @@ app.on('ready', async () => {
baseDir = path.normalize(baseDir); baseDir = path.normalize(baseDir);
const relTarget = path.normalize(path.join(...target.slice(2))); const relTarget = path.normalize(path.join(...target.slice(2)));
if (relTarget.startsWith('..')) { if (relTarget.startsWith("..")) {
callback({ error: -6 }); // FILE_NOT_FOUND callback({ error: -6 }); // FILE_NOT_FOUND
return; return;
} }
@ -404,13 +402,13 @@ app.on('ready', async () => {
}); });
}); });
if (argv['no-update']) { if (argv["no-update"]) {
console.log('Auto update disabled via command line flag "--no-update"'); console.log('Auto update disabled via command line flag "--no-update"');
} else if (global.vectorConfig['update_base_url']) { } else if (global.vectorConfig["update_base_url"]) {
console.log(`Starting auto update with base URL: ${global.vectorConfig['update_base_url']}`); console.log(`Starting auto update with base URL: ${global.vectorConfig["update_base_url"]}`);
updater.start(global.vectorConfig['update_base_url']); updater.start(global.vectorConfig["update_base_url"]);
} else { } else {
console.log('No update_base_url is defined: auto update is disabled'); console.log("No update_base_url is defined: auto update is disabled");
} }
// Load the previous window state with fallback to defaults // Load the previous window state with fallback to defaults
@ -422,11 +420,11 @@ app.on('ready', async () => {
const preloadScript = path.normalize(`${__dirname}/preload.js`); const preloadScript = path.normalize(`${__dirname}/preload.js`);
global.mainWindow = new BrowserWindow({ global.mainWindow = new BrowserWindow({
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do // https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor: '#fff', backgroundColor: "#fff",
icon: iconPath, icon: iconPath,
show: false, show: false,
autoHideMenuBar: global.store.get('autoHideMenuBar', true), autoHideMenuBar: global.store.get("autoHideMenuBar", true),
x: mainWindowState.x, x: mainWindowState.x,
y: mainWindowState.y, y: mainWindowState.y,
@ -440,20 +438,20 @@ app.on('ready', async () => {
webgl: true, webgl: true,
}, },
}); });
global.mainWindow.loadURL('vector://vector/webapp/'); global.mainWindow.loadURL("vector://vector/webapp/");
// Handle spellchecker // Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here // For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true)); global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
// Create trayIcon icon // Create trayIcon icon
if (global.store.get('minimizeToTray', true)) tray.create(global.trayConfig); if (global.store.get("minimizeToTray", true)) tray.create(global.trayConfig);
global.mainWindow.once('ready-to-show', () => { global.mainWindow.once("ready-to-show", () => {
if (!global.mainWindow) return; if (!global.mainWindow) return;
mainWindowState.manage(global.mainWindow); mainWindowState.manage(global.mainWindow);
if (!argv['hidden']) { if (!argv["hidden"]) {
global.mainWindow.show(); global.mainWindow.show();
} else { } else {
// hide here explicitly because window manage above sometimes shows it // hide here explicitly because window manage above sometimes shows it
@ -461,21 +459,21 @@ app.on('ready', async () => {
} }
}); });
global.mainWindow.webContents.on('before-input-event', warnBeforeExit); global.mainWindow.webContents.on("before-input-event", warnBeforeExit);
global.mainWindow.on('closed', () => { global.mainWindow.on("closed", () => {
global.mainWindow = null; global.mainWindow = null;
}); });
global.mainWindow.on('close', async (e) => { global.mainWindow.on("close", async (e) => {
// If we are not quitting and have a tray icon then minimize to tray // If we are not quitting and have a tray icon then minimize to tray
if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) { if (!global.appQuitting && (tray.hasTray() || process.platform === "darwin")) {
// On Mac, closing the window just hides it // On Mac, closing the window just hides it
// (this is generally how single-window Mac apps // (this is generally how single-window Mac apps
// behave, eg. Mail.app) // behave, eg. Mail.app)
e.preventDefault(); e.preventDefault();
if (global.mainWindow?.isFullScreen()) { if (global.mainWindow?.isFullScreen()) {
global.mainWindow.once('leave-full-screen', () => global.mainWindow?.hide()); global.mainWindow.once("leave-full-screen", () => global.mainWindow?.hide());
global.mainWindow.setFullScreen(false); global.mainWindow.setFullScreen(false);
} else { } else {
@ -486,12 +484,12 @@ app.on('ready', async () => {
} }
}); });
if (process.platform === 'win32') { if (process.platform === "win32") {
// Handle forward/backward mouse buttons in Windows // Handle forward/backward mouse buttons in Windows
global.mainWindow.on('app-command', (e, cmd) => { global.mainWindow.on("app-command", (e, cmd) => {
if (cmd === 'browser-backward' && global.mainWindow?.webContents.canGoBack()) { if (cmd === "browser-backward" && global.mainWindow?.webContents.canGoBack()) {
global.mainWindow.webContents.goBack(); global.mainWindow.webContents.goBack();
} else if (cmd === 'browser-forward' && global.mainWindow?.webContents.canGoForward()) { } else if (cmd === "browser-forward" && global.mainWindow?.webContents.canGoForward()) {
global.mainWindow.webContents.goForward(); global.mainWindow.webContents.goForward();
} }
}); });
@ -501,32 +499,29 @@ app.on('ready', async () => {
global.appLocalization = new AppLocalization({ global.appLocalization = new AppLocalization({
store: global.store, store: global.store,
components: [ components: [(): void => tray.initApplicationMenu(), (): void => Menu.setApplicationMenu(buildMenuTemplate())],
(): void => tray.initApplicationMenu(),
(): void => Menu.setApplicationMenu(buildMenuTemplate()),
],
}); });
}); });
app.on('window-all-closed', () => { app.on("window-all-closed", () => {
app.quit(); app.quit();
}); });
app.on('activate', () => { app.on("activate", () => {
global.mainWindow?.show(); global.mainWindow?.show();
}); });
function beforeQuit(): void { function beforeQuit(): void {
global.appQuitting = true; global.appQuitting = true;
global.mainWindow?.webContents.send('before-quit'); global.mainWindow?.webContents.send("before-quit");
} }
app.on('before-quit', beforeQuit); app.on("before-quit", beforeQuit);
autoUpdater.on('before-quit-for-update', beforeQuit); autoUpdater.on("before-quit-for-update", beforeQuit);
app.on('second-instance', (ev, commandLine, workingDirectory) => { app.on("second-instance", (ev, commandLine, workingDirectory) => {
// If other instance launched with --hidden then skip showing window // If other instance launched with --hidden then skip showing window
if (commandLine.includes('--hidden')) return; if (commandLine.includes("--hidden")) return;
// Someone tried to run a second instance, we should focus our window. // Someone tried to run a second instance, we should focus our window.
if (global.mainWindow) { if (global.mainWindow) {
@ -540,4 +535,4 @@ app.on('second-instance', (ev, commandLine, workingDirectory) => {
// installer uses for the shortcut icon. // installer uses for the shortcut icon.
// This makes notifications work on windows 8.1 (and is // This makes notifications work on windows 8.1 (and is
// a noop on other platforms). // a noop on other platforms).
app.setAppUserModelId('com.squirrel.element-desktop.Element'); app.setAppUserModelId("com.squirrel.element-desktop.Element");

View file

@ -22,8 +22,8 @@ import { randomArray } from "./utils";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { keytar } from "./keytar"; import { keytar } from "./keytar";
ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void { ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
if (process.platform !== 'win32') { if (process.platform !== "win32") {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron // only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop // has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942 // each other. See https://github.com/vector-im/element-web/issues/16942
@ -35,10 +35,10 @@ ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void {
}); });
let focusHandlerAttached = false; let focusHandlerAttached = false;
ipcMain.on('loudNotification', function(): void { ipcMain.on("loudNotification", function (): void {
if (process.platform === 'win32' && global.mainWindow && !global.mainWindow.isFocused() && !focusHandlerAttached) { if (process.platform === "win32" && global.mainWindow && !global.mainWindow.isFocused() && !focusHandlerAttached) {
global.mainWindow.flashFrame(true); global.mainWindow.flashFrame(true);
global.mainWindow.once('focus', () => { global.mainWindow.once("focus", () => {
global.mainWindow?.flashFrame(false); global.mainWindow?.flashFrame(false);
focusHandlerAttached = false; focusHandlerAttached = false;
}); });
@ -47,17 +47,17 @@ ipcMain.on('loudNotification', function(): void {
}); });
let powerSaveBlockerId: number | null = null; let powerSaveBlockerId: number | null = null;
ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) { ipcMain.on("app_onAction", function (_ev: IpcMainEvent, payload) {
switch (payload.action) { switch (payload.action) {
case 'call_state': { case "call_state": {
if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) { if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
if (payload.state === 'ended') { if (payload.state === "ended") {
powerSaveBlocker.stop(powerSaveBlockerId); powerSaveBlocker.stop(powerSaveBlockerId);
powerSaveBlockerId = null; powerSaveBlockerId = null;
} }
} else { } else {
if (powerSaveBlockerId === null && payload.state === 'connected') { if (powerSaveBlockerId === null && payload.state === "connected") {
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep'); powerSaveBlockerId = powerSaveBlocker.start("prevent-display-sleep");
} }
} }
break; break;
@ -65,35 +65,35 @@ ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) {
} }
}); });
ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) { ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
if (!global.mainWindow) return; if (!global.mainWindow) return;
const args = payload.args || []; const args = payload.args || [];
let ret: any; let ret: any;
switch (payload.name) { switch (payload.name) {
case 'getUpdateFeedUrl': case "getUpdateFeedUrl":
ret = autoUpdater.getFeedURL(); ret = autoUpdater.getFeedURL();
break; break;
case 'getSettingValue': { case "getSettingValue": {
const [settingName] = args; const [settingName] = args;
const setting = Settings[settingName]; const setting = Settings[settingName];
ret = await setting.read(); ret = await setting.read();
break; break;
} }
case 'setSettingValue': { case "setSettingValue": {
const [settingName, value] = args; const [settingName, value] = args;
const setting = Settings[settingName]; const setting = Settings[settingName];
await setting.write(value); await setting.write(value);
break; break;
} }
case 'setLanguage': case "setLanguage":
global.appLocalization.setAppLocale(args[0]); global.appLocalization.setAppLocale(args[0]);
break; break;
case 'getAppVersion': case "getAppVersion":
ret = app.getVersion(); ret = app.getVersion();
break; break;
case 'focusWindow': case "focusWindow":
if (global.mainWindow.isMinimized()) { if (global.mainWindow.isMinimized()) {
global.mainWindow.restore(); global.mainWindow.restore();
} else if (!global.mainWindow.isVisible()) { } else if (!global.mainWindow.isVisible()) {
@ -102,31 +102,31 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
global.mainWindow.focus(); global.mainWindow.focus();
} }
break; break;
case 'getConfig': case "getConfig":
ret = global.vectorConfig; ret = global.vectorConfig;
break; break;
case 'navigateBack': case "navigateBack":
if (global.mainWindow.webContents.canGoBack()) { if (global.mainWindow.webContents.canGoBack()) {
global.mainWindow.webContents.goBack(); global.mainWindow.webContents.goBack();
} }
break; break;
case 'navigateForward': case "navigateForward":
if (global.mainWindow.webContents.canGoForward()) { if (global.mainWindow.webContents.canGoForward()) {
global.mainWindow.webContents.goForward(); global.mainWindow.webContents.goForward();
} }
break; break;
case 'setSpellCheckEnabled': case "setSpellCheckEnabled":
if (typeof args[0] !== 'boolean') return; if (typeof args[0] !== "boolean") return;
global.mainWindow.webContents.session.setSpellCheckerEnabled(args[0]); global.mainWindow.webContents.session.setSpellCheckerEnabled(args[0]);
global.store.set("spellCheckerEnabled", args[0]); global.store.set("spellCheckerEnabled", args[0]);
break; break;
case 'getSpellCheckEnabled': case "getSpellCheckEnabled":
ret = global.store.get("spellCheckerEnabled", true); ret = global.store.get("spellCheckerEnabled", true);
break; break;
case 'setSpellCheckLanguages': case "setSpellCheckLanguages":
try { try {
global.mainWindow.webContents.session.setSpellCheckerLanguages(args[0]); global.mainWindow.webContents.session.setSpellCheckerLanguages(args[0]);
} catch (er) { } catch (er) {
@ -134,18 +134,18 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
} }
break; break;
case 'getSpellCheckLanguages': case "getSpellCheckLanguages":
ret = global.mainWindow.webContents.session.getSpellCheckerLanguages(); ret = global.mainWindow.webContents.session.getSpellCheckerLanguages();
break; break;
case 'getAvailableSpellCheckLanguages': case "getAvailableSpellCheckLanguages":
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages; ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
break; break;
case 'startSSOFlow': case "startSSOFlow":
recordSSOSession(args[0]); recordSSOSession(args[0]);
break; break;
case 'getPickleKey': case "getPickleKey":
try { try {
ret = await keytar?.getPassword("element.io", `${args[0]}|${args[1]}`); ret = await keytar?.getPassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be // migrate from riot.im (remove once we think there will no longer be
@ -160,7 +160,7 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
} }
break; break;
case 'createPickleKey': case "createPickleKey":
try { try {
const pickleKey = await randomArray(32); const pickleKey = await randomArray(32);
await keytar?.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey); await keytar?.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
@ -170,7 +170,7 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
} }
break; break;
case 'destroyPickleKey': case "destroyPickleKey":
try { try {
await keytar?.deletePassword("element.io", `${args[0]}|${args[1]}`); await keytar?.deletePassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be // migrate from riot.im (remove once we think there will no longer be
@ -178,7 +178,7 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
await keytar?.deletePassword("riot.im", `${args[0]}|${args[1]}`); await keytar?.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {} } catch (e) {}
break; break;
case 'getDesktopCapturerSources': case "getDesktopCapturerSources":
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({ ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
id: source.id, id: source.id,
name: source.name, name: source.name,
@ -187,16 +187,15 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
break; break;
default: default:
global.mainWindow.webContents.send('ipcReply', { global.mainWindow.webContents.send("ipcReply", {
id: payload.id, id: payload.id,
error: "Unknown IPC Call: " + payload.name, error: "Unknown IPC Call: " + payload.name,
}); });
return; return;
} }
global.mainWindow.webContents.send('ipcReply', { global.mainWindow.webContents.send("ipcReply", {
id: payload.id, id: payload.id,
reply: ret, reply: ret,
}); });
}); });

View file

@ -19,7 +19,7 @@ import type * as Keytar from "keytar"; // Hak dependency type
let keytar: typeof Keytar | undefined; let keytar: typeof Keytar | undefined;
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require('keytar'); keytar = require("keytar");
} catch (e) { } catch (e) {
if ((<NodeJS.ErrnoException>e).code === "MODULE_NOT_FOUND") { if ((<NodeJS.ErrnoException>e).code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled."); console.log("Keytar isn't installed; secure key storage is disabled.");

View file

@ -16,9 +16,9 @@ limitations under the License.
import counterpart from "counterpart"; import counterpart from "counterpart";
import type Store from 'electron-store'; import type Store from "electron-store";
const FALLBACK_LOCALE = 'en'; const FALLBACK_LOCALE = "en";
export function _td(text: string): string { export function _td(text: string): string {
return text; return text;
@ -44,11 +44,11 @@ export function _t(text: string, variables: IVariables = {}): string {
Object.keys(variables).forEach((key) => { Object.keys(variables).forEach((key) => {
if (variables[key] === undefined) { if (variables[key] === undefined) {
console.warn("safeCounterpartTranslate called with undefined interpolation name: " + key); console.warn("safeCounterpartTranslate called with undefined interpolation name: " + key);
variables[key] = 'undefined'; variables[key] = "undefined";
} }
if (variables[key] === null) { if (variables[key] === null) {
console.warn("safeCounterpartTranslate called with null interpolation name: " + key); console.warn("safeCounterpartTranslate called with null interpolation name: " + key);
variables[key] = 'null'; variables[key] = "null";
} }
}); });
let translated = counterpart.translate(text, variables); let translated = counterpart.translate(text, variables);
@ -71,10 +71,10 @@ export class AppLocalization {
private readonly store: TypedStore; private readonly store: TypedStore;
private readonly localizedComponents?: Set<Component>; private readonly localizedComponents?: Set<Component>;
public constructor({ store, components = [] }: { store: TypedStore, components: Component[] }) { public constructor({ store, components = [] }: { store: TypedStore; components: Component[] }) {
counterpart.registerTranslations(FALLBACK_LOCALE, this.fetchTranslationJson("en_EN")); counterpart.registerTranslations(FALLBACK_LOCALE, this.fetchTranslationJson("en_EN"));
counterpart.setFallbackLocale(FALLBACK_LOCALE); counterpart.setFallbackLocale(FALLBACK_LOCALE);
counterpart.setSeparator('|'); counterpart.setSeparator("|");
if (Array.isArray(components)) { if (Array.isArray(components)) {
this.localizedComponents = new Set(components); this.localizedComponents = new Set(components);
@ -119,7 +119,7 @@ export class AppLocalization {
locales = [locales]; locales = [locales];
} }
const loadedLocales = locales.filter(locale => { const loadedLocales = locales.filter((locale) => {
const translations = this.fetchTranslationJson(locale); const translations = this.fetchTranslationJson(locale);
if (translations !== null) { if (translations !== null) {
counterpart.registerTranslations(locale, translations); counterpart.registerTranslations(locale, translations);
@ -135,7 +135,7 @@ export class AppLocalization {
public resetLocalizedUI(): void { public resetLocalizedUI(): void {
console.log("Resetting the UI components after locale change"); console.log("Resetting the UI components after locale change");
this.localizedComponents?.forEach(componentSetup => { this.localizedComponents?.forEach((componentSetup) => {
if (typeof componentSetup === "function") { if (typeof componentSetup === "function") {
componentSetup(); componentSetup();
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron'; import { ipcRenderer, contextBridge, IpcRendererEvent } from "electron";
// Expose only expected IPC wrapper APIs to the renderer process to avoid // Expose only expected IPC wrapper APIs to the renderer process to avoid
// handing out generalised messaging access. // handing out generalised messaging access.
@ -36,22 +36,19 @@ const CHANNELS = [
"userDownloadAction", "userDownloadAction",
]; ];
contextBridge.exposeInMainWorld( contextBridge.exposeInMainWorld("electron", {
"electron", on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void {
{ if (!CHANNELS.includes(channel)) {
on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void { console.error(`Unknown IPC channel ${channel} ignored`);
if (!CHANNELS.includes(channel)) { return;
console.error(`Unknown IPC channel ${channel} ignored`); }
return; ipcRenderer.on(channel, listener);
}
ipcRenderer.on(channel, listener);
},
send(channel: string, ...args: any[]): void {
if (!CHANNELS.includes(channel)) {
console.error(`Unknown IPC channel ${channel} ignored`);
return;
}
ipcRenderer.send(channel, ...args);
},
}, },
); send(channel: string, ...args: any[]): void {
if (!CHANNELS.includes(channel)) {
console.error(`Unknown IPC channel ${channel} ignored`);
return;
}
ipcRenderer.send(channel, ...args);
},
});

View file

@ -67,7 +67,7 @@ function writeStore(data: Record<string, string>): void {
} }
export function recordSSOSession(sessionID: string): void { export function recordSSOSession(sessionID: string): void {
const userDataPath = app.getPath('userData'); const userDataPath = app.getPath("userData");
const store = readStore(); const store = readStore();
for (const key in store) { for (const key in store) {
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded // ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
@ -82,7 +82,7 @@ export function recordSSOSession(sessionID: string): void {
export function getProfileFromDeeplink(args: string[]): string | undefined { export function getProfileFromDeeplink(args: string[]): string | undefined {
// check if we are passed a profile in the SSO callback url // check if we are passed a profile in the SSO callback url
const deeplinkUrl = args.find(arg => arg.startsWith(PROTOCOL + '//')); const deeplinkUrl = args.find((arg) => arg.startsWith(PROTOCOL + "//"));
if (deeplinkUrl?.includes(SEARCH_PARAM)) { if (deeplinkUrl?.includes(SEARCH_PARAM)) {
const parsedUrl = new URL(deeplinkUrl); const parsedUrl = new URL(deeplinkUrl);
if (parsedUrl.protocol === PROTOCOL) { if (parsedUrl.protocol === PROTOCOL) {
@ -98,25 +98,26 @@ export function protocolInit(): void {
// get all args except `hidden` as it'd mean the app would not get focused // get all args except `hidden` as it'd mean the app would not get focused
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking // XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url // --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden"); const args = process.argv.slice(1).filter((arg) => arg !== "--hidden" && arg !== "-hidden");
if (app.isPackaged) { if (app.isPackaged) {
app.setAsDefaultProtocolClient('element', process.execPath, args); app.setAsDefaultProtocolClient("element", process.execPath, args);
} else if (process.platform === 'win32') { // on Mac/Linux this would just cause the electron binary to open } else if (process.platform === "win32") {
// on Mac/Linux this would just cause the electron binary to open
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron // special handler for running without being packaged, e.g `electron .` by passing our app path to electron
app.setAsDefaultProtocolClient('element', process.execPath, [app.getAppPath(), ...args]); app.setAsDefaultProtocolClient("element", process.execPath, [app.getAppPath(), ...args]);
} }
if (process.platform === 'darwin') { if (process.platform === "darwin") {
// Protocol handler for macos // Protocol handler for macos
app.on('open-url', function(ev, url) { app.on("open-url", function (ev, url) {
ev.preventDefault(); ev.preventDefault();
processUrl(url); processUrl(url);
}); });
} else { } else {
// Protocol handler for win32/Linux // Protocol handler for win32/Linux
app.on('second-instance', (ev, commandLine) => { app.on("second-instance", (ev, commandLine) => {
const url = commandLine[commandLine.length - 1]; const url = commandLine[commandLine.length - 1];
if (!url.startsWith(PROTOCOL + '//')) return; if (!url.startsWith(PROTOCOL + "//")) return;
processUrl(url); processUrl(url);
}); });
} }

View file

@ -34,7 +34,7 @@ let ReindexError: typeof ReindexErrorType;
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const seshatModule = require('matrix-seshat'); const seshatModule = require("matrix-seshat");
Seshat = seshatModule.Seshat; Seshat = seshatModule.Seshat;
SeshatRecovery = seshatModule.SeshatRecovery; SeshatRecovery = seshatModule.SeshatRecovery;
ReindexError = seshatModule.ReindexError; ReindexError = seshatModule.ReindexError;
@ -75,29 +75,29 @@ const deleteContents = async (p: string): Promise<void> => {
} }
}; };
ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> { ipcMain.on("seshat", async function (_ev: IpcMainEvent, payload): Promise<void> {
if (!global.mainWindow) return; if (!global.mainWindow) return;
// We do this here to ensure we get the path after --profile has been resolved // We do this here to ensure we get the path after --profile has been resolved
const eventStorePath = path.join(app.getPath('userData'), 'EventStore'); const eventStorePath = path.join(app.getPath("userData"), "EventStore");
const sendError = (id: string, e: Error): void => { const sendError = (id: string, e: Error): void => {
const error = { const error = {
message: e.message, message: e.message,
}; };
global.mainWindow?.webContents.send('seshatReply', { id, error }); global.mainWindow?.webContents.send("seshatReply", { id, error });
}; };
const args = payload.args || []; const args = payload.args || [];
let ret: any; let ret: any;
switch (payload.name) { switch (payload.name) {
case 'supportsEventIndexing': case "supportsEventIndexing":
ret = seshatSupported; ret = seshatSupported;
break; break;
case 'initEventIndex': case "initEventIndex":
if (eventIndex === null) { if (eventIndex === null) {
const userId = args[0]; const userId = args[0];
const deviceId = args[1]; const deviceId = args[1];
@ -127,8 +127,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
try { try {
await deleteContents(eventStorePath); await deleteContents(eventStorePath);
} catch (e) { } catch (e) {}
}
} else { } else {
await recoveryIndex.reindex(); await recoveryIndex.reindex();
} }
@ -142,7 +141,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'closeEventIndex': case "closeEventIndex":
if (eventIndex !== null) { if (eventIndex !== null) {
const index = eventIndex; const index = eventIndex;
eventIndex = null; eventIndex = null;
@ -156,26 +155,24 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'deleteEventIndex': { case "deleteEventIndex": {
try { try {
await deleteContents(eventStorePath); await deleteContents(eventStorePath);
} catch (e) { } catch (e) {}
}
break; break;
} }
case 'isEventIndexEmpty': case "isEventIndexEmpty":
if (eventIndex === null) ret = true; if (eventIndex === null) ret = true;
else ret = await eventIndex.isEmpty(); else ret = await eventIndex.isEmpty();
break; break;
case 'isRoomIndexed': case "isRoomIndexed":
if (eventIndex === null) ret = false; if (eventIndex === null) ret = false;
else ret = await eventIndex.isRoomIndexed(args[0]); else ret = await eventIndex.isRoomIndexed(args[0]);
break; break;
case 'addEventToIndex': case "addEventToIndex":
try { try {
eventIndex?.addEvent(args[0], args[1]); eventIndex?.addEvent(args[0], args[1]);
} catch (e) { } catch (e) {
@ -184,7 +181,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'deleteEvent': case "deleteEvent":
try { try {
ret = await eventIndex?.deleteEvent(args[0]); ret = await eventIndex?.deleteEvent(args[0]);
} catch (e) { } catch (e) {
@ -193,7 +190,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'commitLiveEvents': case "commitLiveEvents":
try { try {
ret = await eventIndex?.commit(); ret = await eventIndex?.commit();
} catch (e) { } catch (e) {
@ -202,7 +199,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'searchEventIndex': case "searchEventIndex":
try { try {
ret = await eventIndex?.search(args[0]); ret = await eventIndex?.search(args[0]);
} catch (e) { } catch (e) {
@ -211,12 +208,11 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'addHistoricEvents': case "addHistoricEvents":
if (eventIndex === null) ret = false; if (eventIndex === null) ret = false;
else { else {
try { try {
ret = await eventIndex.addHistoricEvents( ret = await eventIndex.addHistoricEvents(args[0], args[1], args[2]);
args[0], args[1], args[2]);
} catch (e) { } catch (e) {
sendError(payload.id, <Error>e); sendError(payload.id, <Error>e);
return; return;
@ -224,7 +220,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'getStats': case "getStats":
if (eventIndex === null) ret = 0; if (eventIndex === null) ret = 0;
else { else {
try { try {
@ -236,7 +232,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'removeCrawlerCheckpoint': case "removeCrawlerCheckpoint":
if (eventIndex === null) ret = false; if (eventIndex === null) ret = false;
else { else {
try { try {
@ -248,7 +244,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'addCrawlerCheckpoint': case "addCrawlerCheckpoint":
if (eventIndex === null) ret = false; if (eventIndex === null) ret = false;
else { else {
try { try {
@ -260,7 +256,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'loadFileEvents': case "loadFileEvents":
if (eventIndex === null) ret = []; if (eventIndex === null) ret = [];
else { else {
try { try {
@ -272,7 +268,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'loadCheckpoints': case "loadCheckpoints":
if (eventIndex === null) ret = []; if (eventIndex === null) ret = [];
else { else {
try { try {
@ -283,7 +279,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'setUserVersion': case "setUserVersion":
if (eventIndex === null) break; if (eventIndex === null) break;
else { else {
try { try {
@ -295,7 +291,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
} }
break; break;
case 'getUserVersion': case "getUserVersion":
if (eventIndex === null) ret = 0; if (eventIndex === null) ret = 0;
else { else {
try { try {
@ -308,14 +304,14 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
break; break;
default: default:
global.mainWindow.webContents.send('seshatReply', { global.mainWindow.webContents.send("seshatReply", {
id: payload.id, id: payload.id,
error: "Unknown IPC Call: " + payload.name, error: "Unknown IPC Call: " + payload.name,
}); });
return; return;
} }
global.mainWindow.webContents.send('seshatReply', { global.mainWindow.webContents.send("seshatReply", {
id: payload.id, id: payload.id,
reply: ret, reply: ret,
}); });

View file

@ -42,17 +42,19 @@ export const Settings: Record<string, Setting> = {
global.store.set("warnBeforeExit", value); global.store.set("warnBeforeExit", value);
}, },
}, },
"Electron.alwaysShowMenuBar": { // not supported on macOS "Electron.alwaysShowMenuBar": {
// not supported on macOS
async read(): Promise<any> { async read(): Promise<any> {
return !global.mainWindow!.autoHideMenuBar; return !global.mainWindow!.autoHideMenuBar;
}, },
async write(value: any): Promise<void> { async write(value: any): Promise<void> {
global.store.set('autoHideMenuBar', !value); global.store.set("autoHideMenuBar", !value);
global.mainWindow!.autoHideMenuBar = !value; global.mainWindow!.autoHideMenuBar = !value;
global.mainWindow!.setMenuBarVisibility(value); global.mainWindow!.setMenuBarVisibility(value);
}, },
}, },
"Electron.showTrayIcon": { // not supported on macOS "Electron.showTrayIcon": {
// not supported on macOS
async read(): Promise<any> { async read(): Promise<any> {
return tray.hasTray(); return tray.hasTray();
}, },
@ -63,15 +65,15 @@ export const Settings: Record<string, Setting> = {
} else { } else {
tray.destroy(); tray.destroy();
} }
global.store.set('minimizeToTray', value); global.store.set("minimizeToTray", value);
}, },
}, },
"Electron.enableHardwareAcceleration": { "Electron.enableHardwareAcceleration": {
async read(): Promise<any> { async read(): Promise<any> {
return !global.store.get('disableHardwareAcceleration', false); return !global.store.get("disableHardwareAcceleration", false);
}, },
async write(value: any): Promise<void> { async write(value: any): Promise<void> {
global.store.set('disableHardwareAcceleration', !value); global.store.set("disableHardwareAcceleration", !value);
}, },
}, },
}; };

View file

@ -23,29 +23,29 @@ function runUpdateExe(args: string[]): Promise<void> {
// Note that there's an Update.exe in the app-x.x.x directory and one in the parent // Note that there's an Update.exe in the app-x.x.x directory and one in the parent
// directory: we need to run the one in the parent directory, because it discovers // directory: we need to run the one in the parent directory, because it discovers
// information about the app by inspecting the directory it's run from. // information about the app by inspecting the directory it's run from.
const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe'); const updateExe = path.resolve(path.dirname(process.execPath), "..", "Update.exe");
console.log(`Spawning '${updateExe}' with args '${args}'`); console.log(`Spawning '${updateExe}' with args '${args}'`);
return new Promise(resolve => { return new Promise((resolve) => {
spawn(updateExe, args, { spawn(updateExe, args, {
detached: true, detached: true,
}).on('close', resolve); }).on("close", resolve);
}); });
} }
function checkSquirrelHooks(): boolean { function checkSquirrelHooks(): boolean {
if (process.platform !== 'win32') return false; if (process.platform !== "win32") return false;
const cmd = process.argv[1]; const cmd = process.argv[1];
const target = path.basename(process.execPath); const target = path.basename(process.execPath);
if (cmd === '--squirrel-install') { if (cmd === "--squirrel-install") {
runUpdateExe(['--createShortcut=' + target]).then(() => app.quit()); runUpdateExe(["--createShortcut=" + target]).then(() => app.quit());
return true; return true;
} else if (cmd === '--squirrel-updated') { } else if (cmd === "--squirrel-updated") {
app.quit(); app.quit();
return true; return true;
} else if (cmd === '--squirrel-uninstall') { } else if (cmd === "--squirrel-uninstall") {
runUpdateExe(['--removeShortcut=' + target]).then(() => app.quit()); runUpdateExe(["--removeShortcut=" + target]).then(() => app.quit());
return true; return true;
} else if (cmd === '--squirrel-obsolete') { } else if (cmd === "--squirrel-obsolete") {
app.quit(); app.quit();
return true; return true;
} }

View file

@ -25,7 +25,7 @@ import { _t } from "./language-helper";
let trayIcon: Tray | null = null; let trayIcon: Tray | null = null;
export function hasTray(): boolean { export function hasTray(): boolean {
return (trayIcon !== null); return trayIcon !== null;
} }
export function destroy(): void { export function destroy(): void {
@ -52,17 +52,17 @@ interface IConfig {
export function create(config: IConfig): void { export function create(config: IConfig): void {
// no trays on darwin // no trays on darwin
if (process.platform === 'darwin' || trayIcon) return; if (process.platform === "darwin" || trayIcon) return;
const defaultIcon = nativeImage.createFromPath(config.icon_path); const defaultIcon = nativeImage.createFromPath(config.icon_path);
trayIcon = new Tray(defaultIcon); trayIcon = new Tray(defaultIcon);
trayIcon.setToolTip(config.brand); trayIcon.setToolTip(config.brand);
initApplicationMenu(); initApplicationMenu();
trayIcon.on('click', toggleWin); trayIcon.on("click", toggleWin);
let lastFavicon: string | null = null; let lastFavicon: string | null = null;
global.mainWindow?.webContents.on('page-favicon-updated', async function(ev, favicons) { global.mainWindow?.webContents.on("page-favicon-updated", async function (ev, favicons) {
if (!favicons || favicons.length <= 0 || !favicons[0].startsWith('data:')) { if (!favicons || favicons.length <= 0 || !favicons[0].startsWith("data:")) {
if (lastFavicon !== null) { if (lastFavicon !== null) {
global.mainWindow?.setIcon(defaultIcon); global.mainWindow?.setIcon(defaultIcon);
trayIcon?.setImage(defaultIcon); trayIcon?.setImage(defaultIcon);
@ -78,9 +78,9 @@ export function create(config: IConfig): void {
let newFavicon = nativeImage.createFromDataURL(favicons[0]); let newFavicon = nativeImage.createFromDataURL(favicons[0]);
// Windows likes ico's too much. // Windows likes ico's too much.
if (process.platform === 'win32') { if (process.platform === "win32") {
try { try {
const icoPath = path.join(app.getPath('temp'), 'win32_element_icon.ico'); const icoPath = path.join(app.getPath("temp"), "win32_element_icon.ico");
fs.writeFileSync(icoPath, await pngToIco(newFavicon.toPNG())); fs.writeFileSync(icoPath, await pngToIco(newFavicon.toPNG()));
newFavicon = nativeImage.createFromPath(icoPath); newFavicon = nativeImage.createFromPath(icoPath);
} catch (e) { } catch (e) {
@ -92,7 +92,7 @@ export function create(config: IConfig): void {
global.mainWindow?.setIcon(newFavicon); global.mainWindow?.setIcon(newFavicon);
}); });
global.mainWindow?.webContents.on('page-title-updated', function(ev, title) { global.mainWindow?.webContents.on("page-title-updated", function (ev, title) {
trayIcon?.setToolTip(title); trayIcon?.setToolTip(title);
}); });
} }
@ -104,13 +104,13 @@ export function initApplicationMenu(): void {
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: _t('Show/Hide'), label: _t("Show/Hide"),
click: toggleWin, click: toggleWin,
}, },
{ type: 'separator' }, { type: "separator" },
{ {
label: _t('Quit'), label: _t("Quit"),
click: function(): void { click: function (): void {
app.quit(); app.quit();
}, },
}, },

View file

@ -37,33 +37,33 @@ function pollForUpdates(): void {
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
} else { } else {
console.log("Skipping update check as download already present"); console.log("Skipping update check as download already present");
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded); global.mainWindow?.webContents.send("update-downloaded", latestUpdateDownloaded);
} }
} catch (e) { } catch (e) {
console.log('Couldn\'t check for update', e); console.log("Couldn't check for update", e);
} }
} }
export function start(updateBaseUrl: string): void { export function start(updateBaseUrl: string): void {
if (updateBaseUrl.slice(-1) !== '/') { if (updateBaseUrl.slice(-1) !== "/") {
updateBaseUrl = updateBaseUrl + '/'; updateBaseUrl = updateBaseUrl + "/";
} }
try { try {
let url: string; let url: string;
let serverType: "json" | undefined; let serverType: "json" | undefined;
if (process.platform === 'darwin') { if (process.platform === "darwin") {
// On macOS it takes a JSON file with a map between versions and their URLs // On macOS it takes a JSON file with a map between versions and their URLs
url = `${updateBaseUrl}macos/releases.json`; url = `${updateBaseUrl}macos/releases.json`;
serverType = "json"; serverType = "json";
} else if (process.platform === 'win32') { } else if (process.platform === "win32") {
// On windows it takes a base path and looks for files under that path. // On windows it takes a base path and looks for files under that path.
url = `${updateBaseUrl}win32/${process.arch}/`; url = `${updateBaseUrl}win32/${process.arch}/`;
} else { } else {
// Squirrel / electron only supports auto-update on these two platforms. // Squirrel / electron only supports auto-update on these two platforms.
// I'm not even going to try to guess which feed style they'd use if they // I'm not even going to try to guess which feed style they'd use if they
// implemented it on Linux, or if it would be different again. // implemented it on Linux, or if it would be different again.
console.log('Auto update not supported on this platform'); console.log("Auto update not supported on this platform");
return; return;
} }
@ -82,15 +82,15 @@ export function start(updateBaseUrl: string): void {
} }
} catch (err) { } catch (err) {
// will fail if running in debug mode // will fail if running in debug mode
console.log('Couldn\'t enable update checking', err); console.log("Couldn't enable update checking", err);
} }
} }
ipcMain.on('install_update', installUpdate); ipcMain.on("install_update", installUpdate);
ipcMain.on('check_updates', pollForUpdates); ipcMain.on("check_updates", pollForUpdates);
function ipcChannelSendUpdateStatus(status: boolean | string): void { function ipcChannelSendUpdateStatus(status: boolean | string): void {
global.mainWindow?.webContents.send('check_updates', status); global.mainWindow?.webContents.send("check_updates", status);
} }
interface ICachedUpdate { interface ICachedUpdate {
@ -102,23 +102,26 @@ interface ICachedUpdate {
// cache the latest update which has been downloaded as electron offers no api to read it // cache the latest update which has been downloaded as electron offers no api to read it
let latestUpdateDownloaded: ICachedUpdate; let latestUpdateDownloaded: ICachedUpdate;
autoUpdater.on('update-available', function() { autoUpdater
ipcChannelSendUpdateStatus(true); .on("update-available", function () {
}).on('update-not-available', function() { ipcChannelSendUpdateStatus(true);
if (latestUpdateDownloaded) { })
// the only time we will get `update-not-available` if `latestUpdateDownloaded` is already set .on("update-not-available", function () {
// is if the user used the Manual Update check and there is no update newer than the one we if (latestUpdateDownloaded) {
// have downloaded, so show it to them as the latest again. // the only time we will get `update-not-available` if `latestUpdateDownloaded` is already set
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded); // is if the user used the Manual Update check and there is no update newer than the one we
} else { // have downloaded, so show it to them as the latest again.
ipcChannelSendUpdateStatus(false); global.mainWindow?.webContents.send("update-downloaded", latestUpdateDownloaded);
} } else {
}).on('error', function(error) { ipcChannelSendUpdateStatus(false);
ipcChannelSendUpdateStatus(error.message); }
}); })
.on("error", function (error) {
ipcChannelSendUpdateStatus(error.message);
});
autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate, updateURL) => { autoUpdater.on("update-downloaded", (ev, releaseNotes, releaseName, releaseDate, updateURL) => {
// forward to renderer // forward to renderer
latestUpdateDownloaded = { releaseNotes, releaseName, releaseDate, updateURL }; latestUpdateDownloaded = { releaseNotes, releaseName, releaseDate, updateURL };
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded); global.mainWindow?.webContents.send("update-downloaded", latestUpdateDownloaded);
}); });

View file

@ -22,7 +22,7 @@ export async function randomArray(size: number): Promise<string> {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
resolve(buf.toString("base64").replace(/=+$/g, '')); resolve(buf.toString("base64").replace(/=+$/g, ""));
} }
}); });
}); });

View file

@ -14,125 +14,133 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { app, shell, Menu, MenuItem, MenuItemConstructorOptions } from 'electron'; import { app, shell, Menu, MenuItem, MenuItemConstructorOptions } from "electron";
import { _t } from './language-helper'; import { _t } from "./language-helper";
const isMac = process.platform === 'darwin'; const isMac = process.platform === "darwin";
export function buildMenuTemplate(): Menu { export function buildMenuTemplate(): Menu {
// Menu template from http://electron.atom.io/docs/api/menu/, edited // Menu template from http://electron.atom.io/docs/api/menu/, edited
const template: Array<(MenuItemConstructorOptions) | (MenuItem)> = [ const template: Array<MenuItemConstructorOptions | MenuItem> = [
{ {
label: _t('Edit'), label: _t("Edit"),
accelerator: 'e', accelerator: "e",
submenu: [ submenu: [
{ {
role: 'undo', role: "undo",
label: _t('Undo'), label: _t("Undo"),
}, },
{ {
role: 'redo', role: "redo",
label: _t('Redo'), label: _t("Redo"),
}, },
{ type: 'separator' }, { type: "separator" },
{ {
role: 'cut', role: "cut",
label: _t('Cut'), label: _t("Cut"),
}, },
{ {
role: 'copy', role: "copy",
label: _t('Copy'), label: _t("Copy"),
}, },
{ {
role: 'paste', role: "paste",
label: _t('Paste'), label: _t("Paste"),
}, },
{ {
role: 'pasteAndMatchStyle', role: "pasteAndMatchStyle",
label: _t('Paste and Match Style'), label: _t("Paste and Match Style"),
}, },
{ {
role: 'delete', role: "delete",
label: _t('Delete'), label: _t("Delete"),
}, },
{ {
role: 'selectAll', role: "selectAll",
label: _t('Select All'), label: _t("Select All"),
}, },
], ],
}, },
{ {
label: _t('View'), label: _t("View"),
accelerator: 'V', accelerator: "V",
submenu: [ submenu: [
{ type: 'separator' }, { type: "separator" },
{ {
role: 'resetZoom', role: "resetZoom",
accelerator: 'CmdOrCtrl+Num0', accelerator: "CmdOrCtrl+Num0",
visible: false, visible: false,
}, },
{ {
role: 'zoomIn', role: "zoomIn",
accelerator: 'CmdOrCtrl+NumAdd', accelerator: "CmdOrCtrl+NumAdd",
visible: false, visible: false,
}, },
{ {
role: 'zoomOut', role: "zoomOut",
accelerator: 'CmdOrCtrl+NumSub', accelerator: "CmdOrCtrl+NumSub",
visible: false, visible: false,
}, },
{ {
role: 'resetZoom', role: "resetZoom",
label: _t('Actual Size'), label: _t("Actual Size"),
}, },
{ {
role: 'zoomIn', role: "zoomIn",
label: _t('Zoom In'), label: _t("Zoom In"),
}, },
{ {
role: 'zoomOut', role: "zoomOut",
label: _t('Zoom Out'), label: _t("Zoom Out"),
}, },
{ type: 'separator' }, { type: "separator" },
// in macOS the Preferences menu item goes in the first menu // in macOS the Preferences menu item goes in the first menu
...(!isMac ? [{ ...(!isMac
label: _t('Preferences'), ? [
click(): void { global.mainWindow?.webContents.send('preferences'); }, {
}] : []), label: _t("Preferences"),
click(): void {
global.mainWindow?.webContents.send("preferences");
},
},
]
: []),
{ {
role: 'togglefullscreen', role: "togglefullscreen",
label: _t('Toggle Full Screen'), label: _t("Toggle Full Screen"),
}, },
{ {
role: 'toggleDevTools', role: "toggleDevTools",
label: _t('Toggle Developer Tools'), label: _t("Toggle Developer Tools"),
}, },
], ],
}, },
{ {
label: _t('Window'), label: _t("Window"),
accelerator: 'w', accelerator: "w",
role: 'window', role: "window",
submenu: [ submenu: [
{ {
role: 'minimize', role: "minimize",
label: _t('Minimize'), label: _t("Minimize"),
}, },
{ {
role: 'close', role: "close",
label: _t('Close'), label: _t("Close"),
}, },
], ],
}, },
{ {
label: _t('Help'), label: _t("Help"),
accelerator: 'h', accelerator: "h",
role: 'help', role: "help",
submenu: [ submenu: [
{ {
label: _t('Element Help'), label: _t("Element Help"),
click(): void { shell.openExternal('https://element.io/help'); }, click(): void {
shell.openExternal("https://element.io/help");
},
}, },
], ],
}, },
@ -142,92 +150,95 @@ export function buildMenuTemplate(): Menu {
if (isMac) { if (isMac) {
template.unshift({ template.unshift({
// first macOS menu is the name of the app // first macOS menu is the name of the app
role: 'appMenu', role: "appMenu",
label: app.name, label: app.name,
submenu: [ submenu: [
{ {
role: 'about', role: "about",
label: _t('About') + ' ' + app.name, label: _t("About") + " " + app.name,
}, },
{ type: 'separator' }, { type: "separator" },
{ {
label: _t('Preferences') + '…', label: _t("Preferences") + "…",
accelerator: 'Command+,', // Mac-only accelerator accelerator: "Command+,", // Mac-only accelerator
click(): void { global.mainWindow?.webContents.send('preferences'); }, click(): void {
global.mainWindow?.webContents.send("preferences");
},
}, },
{ type: 'separator' }, { type: "separator" },
{ {
role: 'services', role: "services",
label: _t('Services'), label: _t("Services"),
submenu: [], submenu: [],
}, },
{ type: 'separator' }, { type: "separator" },
{ {
role: 'hide', role: "hide",
label: _t('Hide'), label: _t("Hide"),
}, },
{ {
role: 'hideOthers', role: "hideOthers",
label: _t('Hide Others'), label: _t("Hide Others"),
}, },
{ {
role: 'unhide', role: "unhide",
label: _t('Unhide'), label: _t("Unhide"),
}, },
{ type: 'separator' }, { type: "separator" },
{ {
role: 'quit', role: "quit",
label: _t('Quit'), label: _t("Quit"),
}, },
], ],
}); });
// Edit menu. // Edit menu.
// This has a 'speech' section on macOS // This has a 'speech' section on macOS
(template[1].submenu as MenuItemConstructorOptions[]).push( (template[1].submenu as MenuItemConstructorOptions[]).push(
{ type: 'separator' }, { type: "separator" },
{ {
label: _t('Speech'), label: _t("Speech"),
submenu: [ submenu: [
{ {
role: 'startSpeaking', role: "startSpeaking",
label: _t('Start Speaking'), label: _t("Start Speaking"),
}, },
{ {
role: 'stopSpeaking', role: "stopSpeaking",
label: _t('Stop Speaking'), label: _t("Stop Speaking"),
}, },
], ],
}); },
);
// Window menu. // Window menu.
// This also has specific functionality on macOS // This also has specific functionality on macOS
template[3].submenu = [ template[3].submenu = [
{ {
label: _t('Close'), label: _t("Close"),
accelerator: 'CmdOrCtrl+W', accelerator: "CmdOrCtrl+W",
role: 'close', role: "close",
}, },
{ {
label: _t('Minimize'), label: _t("Minimize"),
accelerator: 'CmdOrCtrl+M', accelerator: "CmdOrCtrl+M",
role: 'minimize', role: "minimize",
}, },
{ {
label: _t('Zoom'), label: _t("Zoom"),
role: 'zoom', role: "zoom",
}, },
{ {
type: 'separator', type: "separator",
}, },
{ {
label: _t('Bring All to Front'), label: _t("Bring All to Front"),
role: 'front', role: "front",
}, },
]; ];
} else { } else {
template.unshift({ template.unshift({
label: _t('File'), label: _t("File"),
accelerator: 'f', accelerator: "f",
submenu: [ submenu: [
// For some reason, 'about' does not seem to work on windows. // For some reason, 'about' does not seem to work on windows.
/*{ /*{
@ -235,8 +246,8 @@ export function buildMenuTemplate(): Menu {
label: _t('About'), label: _t('About'),
},*/ },*/
{ {
role: 'quit', role: "quit",
label: _t('Quit'), label: _t("Quit"),
}, },
], ],
}); });

View file

@ -28,22 +28,18 @@ import {
DownloadItem, DownloadItem,
MenuItemConstructorOptions, MenuItemConstructorOptions,
IpcMainEvent, IpcMainEvent,
} from 'electron'; } from "electron";
import url from 'url'; import url from "url";
import fs from 'fs'; import fs from "fs";
import fetch from 'node-fetch'; import fetch from "node-fetch";
import { pipeline } from 'stream'; import { pipeline } from "stream";
import path from 'path'; import path from "path";
import { _t } from './language-helper'; import { _t } from "./language-helper";
const MAILTO_PREFIX = "mailto:"; const MAILTO_PREFIX = "mailto:";
const PERMITTED_URL_SCHEMES: string[] = [ const PERMITTED_URL_SCHEMES: string[] = ["http:", "https:", MAILTO_PREFIX];
'http:',
'https:',
MAILTO_PREFIX,
];
function safeOpenURL(target: string): void { function safeOpenURL(target: string): void {
// openExternal passes the target to open/start/xdg-open, // openExternal passes the target to open/start/xdg-open,
@ -70,7 +66,7 @@ function onWindowOrNavigate(ev: Event, target: string): void {
} }
function writeNativeImage(filePath: string, img: NativeImage): Promise<void> { function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
switch (filePath.split('.').pop()?.toLowerCase()) { switch (filePath.split(".").pop()?.toLowerCase()) {
case "jpg": case "jpg":
case "jpeg": case "jpeg":
return fs.promises.writeFile(filePath, img.toJPEG(100)); return fs.promises.writeFile(filePath, img.toJPEG(100));
@ -85,7 +81,7 @@ function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: WebContents): void { function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: WebContents): void {
let url = params.linkURL || params.srcURL; let url = params.linkURL || params.srcURL;
if (url.startsWith('vector://vector/webapp')) { if (url.startsWith("vector://vector/webapp")) {
// Avoid showing a context menu for app icons // Avoid showing a context menu for app icons
if (params.hasImageContents) return; if (params.hasImageContents) return;
// Rewrite URL so that it can be used outside of the app // Rewrite URL so that it can be used outside of the app
@ -94,82 +90,90 @@ function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: We
const popupMenu = new Menu(); const popupMenu = new Menu();
// No point trying to open blob: URLs in an external browser: it ain't gonna work. // No point trying to open blob: URLs in an external browser: it ain't gonna work.
if (!url.startsWith('blob:')) { if (!url.startsWith("blob:")) {
popupMenu.append(new MenuItem({ popupMenu.append(
label: url, new MenuItem({
click(): void { label: url,
safeOpenURL(url); click(): void {
}, safeOpenURL(url);
})); },
}),
);
} }
if (params.hasImageContents) { if (params.hasImageContents) {
popupMenu.append(new MenuItem({ popupMenu.append(
label: _t('Copy image'), new MenuItem({
accelerator: 'c', label: _t("Copy image"),
click(): void { accelerator: "c",
webContents.copyImageAt(params.x, params.y); click(): void {
}, webContents.copyImageAt(params.x, params.y);
})); },
}),
);
} }
// No point offering to copy a blob: URL either // No point offering to copy a blob: URL either
if (!url.startsWith('blob:')) { if (!url.startsWith("blob:")) {
// Special-case e-mail URLs to strip the `mailto:` like modern browsers do // Special-case e-mail URLs to strip the `mailto:` like modern browsers do
if (url.startsWith(MAILTO_PREFIX)) { if (url.startsWith(MAILTO_PREFIX)) {
popupMenu.append(new MenuItem({ popupMenu.append(
label: _t('Copy email address'), new MenuItem({
accelerator: 'a', label: _t("Copy email address"),
click(): void { accelerator: "a",
clipboard.writeText(url.substr(MAILTO_PREFIX.length)); click(): void {
}, clipboard.writeText(url.substr(MAILTO_PREFIX.length));
})); },
}),
);
} else { } else {
popupMenu.append(new MenuItem({ popupMenu.append(
label: params.hasImageContents new MenuItem({
? _t('Copy image address') label: params.hasImageContents ? _t("Copy image address") : _t("Copy link address"),
: _t('Copy link address'), accelerator: "a",
accelerator: 'a', click(): void {
click(): void { clipboard.writeText(url);
clipboard.writeText(url); },
}, }),
})); );
} }
} }
// XXX: We cannot easily save a blob from the main process as // XXX: We cannot easily save a blob from the main process as
// only the renderer can resolve them so don't give the user an option to. // only the renderer can resolve them so don't give the user an option to.
if (params.hasImageContents && !url.startsWith('blob:')) { if (params.hasImageContents && !url.startsWith("blob:")) {
popupMenu.append(new MenuItem({ popupMenu.append(
label: _t('Save image as...'), new MenuItem({
accelerator: 's', label: _t("Save image as..."),
async click(): Promise<void> { accelerator: "s",
const targetFileName = params.suggestedFilename || params.altText || "image.png"; async click(): Promise<void> {
const { filePath } = await dialog.showSaveDialog({ const targetFileName = params.suggestedFilename || params.altText || "image.png";
defaultPath: targetFileName, const { filePath } = await dialog.showSaveDialog({
}); defaultPath: targetFileName,
if (!filePath) return; // user cancelled dialog
try {
if (url.startsWith("data:")) {
await writeNativeImage(filePath, nativeImage.createFromDataURL(url));
} else {
const resp = await fetch(url);
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
pipeline(resp.body, fs.createWriteStream(filePath));
}
} catch (err) {
console.error(err);
dialog.showMessageBox({
type: "error",
title: _t("Failed to save image"),
message: _t("The image failed to save"),
}); });
}
}, if (!filePath) return; // user cancelled dialog
}));
try {
if (url.startsWith("data:")) {
await writeNativeImage(filePath, nativeImage.createFromDataURL(url));
} else {
const resp = await fetch(url);
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
pipeline(resp.body, fs.createWriteStream(filePath));
}
} catch (err) {
console.error(err);
dialog.showMessageBox({
type: "error",
title: _t("Failed to save image"),
message: _t("The image failed to save"),
});
}
},
}),
);
} }
// popup() requires an options object even for no options // popup() requires an options object even for no options
@ -181,7 +185,7 @@ function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemCons
const options: MenuItemConstructorOptions[] = []; const options: MenuItemConstructorOptions[] = [];
if (params.misspelledWord) { if (params.misspelledWord) {
params.dictionarySuggestions.forEach(word => { params.dictionarySuggestions.forEach((word) => {
options.push({ options.push({
label: word, label: word,
click: (menuItem, browserWindow) => { click: (menuItem, browserWindow) => {
@ -189,42 +193,52 @@ function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemCons
}, },
}); });
}); });
options.push({ options.push(
type: 'separator', {
}, { type: "separator",
label: _t('Add to dictionary'),
click: (menuItem, browserWindow) => {
browserWindow?.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
}, },
}, { {
type: 'separator', label: _t("Add to dictionary"),
}); click: (menuItem, browserWindow) => {
browserWindow?.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
},
},
{
type: "separator",
},
);
} }
options.push({ options.push(
role: 'cut', {
label: _t('Cut'), role: "cut",
accelerator: 't', label: _t("Cut"),
enabled: params.editFlags.canCut, accelerator: "t",
}, { enabled: params.editFlags.canCut,
role: 'copy', },
label: _t('Copy'), {
accelerator: 'c', role: "copy",
enabled: params.editFlags.canCopy, label: _t("Copy"),
}, { accelerator: "c",
role: 'paste', enabled: params.editFlags.canCopy,
label: _t('Paste'), },
accelerator: 'p', {
enabled: params.editFlags.canPaste, role: "paste",
}, { label: _t("Paste"),
role: 'pasteAndMatchStyle', accelerator: "p",
enabled: params.editFlags.canPaste, enabled: params.editFlags.canPaste,
}, { },
role: 'selectAll', {
label: _t("Select All"), role: "pasteAndMatchStyle",
accelerator: 'a', enabled: params.editFlags.canPaste,
enabled: params.editFlags.canSelectAll, },
}); {
role: "selectAll",
label: _t("Select All"),
accelerator: "a",
enabled: params.editFlags.canSelectAll,
},
);
return options; return options;
} }
@ -239,9 +253,9 @@ function onSelectedContextMenu(ev: Event, params: ContextMenuParams): void {
function onEditableContextMenu(ev: Event, params: ContextMenuParams): void { function onEditableContextMenu(ev: Event, params: ContextMenuParams): void {
const items: MenuItemConstructorOptions[] = [ const items: MenuItemConstructorOptions[] = [
{ role: 'undo' }, { role: "undo" },
{ role: 'redo', enabled: params.editFlags.canRedo }, { role: "redo", enabled: params.editFlags.canRedo },
{ type: 'separator' }, { type: "separator" },
...cutCopyPasteSelectContextMenus(params), ...cutCopyPasteSelectContextMenus(params),
]; ];
@ -254,7 +268,7 @@ function onEditableContextMenu(ev: Event, params: ContextMenuParams): void {
let userDownloadIndex = 0; let userDownloadIndex = 0;
const userDownloadMap = new Map<number, string>(); // Map from id to path const userDownloadMap = new Map<number, string>(); // Map from id to path
ipcMain.on('userDownloadAction', function(ev: IpcMainEvent, { id, open = false }) { ipcMain.on("userDownloadAction", function (ev: IpcMainEvent, { id, open = false }) {
const path = userDownloadMap.get(id); const path = userDownloadMap.get(id);
if (open && path) { if (open && path) {
shell.openPath(path); shell.openPath(path);
@ -268,12 +282,12 @@ export default (webContents: WebContents): void => {
return { action: "deny" }; return { action: "deny" };
}); });
webContents.on('will-navigate', (ev: Event, target: string): void => { webContents.on("will-navigate", (ev: Event, target: string): void => {
if (target.startsWith("vector://")) return; if (target.startsWith("vector://")) return;
return onWindowOrNavigate(ev, target); return onWindowOrNavigate(ev, target);
}); });
webContents.on('context-menu', function(ev: Event, params: ContextMenuParams): void { webContents.on("context-menu", function (ev: Event, params: ContextMenuParams): void {
if (params.linkURL || params.srcURL) { if (params.linkURL || params.srcURL) {
onLinkContextMenu(ev, params, webContents); onLinkContextMenu(ev, params, webContents);
} else if (params.selectionText) { } else if (params.selectionText) {
@ -283,13 +297,13 @@ export default (webContents: WebContents): void => {
} }
}); });
webContents.session.on('will-download', (event: Event, item: DownloadItem): void => { webContents.session.on("will-download", (event: Event, item: DownloadItem): void => {
item.once('done', (event, state) => { item.once("done", (event, state) => {
if (state === 'completed') { if (state === "completed") {
const savePath = item.getSavePath(); const savePath = item.getSavePath();
const id = userDownloadIndex++; const id = userDownloadIndex++;
userDownloadMap.set(id, savePath); userDownloadMap.set(id, savePath);
webContents.send('userDownloadCompleted', { webContents.send("userDownloadCompleted", {
id, id,
name: path.basename(savePath), name: path.basename(savePath),
}); });

View file

@ -45,13 +45,13 @@ describe("App launch", () => {
args, args,
recordVideo: { recordVideo: {
dir: artifactsPath, dir: artifactsPath,
} },
}); });
window = await app.firstWindow(); window = await app.firstWindow();
}, 30000); }, 30000);
afterAll(async () => { afterAll(async () => {
await app?.close().catch(e => { await app?.close().catch((e) => {
console.error(e); console.error(e);
}); });
fs.rmSync(tmpDir, { recursive: true }); fs.rmSync(tmpDir, { recursive: true });

View file

@ -1,24 +1,18 @@
{ {
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"target": "es2016", "target": "es2016",
"sourceMap": false, "sourceMap": false,
"outDir": "./lib", "outDir": "./lib",
"rootDir": "./src", "rootDir": "./src",
"declaration": true, "declaration": true,
"typeRoots": ["src/@types"], "typeRoots": ["src/@types"],
"lib": [ "lib": ["es2019", "dom"],
"es2019", "types": ["jest", "node"],
"dom" "strict": true
], },
"types": ["jest", "node"], "include": ["./src/**/*.ts", "./tests/**/*.ts"]
"strict": true
},
"include": [
"./src/**/*.ts",
"./tests/**/*.ts"
]
} }

View file

@ -4493,6 +4493,11 @@ eslint-config-google@^0.14.0:
resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
eslint-config-prettier@^8.5.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
eslint-import-resolver-node@^0.3.6: eslint-import-resolver-node@^0.3.6:
version "0.3.6" version "0.3.6"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
@ -4527,10 +4532,10 @@ eslint-plugin-import@^2.25.4:
resolve "^1.22.0" resolve "^1.22.0"
tsconfig-paths "^3.14.1" tsconfig-paths "^3.14.1"
eslint-plugin-matrix-org@^0.8.0: eslint-plugin-matrix-org@^0.9.0:
version "0.8.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.8.0.tgz#daa1396900a8cb1c1d88f1a370e45fc32482cd9e" resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.9.0.tgz#b2a5186052ddbfa7dc9878779bafa5d68681c7b4"
integrity sha512-/Poz/F8lXYDsmQa29iPSt+kO+Jn7ArvRdq10g0CCk8wbRS0sb2zb6fvd9xL1BgR5UDQL771V0l8X32etvY5yKA== integrity sha512-+j6JuMnFH421Z2vOxc+0YMt5Su5vD76RSatviy3zHBaZpgd+sOeAWoCLBHD5E7mMz5oKae3Y3wewCt9LRzq2Nw==
eslint-plugin-unicorn@^45.0.0: eslint-plugin-unicorn@^45.0.0:
version "45.0.1" version "45.0.1"