Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65f121c451 | ||
|
|
821cc62c4c | ||
|
|
4ce327eb6a | ||
|
|
4beb1bb574 | ||
|
|
0a2a1a026b | ||
|
|
eb096d07e2 | ||
|
|
19e42c9c52 | ||
|
|
f4d86fccfd | ||
|
|
ad0576a752 | ||
|
|
35be05df20 | ||
|
|
dae6d114da | ||
|
|
ecce47fdcd |
@@ -1,2 +0,0 @@
|
|||||||
dist/
|
|
||||||
dist_electron/
|
|
||||||
11
.github/actions/npm-install-dependencies/action.yml
vendored
Normal file
11
.github/actions/npm-install-dependencies/action.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
inputs:
|
||||||
|
working-directory:
|
||||||
|
required: false
|
||||||
|
default: '.'
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Run `npm ci` with retries
|
||||||
|
shell: bash
|
||||||
|
run: npm run install-deps -- --ci --root-directory "${{ inputs.working-directory }}"
|
||||||
37
.github/workflows/checks.build.yaml
vendored
37
.github/workflows/checks.build.yaml
vendored
@@ -27,10 +27,13 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build web
|
||||||
run: npm run build -- --mode ${{ matrix.mode }}
|
run: npm run build -- --mode ${{ matrix.mode }}
|
||||||
|
-
|
||||||
|
name: Verify web build artifacts
|
||||||
|
run: npm run check:verify-build-artifacts -- --web
|
||||||
|
|
||||||
build-desktop:
|
build-desktop:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -52,30 +55,16 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Prebuild
|
name: Prebuild desktop
|
||||||
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
|
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
|
||||||
-
|
-
|
||||||
name: Build
|
name: Verify unbundled desktop build artifacts
|
||||||
|
run: npm run check:verify-build-artifacts -- --electron-unbundled
|
||||||
|
-
|
||||||
|
name: Build (bundle and package) desktop application
|
||||||
run: npm run electron:build -- --publish never
|
run: npm run electron:build -- --publish never
|
||||||
|
|
||||||
create-icons:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ macos, ubuntu, windows ]
|
|
||||||
fail-fast: false # Allows to see results from other combinations
|
|
||||||
runs-on: ${{ matrix.os }}-latest
|
|
||||||
steps:
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Verify bundled desktop build artifacts
|
||||||
uses: actions/checkout@v2
|
run: npm run check:verify-build-artifacts -- --electron-bundled
|
||||||
-
|
|
||||||
name: Setup node
|
|
||||||
uses: ./.github/actions/setup-node
|
|
||||||
-
|
|
||||||
name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
-
|
|
||||||
name: Create icons
|
|
||||||
run: npm run icons:build
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-desktop:
|
run-check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu, windows ]
|
os: [ macos, ubuntu, windows ]
|
||||||
@@ -19,6 +19,9 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Configure Ubuntu
|
name: Configure Ubuntu
|
||||||
if: matrix.os == 'ubuntu'
|
if: matrix.os == 'ubuntu'
|
||||||
@@ -57,7 +60,9 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
shell: bash
|
shell: bash
|
||||||
run: node ./scripts/check-desktop-runtime-errors --screenshot
|
run: |-
|
||||||
|
export SCREENSHOT=true
|
||||||
|
npm run check:desktop
|
||||||
-
|
-
|
||||||
name: Upload screenshot
|
name: Upload screenshot
|
||||||
if: always() # Run even if previous step fails
|
if: always() # Run even if previous step fails
|
||||||
|
|||||||
22
.github/workflows/checks.external-urls.yaml
vendored
Normal file
22
.github/workflows/checks.external-urls.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: checks.external-urls
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
|
-
|
||||||
|
name: Test
|
||||||
|
run: npm run check:external-urls
|
||||||
14
.github/workflows/checks.quality.yaml
vendored
14
.github/workflows/checks.quality.yaml
vendored
@@ -16,11 +16,15 @@ jobs:
|
|||||||
os: [ macos, ubuntu, windows ]
|
os: [ macos, ubuntu, windows ]
|
||||||
fail-fast: false # Still interested to see results from other combinations
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
-
|
||||||
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Setup node
|
-
|
||||||
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
- name: Install dependencies
|
-
|
||||||
run: npm ci
|
name: Install dependencies
|
||||||
- name: Lint
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
|
-
|
||||||
|
name: Lint
|
||||||
run: ${{ matrix.lint-command }}
|
run: ${{ matrix.lint-command }}
|
||||||
|
|||||||
55
.github/workflows/checks.scripts.yaml
vendored
Normal file
55
.github/workflows/checks.scripts.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: checks.scripts
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
icons-build:
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
|
-
|
||||||
|
name: Create icons
|
||||||
|
run: npm run icons:build
|
||||||
|
|
||||||
|
install-deps:
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
install-deps-before: [true, false]
|
||||||
|
install-command:
|
||||||
|
- npm run install-deps
|
||||||
|
- npm run install-deps -- --no-errors
|
||||||
|
- npm run install-deps -- --ci
|
||||||
|
- npm run install-deps -- --fresh --non-deterministic
|
||||||
|
- npm run install-deps -- --fresh
|
||||||
|
- npm run install-deps -- --non-deterministic
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
if: matrix.install-deps-before == true
|
||||||
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
|
-
|
||||||
|
name: Run install-deps
|
||||||
|
run: ${{ matrix.install-command }}
|
||||||
2
.github/workflows/release.desktop.yaml
vendored
2
.github/workflows/release.desktop.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Run unit tests
|
name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
|
|||||||
17
.github/workflows/release.site.yaml
vendored
17
.github/workflows/release.site.yaml
vendored
@@ -84,8 +84,9 @@ jobs:
|
|||||||
uses: ./app/.github/actions/setup-node
|
uses: ./app/.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: "App: Install dependencies"
|
name: "App: Install dependencies"
|
||||||
run: npm ci
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
working-directory: app
|
with:
|
||||||
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Run unit tests"
|
name: "App: Run unit tests"
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
@@ -94,11 +95,21 @@ jobs:
|
|||||||
name: "App: Build"
|
name: "App: Build"
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: app
|
working-directory: app
|
||||||
|
-
|
||||||
|
name: "App: Verify web build artifacts"
|
||||||
|
run: npm run check:verify-build-artifacts -- --web
|
||||||
|
working-directory: app
|
||||||
-
|
-
|
||||||
name: "App: Deploy to S3"
|
name: "App: Deploy to S3"
|
||||||
|
shell: bash
|
||||||
run: >-
|
run: >-
|
||||||
|
declare web_output_dir
|
||||||
|
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
||||||
|
echo 'Error: Could not determine distribution directory.'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
bash "aws/scripts/deploy/deploy-to-s3.sh" \
|
||||||
--folder app/dist \
|
--folder "${web_output_dir}" \
|
||||||
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
||||||
--storage-class ONEZONE_IA \
|
--storage-class ONEZONE_IA \
|
||||||
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
|
||||||
|
|||||||
2
.github/workflows/tests.e2e.yaml
vendored
2
.github/workflows/tests.e2e.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Run e2e tests
|
name: Run e2e tests
|
||||||
run: npm run test:cy:run
|
run: npm run test:cy:run
|
||||||
|
|||||||
2
.github/workflows/tests.integration.yaml
vendored
2
.github/workflows/tests.integration.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Run integration tests
|
name: Run integration tests
|
||||||
run: npm run test:integration
|
run: npm run test:integration
|
||||||
|
|||||||
2
.github/workflows/tests.unit.yaml
vendored
2
.github/workflows/tests.unit.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Run unit tests
|
name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist/
|
/dist-*/
|
||||||
.vs
|
.vs
|
||||||
.vscode/**/*
|
.vscode/**/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
#Electron-builder output
|
|
||||||
/dist_electron
|
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.2 (2023-08-25)
|
||||||
|
|
||||||
|
* Add automated checks for desktop app runtime #233 | [04b3133](https://github.com/undergroundwires/privacy.sexy/commit/04b3133500485d0d278a81a177a1677134131405)
|
||||||
|
* win: fix automatic updates revert #234 | [0873769](https://github.com/undergroundwires/privacy.sexy/commit/08737698c2283bdf535d1611a730031ebfc7c0df)
|
||||||
|
* Migrate unit/integration tests to Vitest with Vite | [5f11c8d](https://github.com/undergroundwires/privacy.sexy/commit/5f11c8d98f782dd7c77f27649a1685fb7bd06e13)
|
||||||
|
* Remove Vue ESLint plugin for Vite compatibility | [6e40edd](https://github.com/undergroundwires/privacy.sexy/commit/6e40edd3f8a063c1b7482c27d8368e14c2fbcfbf)
|
||||||
|
* Migrate web builds from Vue CLI to Vite | [7365905](https://github.com/undergroundwires/privacy.sexy/commit/736590558be51a09435bb87e78b6655e8533bc2e)
|
||||||
|
* Migrate Cypress (E2E) tests to Vite and TypeScript | [ec98d84](https://github.com/undergroundwires/privacy.sexy/commit/ec98d8417f779fa818ccdda6bb90f521e1738002)
|
||||||
|
* Migrate to `electron-vite` and `electron-builder` | [75c9b51](https://github.com/undergroundwires/privacy.sexy/commit/75c9b51bf2d1dc7269adfd7b5ed71acfb5031299)
|
||||||
|
* Fix searching/filtering bugs #235 | [62f8bfa](https://github.com/undergroundwires/privacy.sexy/commit/62f8bfac2f481c93598fe19a51594769f522d684)
|
||||||
|
* Improve desktop security by isolating Electron | [e9e0001](https://github.com/undergroundwires/privacy.sexy/commit/e9e0001ef845fa6935c59a4e20a89aac9e71756a)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.1...0.12.2)
|
||||||
|
|
||||||
## 0.12.1 (2023-08-17)
|
## 0.12.1 (2023-08-17)
|
||||||
|
|
||||||
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)
|
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)
|
||||||
|
|||||||
141
LICENSE
141
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -76,6 +76,18 @@
|
|||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.scripts.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Status of script checks"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.scripts/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.external-urls.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Status of external URL checks"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.external-urls/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
<!-- Release -->
|
<!-- Release -->
|
||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
|||||||
5
dist-dirs.json
Normal file
5
dist-dirs.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"electronUnbundled": "dist-electron-unbundled",
|
||||||
|
"electronBundled": "dist-electron-bundled",
|
||||||
|
"web": "dist-web"
|
||||||
|
}
|
||||||
@@ -5,14 +5,16 @@ Before your commit, a good practice is to:
|
|||||||
1. [Run unit tests](#testing)
|
1. [Run unit tests](#testing)
|
||||||
2. [Lint your code](#linting)
|
2. [Lint your code](#linting)
|
||||||
|
|
||||||
You could run other types of tests as well, but they may take longer time and overkill for your changes. Automated actions executes the tests for a pull request or change in the main branch. See [ci-cd.md](./ci-cd.md) for more information.
|
You could run other types of tests as well, but they may take longer time and overkill for your changes.
|
||||||
|
Automated actions are set up to execute these tests as necessary.
|
||||||
|
See [ci-cd.md](./ci-cd.md) for more information.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Install node >15.x.
|
- Install Node >16.x.
|
||||||
- Install dependencies using `npm install`.
|
- Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options).
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
@@ -21,6 +23,10 @@ You could run other types of tests as well, but they may take longer time and ov
|
|||||||
- Run end-to-end (e2e) tests:
|
- Run end-to-end (e2e) tests:
|
||||||
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
||||||
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
||||||
|
- Run checks:
|
||||||
|
- `npm run check:desktop`: Run runtime checks for packaged desktop applications ([README.md](./../tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/README.md)).
|
||||||
|
- You can set environment variables active its flags such as `BUILD=true SCREENSHOT=true npm run check:desktop`
|
||||||
|
- `npm run check:external-urls`: Test whether external URLs used in applications are alive.
|
||||||
|
|
||||||
📖 Read more about testing in [tests](./tests.md).
|
📖 Read more about testing in [tests](./tests.md).
|
||||||
|
|
||||||
@@ -61,13 +67,26 @@ You could run other types of tests as well, but they may take longer time and ov
|
|||||||
- Build desktop application: `npm run electron:build`
|
- Build desktop application: `npm run electron:build`
|
||||||
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
|
- (Re)create icons (see [documentation](../img/README.md)): `npm run create-icons`
|
||||||
|
|
||||||
### Utility Scripts
|
### Scripts
|
||||||
|
|
||||||
- Run fresh NPM install: [`./scripts/fresh-npm-install.sh`](../scripts/fresh-npm-install.sh)
|
📖 For detailed options and behavior for any of the following scripts, please refer to the script file itself.
|
||||||
- This script provides a clean NPM install, removing existing node modules and optionally the package-lock.json (when run with -n), then installs dependencies and runs unit tests.
|
|
||||||
- Configure VSCode: [`./scripts/configure-vscode.sh`](../scripts/configure-vscode.sh)
|
#### Utility scripts
|
||||||
|
|
||||||
|
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
||||||
|
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||||
|
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
||||||
|
- [**`./scripts/configure-vscode.sh`**](../scripts/configure-vscode.sh):
|
||||||
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
- This script checks and sets the necessary configurations for VSCode in `settings.json` file.
|
||||||
|
|
||||||
|
#### Automation scripts
|
||||||
|
|
||||||
|
- [**`node scripts/print-dist-dir.js [-- <options>]`**](../scripts/print-dist-dir.js):
|
||||||
|
- Determines the absolute path of a distribution directory based on CLI arguments and outputs its absolute path.
|
||||||
|
- Primarily used by automation scripts.
|
||||||
|
- [**`npm run check:verify-build-artifacts [-- <options>]`**](../scripts/verify-build-artifacts.js):
|
||||||
|
- Verifies the existence and content of build artifacts. Useful for ensuring that the build process is generating the expected output.
|
||||||
|
|
||||||
## Recommended extensions
|
## Recommended extensions
|
||||||
|
|
||||||
You should use EditorConfig to follow project style.
|
You should use EditorConfig to follow project style.
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
||||||
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles for Vue components.
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
||||||
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
|
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
@@ -91,7 +91,14 @@ Shared components include:
|
|||||||
|
|
||||||
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
|
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
|
||||||
|
|
||||||
## Sass naming convention
|
## Styles
|
||||||
|
|
||||||
|
### Style location
|
||||||
|
|
||||||
|
- **Global styles**: The [`assets/styles/`](#structure) directory is reserved for styles that have a broader scope, affecting multiple components or entire layouts. They are generic and should not be tightly coupled to a specific component's functionality.
|
||||||
|
- **Component-specific styles**: Styles closely tied to a particular component's functionality or appearance should reside near the component they are used by. This makes it easier to locate and modify styles when working on a specific component.
|
||||||
|
|
||||||
|
### Sass naming convention
|
||||||
|
|
||||||
- Use lowercase for variables/functions/mixins, e.g.:
|
- Use lowercase for variables/functions/mixins, e.g.:
|
||||||
- Variable: `$variable: value;`
|
- Variable: `$variable: value;`
|
||||||
|
|||||||
43
electron-builder.cjs
Normal file
43
electron-builder.cjs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* eslint-disable no-template-curly-in-string */
|
||||||
|
|
||||||
|
const { join } = require('path');
|
||||||
|
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Common options
|
||||||
|
publish: {
|
||||||
|
provider: 'github',
|
||||||
|
vPrefixedTagName: false, // default: true
|
||||||
|
releaseType: 'release', // default: draft
|
||||||
|
},
|
||||||
|
directories: {
|
||||||
|
output: electronBundled,
|
||||||
|
},
|
||||||
|
extraMetadata: {
|
||||||
|
main: join(electronUnbundled, 'main/index.cjs'), // do not `path.resolve`, it expects a relative path
|
||||||
|
},
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
win: {
|
||||||
|
target: 'nsis',
|
||||||
|
},
|
||||||
|
nsis: {
|
||||||
|
artifactName: '${name}-Setup-${version}.${ext}',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Linux
|
||||||
|
linux: {
|
||||||
|
target: 'AppImage',
|
||||||
|
},
|
||||||
|
appImage: {
|
||||||
|
artifactName: '${name}-${version}.${ext}',
|
||||||
|
},
|
||||||
|
|
||||||
|
// macOS
|
||||||
|
mac: {
|
||||||
|
target: 'dmg',
|
||||||
|
},
|
||||||
|
dmg: {
|
||||||
|
artifactName: '${name}-${version}.${ext}',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# -------
|
|
||||||
# Windows
|
|
||||||
# -------
|
|
||||||
win:
|
|
||||||
target: nsis
|
|
||||||
nsis:
|
|
||||||
artifactName: ${name}-${version}-Setup.${ext}
|
|
||||||
|
|
||||||
# -----
|
|
||||||
# Linux
|
|
||||||
# -----
|
|
||||||
linux:
|
|
||||||
target: AppImage
|
|
||||||
appImage:
|
|
||||||
artifactName: ${name}-${version}.${ext}
|
|
||||||
|
|
||||||
# -----
|
|
||||||
# macOS
|
|
||||||
# -----
|
|
||||||
mac:
|
|
||||||
target: dmg
|
|
||||||
dmg:
|
|
||||||
artifactName: ${name}-${version}.${ext}
|
|
||||||
|
|
||||||
# ----------------
|
|
||||||
# Publish options
|
|
||||||
# ----------------
|
|
||||||
publish:
|
|
||||||
provider: 'github'
|
|
||||||
vPrefixedTagName: false # default: true
|
|
||||||
releaseType: release # default: draft
|
|
||||||
@@ -3,11 +3,12 @@ import { mergeConfig, UserConfig } from 'vite';
|
|||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
||||||
import { createVueConfig } from './vite.config';
|
import { createVueConfig } from './vite.config';
|
||||||
|
import distDirs from './dist-dirs.json' assert { type: 'json' };
|
||||||
|
|
||||||
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
||||||
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||||
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
||||||
const DIST_DIR = resolvePathFromProjectRoot('dist_electron/');
|
const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: getSharedElectronConfig({
|
main: getSharedElectronConfig({
|
||||||
|
|||||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.1",
|
"version": "0.12.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.1",
|
"version": "0.12.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-progressbar": "^2.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"install": "^0.13.0",
|
|
||||||
"liquor-tree": "^0.2.70",
|
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"npm": "^9.8.1",
|
"npm": "^9.8.1",
|
||||||
"v-tooltip": "2.1.3",
|
"v-tooltip": "2.1.3",
|
||||||
@@ -9425,14 +9423,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/install": {
|
|
||||||
"version": "0.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
|
|
||||||
"integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
|
||||||
@@ -10771,11 +10761,6 @@
|
|||||||
"uc.micro": "^1.0.1"
|
"uc.micro": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/liquor-tree": {
|
|
||||||
"version": "0.2.70",
|
|
||||||
"resolved": "https://registry.npmjs.org/liquor-tree/-/liquor-tree-0.2.70.tgz",
|
|
||||||
"integrity": "sha512-5CiMlDVmuveYwwc27mYe1xZ3J4aHhZBErUhIp9ov4v4wIBso+s5JAByOOit4iOCMCQ5ODd8VggbKymzZREYbBQ=="
|
|
||||||
},
|
|
||||||
"node_modules/listr2": {
|
"node_modules/listr2": {
|
||||||
"version": "3.14.0",
|
"version": "3.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
|
||||||
@@ -28223,11 +28208,6 @@
|
|||||||
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
|
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"install": {
|
|
||||||
"version": "0.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
|
|
||||||
"integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="
|
|
||||||
},
|
|
||||||
"internal-slot": {
|
"internal-slot": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
|
||||||
@@ -29256,11 +29236,6 @@
|
|||||||
"uc.micro": "^1.0.1"
|
"uc.micro": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"liquor-tree": {
|
|
||||||
"version": "0.2.70",
|
|
||||||
"resolved": "https://registry.npmjs.org/liquor-tree/-/liquor-tree-0.2.70.tgz",
|
|
||||||
"integrity": "sha512-5CiMlDVmuveYwwc27mYe1xZ3J4aHhZBErUhIp9ov4v4wIBso+s5JAByOOit4iOCMCQ5ODd8VggbKymzZREYbBQ=="
|
|
||||||
},
|
|
||||||
"listr2": {
|
"listr2": {
|
||||||
"version": "3.14.0",
|
"version": "3.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -1,27 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.1",
|
"version": "0.12.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Now you have the choice",
|
"slogan": "Now you have the choice",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./dist-electron-unbundled/main/index.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "vitest run --dir tests/unit",
|
"test:unit": "vitest run --dir tests/unit",
|
||||||
"test:integration": "vitest run --dir tests/integration",
|
"test:integration": "vitest run --dir tests/integration",
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
|
||||||
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||||
|
"install-deps": "node scripts/npm-install.js",
|
||||||
"icons:build": "node scripts/logo-update.js",
|
"icons:build": "node scripts/logo-update.js",
|
||||||
|
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||||
|
"check:external-urls": "vitest run --dir tests/checks/external-urls --environment node",
|
||||||
|
"check:verify-build-artifacts": "node scripts/verify-build-artifacts",
|
||||||
"electron:dev": "electron-vite dev",
|
"electron:dev": "electron-vite dev",
|
||||||
"electron:preview": "electron-vite preview",
|
"electron:preview": "electron-vite preview",
|
||||||
"electron:prebuild": "electron-vite build",
|
"electron:prebuild": "electron-vite build",
|
||||||
"electron:build": "electron-builder",
|
"electron:build": "electron-builder",
|
||||||
"lint:eslint": "eslint .",
|
"lint:eslint": "eslint . --ignore-path .gitignore",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
@@ -29,7 +33,6 @@
|
|||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"main": "./dist_electron/main/index.cjs",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
@@ -41,8 +44,6 @@
|
|||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-progressbar": "^2.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"install": "^0.13.0",
|
|
||||||
"liquor-tree": "^0.2.70",
|
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"npm": "^9.8.1",
|
"npm": "^9.8.1",
|
||||||
"v-tooltip": "2.1.3",
|
"v-tooltip": "2.1.3",
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
require('@rushstack/eslint-patch/modern-module-resolution.js');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'import/extensions': ['error', 'always'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { unlink, readFile } from 'fs/promises';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { log, die, LOG_LEVELS } from '../utils/log.js';
|
|
||||||
import { exists } from '../utils/io.js';
|
|
||||||
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../utils/platform.js';
|
|
||||||
import { getAppName } from '../utils/npm.js';
|
|
||||||
|
|
||||||
export async function clearAppLogFile(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const logPath = await determineLogPath(projectDir);
|
|
||||||
if (!logPath || !await exists(logPath)) {
|
|
||||||
log(`Skipping clearing logs, log file does not exist: ${logPath}.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await unlink(logPath);
|
|
||||||
log(`Successfully cleared the log file at: ${logPath}.`);
|
|
||||||
} catch (error) {
|
|
||||||
die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readAppLogFile(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const logPath = await determineLogPath(projectDir);
|
|
||||||
if (!logPath || !await exists(logPath)) {
|
|
||||||
log(`No log file at: ${logPath}`, LOG_LEVELS.WARN);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const logContent = await readLogFile(logPath);
|
|
||||||
return logContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function determineLogPath(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const appName = await getAppName(projectDir);
|
|
||||||
if (!appName) {
|
|
||||||
die('App name not found.');
|
|
||||||
}
|
|
||||||
const logFilePaths = {
|
|
||||||
[SUPPORTED_PLATFORMS.MAC]: () => join(process.env.HOME, 'Library', 'Logs', appName, 'main.log'),
|
|
||||||
[SUPPORTED_PLATFORMS.LINUX]: () => join(process.env.HOME, '.config', appName, 'logs', 'main.log'),
|
|
||||||
[SUPPORTED_PLATFORMS.WINDOWS]: () => join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', 'main.log'),
|
|
||||||
};
|
|
||||||
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
|
||||||
if (!logFilePath) {
|
|
||||||
log(`Cannot determine log path, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
|
||||||
}
|
|
||||||
return logFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readLogFile(logFilePath) {
|
|
||||||
const content = await readFile(logFilePath, 'utf-8');
|
|
||||||
return content?.trim().length > 0 ? content : undefined;
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { splitTextIntoLines, indentText } from '../utils/text.js';
|
|
||||||
import { die } from '../utils/log.js';
|
|
||||||
import { readAppLogFile } from './app-logs.js';
|
|
||||||
|
|
||||||
const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes
|
|
||||||
const LOG_ERROR_MARKER = '[error]'; // from electron-log
|
|
||||||
const EXPECTED_LOG_MARKERS = [
|
|
||||||
'[WINDOW_INIT]',
|
|
||||||
'[PRELOAD_INIT]',
|
|
||||||
'[APP_MOUNT_INIT]',
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function checkForErrors(stderr, windowTitles, projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const errors = await gatherErrors(stderr, windowTitles, projectDir);
|
|
||||||
if (errors.length) {
|
|
||||||
die(formatErrors(errors));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function gatherErrors(stderr, windowTitles, projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const logContent = await readAppLogFile(projectDir);
|
|
||||||
return [
|
|
||||||
verifyStdErr(stderr),
|
|
||||||
verifyApplicationLogsExist(logContent),
|
|
||||||
...EXPECTED_LOG_MARKERS.map((marker) => verifyLogMarkerExistsInLogs(logContent, marker)),
|
|
||||||
verifyWindowTitle(windowTitles),
|
|
||||||
verifyErrorsInLogs(logContent),
|
|
||||||
].filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatErrors(errors) {
|
|
||||||
if (!errors || !errors.length) { throw new Error('missing errors'); }
|
|
||||||
return [
|
|
||||||
'Errors detected during execution:',
|
|
||||||
...errors.map(
|
|
||||||
(error) => formatError(error),
|
|
||||||
),
|
|
||||||
].join('\n---\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatError(error) {
|
|
||||||
if (!error) { throw new Error('missing error'); }
|
|
||||||
if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); }
|
|
||||||
let message = `Reason: ${indentText(error.reason, 1)}`;
|
|
||||||
if (error.description) {
|
|
||||||
message += `\nDescription:\n${indentText(error.description, 2)}`;
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyApplicationLogsExist(logContent) {
|
|
||||||
if (!logContent || !logContent.length) {
|
|
||||||
return describeError(
|
|
||||||
'Missing application logs',
|
|
||||||
'Application logs are empty not were not found.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyLogMarkerExistsInLogs(logContent, marker) {
|
|
||||||
if (!marker) {
|
|
||||||
throw new Error('missing marker');
|
|
||||||
}
|
|
||||||
if (!logContent?.includes(marker)) {
|
|
||||||
return describeError(
|
|
||||||
'Incomplete application logs',
|
|
||||||
`Missing identifier "${marker}" in application logs.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyWindowTitle(windowTitles) {
|
|
||||||
const errorTitles = windowTitles.filter(
|
|
||||||
(title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE),
|
|
||||||
);
|
|
||||||
if (errorTitles.length) {
|
|
||||||
return describeError(
|
|
||||||
'Unexpected window title',
|
|
||||||
'One or more window titles suggest an error occurred in the application:'
|
|
||||||
+ `\nError Titles: ${errorTitles.join(', ')}`
|
|
||||||
+ `\nAll Titles: ${windowTitles.join(', ')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyStdErr(stderrOutput) {
|
|
||||||
if (stderrOutput && stderrOutput.length > 0) {
|
|
||||||
return describeError(
|
|
||||||
'Standard error stream (`stderr`) is not empty.',
|
|
||||||
stderrOutput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyErrorsInLogs(logContent) {
|
|
||||||
if (!logContent || !logContent.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const logLines = getNonEmptyLines(logContent)
|
|
||||||
.filter((line) => line.includes(LOG_ERROR_MARKER));
|
|
||||||
if (!logLines.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return describeError(
|
|
||||||
'Application log file',
|
|
||||||
logLines.join('\n'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function describeError(reason, description) {
|
|
||||||
return {
|
|
||||||
reason,
|
|
||||||
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNonEmptyLines(text) {
|
|
||||||
return splitTextIntoLines(text)
|
|
||||||
.filter((line) => line?.trim().length > 0);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { access, chmod } from 'fs/promises';
|
|
||||||
import { constants } from 'fs';
|
|
||||||
import { findSingleFileByExtension } from '../../utils/io.js';
|
|
||||||
import { log } from '../../utils/log.js';
|
|
||||||
|
|
||||||
export async function prepareLinuxApp(desktopDistPath) {
|
|
||||||
const { absolutePath: appFile } = await findSingleFileByExtension(
|
|
||||||
'AppImage',
|
|
||||||
desktopDistPath,
|
|
||||||
);
|
|
||||||
await makeExecutable(appFile);
|
|
||||||
return {
|
|
||||||
appExecutablePath: appFile,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function makeExecutable(appFile) {
|
|
||||||
if (!appFile) { throw new Error('missing file'); }
|
|
||||||
if (await isExecutable(appFile)) {
|
|
||||||
log('AppImage is already executable.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log('Making it executable...');
|
|
||||||
await chmod(appFile, 0o755);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isExecutable(file) {
|
|
||||||
try {
|
|
||||||
await access(file, constants.X_OK);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { mkdtemp, rmdir } from 'fs/promises';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { tmpdir } from 'os';
|
|
||||||
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
|
||||||
import { log, die } from '../../utils/log.js';
|
|
||||||
import { runCommand } from '../../utils/run-command.js';
|
|
||||||
|
|
||||||
export async function prepareWindowsApp(desktopDistPath) {
|
|
||||||
const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-'));
|
|
||||||
if (await exists(workdir)) {
|
|
||||||
log(`Temporary directory ${workdir} already exists, cleaning up...`);
|
|
||||||
await rmdir(workdir, { recursive: true });
|
|
||||||
}
|
|
||||||
const { appExecutablePath } = await installNsis(workdir, desktopDistPath);
|
|
||||||
return {
|
|
||||||
appExecutablePath,
|
|
||||||
cleanup: async () => {
|
|
||||||
log(`Cleaning up working directory ${workdir}...`);
|
|
||||||
await rmdir(workdir, { recursive: true });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installNsis(installationPath, desktopDistPath) {
|
|
||||||
const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath);
|
|
||||||
|
|
||||||
log(`Silently installing contents of ${installerPath} to ${installationPath}...`);
|
|
||||||
const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`);
|
|
||||||
if (error) {
|
|
||||||
die(`Failed to install.\n${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
appExecutablePath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { log } from './utils/log.js';
|
|
||||||
|
|
||||||
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
|
||||||
|
|
||||||
export const COMMAND_LINE_FLAGS = Object.freeze({
|
|
||||||
FORCE_REBUILD: '--build',
|
|
||||||
TAKE_SCREENSHOT: '--screenshot',
|
|
||||||
});
|
|
||||||
|
|
||||||
export function logCurrentArgs() {
|
|
||||||
if (!PROCESS_ARGUMENTS.length) {
|
|
||||||
log('No additional arguments provided.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log(`Arguments: ${PROCESS_ARGUMENTS.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasCommandLineFlag(flag) {
|
|
||||||
return PROCESS_ARGUMENTS.includes(flag);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export const DESKTOP_BUILD_COMMAND = 'npm run electron:prebuild && npm run electron:build -- --publish never';
|
|
||||||
export const PROJECT_DIR = process.cwd();
|
|
||||||
export const DESKTOP_DIST_PATH = join(PROJECT_DIR, 'dist');
|
|
||||||
export const APP_EXECUTION_DURATION_IN_SECONDS = 60; // Long enough for CI runners
|
|
||||||
export const SCREENSHOT_PATH = join(PROJECT_DIR, 'screenshot.png');
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { main } from './main.js';
|
|
||||||
|
|
||||||
await main();
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { logCurrentArgs, COMMAND_LINE_FLAGS, hasCommandLineFlag } from './cli-args.js';
|
|
||||||
import { log, die } from './utils/log.js';
|
|
||||||
import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm.js';
|
|
||||||
import { clearAppLogFile } from './app/app-logs.js';
|
|
||||||
import { checkForErrors } from './app/check-for-errors.js';
|
|
||||||
import { runApplication } from './app/runner.js';
|
|
||||||
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from './utils/platform.js';
|
|
||||||
import { prepareLinuxApp } from './app/extractors/linux.js';
|
|
||||||
import { prepareWindowsApp } from './app/extractors/windows.js';
|
|
||||||
import { prepareMacOsApp } from './app/extractors/macos.js';
|
|
||||||
import {
|
|
||||||
DESKTOP_BUILD_COMMAND,
|
|
||||||
PROJECT_DIR,
|
|
||||||
DESKTOP_DIST_PATH,
|
|
||||||
APP_EXECUTION_DURATION_IN_SECONDS,
|
|
||||||
SCREENSHOT_PATH,
|
|
||||||
} from './config.js';
|
|
||||||
|
|
||||||
export async function main() {
|
|
||||||
logCurrentArgs();
|
|
||||||
await ensureNpmProjectDir(PROJECT_DIR);
|
|
||||||
await npmInstall(PROJECT_DIR);
|
|
||||||
await npmBuild(
|
|
||||||
PROJECT_DIR,
|
|
||||||
DESKTOP_BUILD_COMMAND,
|
|
||||||
DESKTOP_DIST_PATH,
|
|
||||||
hasCommandLineFlag(COMMAND_LINE_FLAGS.FORCE_REBUILD),
|
|
||||||
);
|
|
||||||
await clearAppLogFile(PROJECT_DIR);
|
|
||||||
const {
|
|
||||||
stderr, stdout, isCrashed, windowTitles,
|
|
||||||
} = await extractAndRun();
|
|
||||||
if (stdout) {
|
|
||||||
log(`Output (stdout) from application execution:\n${stdout}`);
|
|
||||||
}
|
|
||||||
if (isCrashed) {
|
|
||||||
die('The application encountered an error during its execution.');
|
|
||||||
}
|
|
||||||
await checkForErrors(stderr, windowTitles, PROJECT_DIR);
|
|
||||||
log('🥳🎈 Success! Application completed without any runtime errors.');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractAndRun() {
|
|
||||||
const extractors = {
|
|
||||||
[SUPPORTED_PLATFORMS.MAC]: () => prepareMacOsApp(DESKTOP_DIST_PATH),
|
|
||||||
[SUPPORTED_PLATFORMS.LINUX]: () => prepareLinuxApp(DESKTOP_DIST_PATH),
|
|
||||||
[SUPPORTED_PLATFORMS.WINDOWS]: () => prepareWindowsApp(DESKTOP_DIST_PATH),
|
|
||||||
};
|
|
||||||
const extractor = extractors[CURRENT_PLATFORM];
|
|
||||||
if (!extractor) {
|
|
||||||
throw new Error(`Platform not supported: ${CURRENT_PLATFORM}`);
|
|
||||||
}
|
|
||||||
const { appExecutablePath, cleanup } = await extractor();
|
|
||||||
try {
|
|
||||||
return await runApplication(
|
|
||||||
appExecutablePath,
|
|
||||||
APP_EXECUTION_DURATION_IN_SECONDS,
|
|
||||||
hasCommandLineFlag(COMMAND_LINE_FLAGS.TAKE_SCREENSHOT),
|
|
||||||
SCREENSHOT_PATH,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (cleanup) {
|
|
||||||
log('Cleaning up post-execution resources...');
|
|
||||||
await cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { extname, join } from 'path';
|
|
||||||
import { readdir, access } from 'fs/promises';
|
|
||||||
import { constants } from 'fs';
|
|
||||||
import { log, die, LOG_LEVELS } from './log.js';
|
|
||||||
|
|
||||||
export async function findSingleFileByExtension(extension, directory) {
|
|
||||||
if (!directory) { throw new Error('Missing directory'); }
|
|
||||||
if (!extension) { throw new Error('Missing file extension'); }
|
|
||||||
|
|
||||||
if (!await exists(directory)) {
|
|
||||||
die(`Directory does not exist: ${directory}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const directoryContents = await readdir(directory);
|
|
||||||
const foundFileNames = directoryContents.filter((file) => extname(file) === `.${extension}`);
|
|
||||||
const withoutUninstaller = foundFileNames.filter(
|
|
||||||
(fileName) => !fileName.toLowerCase().includes('uninstall'), // NSIS build has `Uninstall {app-name}.exe`
|
|
||||||
);
|
|
||||||
if (!withoutUninstaller.length) {
|
|
||||||
die(`No ${extension} found in ${directory} directory.`);
|
|
||||||
}
|
|
||||||
if (withoutUninstaller.length > 1) {
|
|
||||||
log(`Found multiple ${extension} files: ${withoutUninstaller.join(', ')}. Using first occurrence`, LOG_LEVELS.WARN);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
absolutePath: join(directory, withoutUninstaller[0]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exists(path) {
|
|
||||||
if (!path) { throw new Error('Missing path'); }
|
|
||||||
try {
|
|
||||||
await access(path, constants.F_OK);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isDirMissingOrEmpty(dir) {
|
|
||||||
if (!dir) { throw new Error('Missing directory'); }
|
|
||||||
if (!await exists(dir)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const contents = await readdir(dir);
|
|
||||||
return contents.length === 0;
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
export const LOG_LEVELS = Object.freeze({
|
|
||||||
INFO: 'INFO',
|
|
||||||
WARN: 'WARN',
|
|
||||||
ERROR: 'ERROR',
|
|
||||||
});
|
|
||||||
|
|
||||||
export function log(message, level = LOG_LEVELS.INFO) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const config = LOG_LEVEL_CONFIG[level] || LOG_LEVEL_CONFIG[LOG_LEVELS.INFO];
|
|
||||||
const formattedMessage = `[${timestamp}][${config.color}${level}${COLOR_CODES.RESET}] ${message}`;
|
|
||||||
config.method(formattedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function die(message) {
|
|
||||||
log(message, LOG_LEVELS.ERROR);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLOR_CODES = {
|
|
||||||
RESET: '\x1b[0m',
|
|
||||||
LIGHT_RED: '\x1b[91m',
|
|
||||||
YELLOW: '\x1b[33m',
|
|
||||||
LIGHT_BLUE: '\x1b[94m',
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOG_LEVEL_CONFIG = {
|
|
||||||
[LOG_LEVELS.INFO]: {
|
|
||||||
color: COLOR_CODES.LIGHT_BLUE,
|
|
||||||
method: console.log,
|
|
||||||
},
|
|
||||||
[LOG_LEVELS.WARN]: {
|
|
||||||
color: COLOR_CODES.YELLOW,
|
|
||||||
method: console.warn,
|
|
||||||
},
|
|
||||||
[LOG_LEVELS.ERROR]: {
|
|
||||||
color: COLOR_CODES.LIGHT_RED,
|
|
||||||
method: console.error,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
import { rmdir, readFile } from 'fs/promises';
|
|
||||||
import { exists, isDirMissingOrEmpty } from './io.js';
|
|
||||||
import { runCommand } from './run-command.js';
|
|
||||||
import { LOG_LEVELS, die, log } from './log.js';
|
|
||||||
|
|
||||||
export async function ensureNpmProjectDir(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
if (!await exists(join(projectDir, 'package.json'))) {
|
|
||||||
die(`'package.json' not found in project directory: ${projectDir}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function npmInstall(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const npmModulesPath = join(projectDir, 'node_modules');
|
|
||||||
if (!await isDirMissingOrEmpty(npmModulesPath)) {
|
|
||||||
log(`Directory "${npmModulesPath}" exists and has content. Skipping \`npm install\`.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log('Starting dependency installation...');
|
|
||||||
const { error } = await runCommand('npm install --loglevel=error', {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: projectDir,
|
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
die(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function npmBuild(projectDir, buildCommand, distDir, forceRebuild) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
if (!buildCommand) { throw new Error('missing build command'); }
|
|
||||||
if (!distDir) { throw new Error('missing distribution directory'); }
|
|
||||||
|
|
||||||
const isMissingBuild = await isDirMissingOrEmpty(distDir);
|
|
||||||
|
|
||||||
if (!isMissingBuild && !forceRebuild) {
|
|
||||||
log(`Directory "${distDir}" exists and has content. Skipping build: '${buildCommand}'.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceRebuild) {
|
|
||||||
log(`Removing directory "${distDir}" for a clean build (triggered by --build flag).`);
|
|
||||||
await rmdir(distDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Starting project build...');
|
|
||||||
const { error } = await runCommand(buildCommand, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: projectDir,
|
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
log(error, LOG_LEVELS.WARN); // Cannot disable Vue CLI errors, stderr contains false-positives.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAppName(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const packageData = await readPackageJsonContents(projectDir);
|
|
||||||
try {
|
|
||||||
const packageJson = JSON.parse(packageData);
|
|
||||||
if (!packageJson.name) {
|
|
||||||
die(`The 'package.json' file doesn't specify a name: ${packageData}`);
|
|
||||||
}
|
|
||||||
return packageJson.name;
|
|
||||||
} catch (error) {
|
|
||||||
die(`Unable to parse 'package.json'. Error: ${error}\nContent: ${packageData}`, LOG_LEVELS.ERROR);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readPackageJsonContents(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const packagePath = join(projectDir, 'package.json');
|
|
||||||
if (!await exists(packagePath)) {
|
|
||||||
die(`'package.json' file not found at ${packagePath}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const packageData = await readFile(packagePath, 'utf8');
|
|
||||||
return packageData;
|
|
||||||
} catch (error) {
|
|
||||||
log(`Error reading 'package.json' from ${packagePath}.`, LOG_LEVELS.ERROR);
|
|
||||||
die(`Error detail: ${error}`, LOG_LEVELS.ERROR);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { platform } from 'os';
|
|
||||||
|
|
||||||
export const SUPPORTED_PLATFORMS = {
|
|
||||||
MAC: 'darwin',
|
|
||||||
LINUX: 'linux',
|
|
||||||
WINDOWS: 'win32',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CURRENT_PLATFORM = platform();
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Description:
|
|
||||||
# This script ensures npm is available, removes existing node modules, optionally
|
|
||||||
# removes package-lock.json (when -n flag is used), installs dependencies and runs unit tests.
|
|
||||||
# Usage:
|
|
||||||
# ./fresh-npm-install.sh # Regular execution
|
|
||||||
# ./fresh-npm-install.sh -n # Non-deterministic mode (removes package-lock.json)
|
|
||||||
|
|
||||||
declare NON_DETERMINISTIC_FLAG=0
|
|
||||||
|
|
||||||
|
|
||||||
main() {
|
|
||||||
parse_args "$@"
|
|
||||||
ensure_npm_is_available
|
|
||||||
ensure_npm_root
|
|
||||||
remove_existing_modules
|
|
||||||
if [[ $NON_DETERMINISTIC_FLAG -eq 1 ]]; then
|
|
||||||
remove_package_lock_json
|
|
||||||
fi
|
|
||||||
install_dependencies
|
|
||||||
run_unit_tests
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_npm_is_available() {
|
|
||||||
if ! command -v npm &> /dev/null; then
|
|
||||||
log::fatal 'npm could not be found, please install it first.'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_npm_root() {
|
|
||||||
if [ ! -f package.json ]; then
|
|
||||||
log::fatal 'Current directory is not a npm root. Please run the script in a npm root directory.'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_existing_modules() {
|
|
||||||
if [ -d ./node_modules ]; then
|
|
||||||
log::info 'Removing existing node modules...'
|
|
||||||
if ! rm -rf ./node_modules; then
|
|
||||||
log::fatal 'Could not remove existing node modules.'
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_dependencies() {
|
|
||||||
log::info 'Installing dependencies...'
|
|
||||||
if ! npm install; then
|
|
||||||
log::fatal 'Failed to install dependencies.'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_package_lock_json() {
|
|
||||||
if [ -f ./package-lock.json ]; then
|
|
||||||
log::info 'Removing package-lock.json...'
|
|
||||||
if ! rm -rf ./package-lock.json; then
|
|
||||||
log::fatal 'Could not remove package-lock.json.'
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_unit_tests() {
|
|
||||||
log::info 'Running unit tests...'
|
|
||||||
if ! npm run test:unit; then
|
|
||||||
pwd
|
|
||||||
log::fatal 'Failed to run unit tests.'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info() {
|
|
||||||
local -r message="$1"
|
|
||||||
echo "📣 ${message}"
|
|
||||||
}
|
|
||||||
|
|
||||||
log::fatal() {
|
|
||||||
local -r message="$1"
|
|
||||||
echo "❌ ${message}" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_args() {
|
|
||||||
while getopts "n" opt; do
|
|
||||||
case ${opt} in
|
|
||||||
n)
|
|
||||||
NON_DETERMINISTIC_FLAG=1
|
|
||||||
;;
|
|
||||||
\?)
|
|
||||||
echo "Invalid option: $OPTARG" 1>&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$1"
|
|
||||||
199
scripts/npm-install.js
Normal file
199
scripts/npm-install.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
Description:
|
||||||
|
This script manages NPM dependencies for a project.
|
||||||
|
It offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
npm run install-deps [-- <options>]
|
||||||
|
node scripts/npm-install.js [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--root-directory <path>
|
||||||
|
Specifies the root directory where package.json resides
|
||||||
|
Defaults to the current working directory.
|
||||||
|
Example: npm run install-deps -- --root-directory /your/path/here
|
||||||
|
|
||||||
|
--no-errors
|
||||||
|
Ignores errors and continues the execution.
|
||||||
|
Example: npm run install-deps -- --no-errors
|
||||||
|
|
||||||
|
--ci
|
||||||
|
Uses 'npm ci' for dependency installation instead of 'npm install'.
|
||||||
|
Example: npm run install-deps -- --ci
|
||||||
|
|
||||||
|
--fresh
|
||||||
|
Removes the existing node_modules directory before installing dependencies.
|
||||||
|
Example: npm run install-deps -- --fresh
|
||||||
|
|
||||||
|
--non-deterministic
|
||||||
|
Removes package-lock.json for a non-deterministic installation.
|
||||||
|
Example: npm run install-deps -- --non-deterministic
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
Flags can be combined as needed.
|
||||||
|
Example: npm run install-deps -- --fresh --non-deterministic
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { access, rm, unlink } from 'fs/promises';
|
||||||
|
import { constants } from 'fs';
|
||||||
|
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
|
const RETRY_DELAY_IN_MS = 5 /* seconds */ * 1000;
|
||||||
|
const ARG_NAMES = {
|
||||||
|
rootDirectory: '--root-directory',
|
||||||
|
ignoreErrors: '--no-errors',
|
||||||
|
ci: '--ci',
|
||||||
|
fresh: '--fresh',
|
||||||
|
nonDeterministic: '--non-deterministic',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const options = getOptions();
|
||||||
|
console.log('Options:', options);
|
||||||
|
await ensureNpmRootDirectory(options.rootDirectory);
|
||||||
|
await ensureNpmIsAvailable();
|
||||||
|
if (options.fresh) {
|
||||||
|
await removeNodeModules(options.rootDirectory);
|
||||||
|
}
|
||||||
|
if (options.nonDeterministic) {
|
||||||
|
await removePackageLockJson(options.rootDirectory);
|
||||||
|
}
|
||||||
|
const command = buildCommand(options.ci, options.outputErrors);
|
||||||
|
console.log('Starting dependency installation...');
|
||||||
|
const exitCode = await executeWithRetry(
|
||||||
|
command,
|
||||||
|
options.workingDirectory,
|
||||||
|
MAX_RETRIES,
|
||||||
|
RETRY_DELAY_IN_MS,
|
||||||
|
);
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log('🎊 Installed dependencies...');
|
||||||
|
} else {
|
||||||
|
console.error(`💀 Failed to install dependencies, exit code: ${exitCode}`);
|
||||||
|
}
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeNodeModules(workingDirectory) {
|
||||||
|
const nodeModulesDirectory = resolve(workingDirectory, 'node_modules');
|
||||||
|
if (await exists('./node_modules')) {
|
||||||
|
console.log('Removing node_modules...');
|
||||||
|
await rm(nodeModulesDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removePackageLockJson(workingDirectory) {
|
||||||
|
const packageLockJsonFile = resolve(workingDirectory, 'package-lock.json');
|
||||||
|
if (await exists(packageLockJsonFile)) {
|
||||||
|
console.log('Removing package-lock.json...');
|
||||||
|
await unlink(packageLockJsonFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureNpmIsAvailable() {
|
||||||
|
const exitCode = await executeCommand('npm --version');
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
throw new Error('`npm` in not available!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureNpmRootDirectory(workingDirectory) {
|
||||||
|
const packageJsonPath = resolve(workingDirectory, 'package.json');
|
||||||
|
if (!await exists(packageJsonPath)) {
|
||||||
|
throw new Error(`Not an NPM project root: ${workingDirectory}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCommand(ci, outputErrors) {
|
||||||
|
const baseCommand = ci ? 'npm ci' : 'npm install';
|
||||||
|
if (!outputErrors) {
|
||||||
|
return `${baseCommand} --loglevel=error`;
|
||||||
|
}
|
||||||
|
return baseCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions() {
|
||||||
|
const processArgs = process.argv.slice(2); // Slice off the node and script name
|
||||||
|
return {
|
||||||
|
rootDirectory: processArgs.includes('--root-directory') ? processArgs[processArgs.indexOf('--root-directory') + 1] : process.cwd(),
|
||||||
|
outputErrors: !processArgs.includes(ARG_NAMES.ignoreErrors),
|
||||||
|
ci: processArgs.includes(ARG_NAMES.ci),
|
||||||
|
fresh: processArgs.includes(ARG_NAMES.fresh),
|
||||||
|
nonDeterministic: processArgs.includes(ARG_NAMES.nonDeterministic),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeWithRetry(
|
||||||
|
command,
|
||||||
|
workingDirectory,
|
||||||
|
maxRetries,
|
||||||
|
retryDelayInMs,
|
||||||
|
currentAttempt = 1,
|
||||||
|
) {
|
||||||
|
const statusCode = await executeCommand(command, workingDirectory, true, true);
|
||||||
|
if (statusCode === 0 || currentAttempt >= maxRetries) {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`⚠️🔄 Attempt ${currentAttempt} failed. Retrying in ${retryDelayInMs / 1000} seconds...`);
|
||||||
|
await sleep(retryDelayInMs);
|
||||||
|
|
||||||
|
const retryResult = await executeWithRetry(
|
||||||
|
command,
|
||||||
|
workingDirectory,
|
||||||
|
maxRetries,
|
||||||
|
retryDelayInMs,
|
||||||
|
currentAttempt + 1,
|
||||||
|
);
|
||||||
|
return retryResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCommand(
|
||||||
|
command,
|
||||||
|
workingDirectory = process.cwd(),
|
||||||
|
logStdout = false,
|
||||||
|
logCommand = false,
|
||||||
|
) {
|
||||||
|
if (logCommand) {
|
||||||
|
console.log(`▶️ Executing command "${command}" at "${workingDirectory}"`);
|
||||||
|
}
|
||||||
|
const process = exec(
|
||||||
|
command,
|
||||||
|
{
|
||||||
|
cwd: workingDirectory,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (logStdout) {
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
console.log(data.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
console.error(data.toString());
|
||||||
|
});
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
process.on('exit', (code) => {
|
||||||
|
resolve(code);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(milliseconds) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, milliseconds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(path) {
|
||||||
|
try {
|
||||||
|
await access(path, constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
58
scripts/print-dist-dir.js
Normal file
58
scripts/print-dist-dir.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Description:
|
||||||
|
* This script determines the absolute path of a distribution directory based on CLI arguments
|
||||||
|
* and outputs its absolute path. It is designed to be run programmatically by other scripts.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/print-dist-dir.js [options]
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* --electron-unbundled Path for the unbundled Electron application
|
||||||
|
* --electron-bundled Path for the bundled Electron application
|
||||||
|
* --web Path for the web application
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
|
||||||
|
const DIST_DIRS_JSON_FILE_PATH = resolve(process.cwd(), 'dist-dirs.json'); // cannot statically import because ESLint does not support it https://github.com/eslint/eslint/discussions/15305
|
||||||
|
const CLI_ARGUMENTS = process.argv.slice(2);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const distDirs = await readDistDirsJsonFile(DIST_DIRS_JSON_FILE_PATH);
|
||||||
|
const relativeDistDir = determineRelativeDistDir(distDirs, CLI_ARGUMENTS);
|
||||||
|
const absoluteDistDir = resolve(process.cwd(), relativeDistDir);
|
||||||
|
console.log(absoluteDistDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapCliFlagsToDistDirs(distDirs) {
|
||||||
|
return {
|
||||||
|
'--electron-unbundled': distDirs.electronUnbundled,
|
||||||
|
'--electron-bundled': distDirs.electronBundled,
|
||||||
|
'--web': distDirs.web,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineRelativeDistDir(distDirsJsonObject, cliArguments) {
|
||||||
|
const cliFlagDistDirMap = mapCliFlagsToDistDirs(distDirsJsonObject);
|
||||||
|
const availableCliFlags = Object.keys(cliFlagDistDirMap);
|
||||||
|
const requestedCliFlags = cliArguments.filter((arg) => {
|
||||||
|
return availableCliFlags.includes(arg);
|
||||||
|
});
|
||||||
|
if (!requestedCliFlags.length) {
|
||||||
|
throw new Error(`No distribution directory was requested. Please use one of these flags: ${availableCliFlags.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (requestedCliFlags.length > 1) {
|
||||||
|
throw new Error(`Multiple distribution directories were requested, but this script only supports one: ${requestedCliFlags.join(', ')}`);
|
||||||
|
}
|
||||||
|
const selectedCliFlag = requestedCliFlags[0];
|
||||||
|
return cliFlagDistDirMap[selectedCliFlag];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readDistDirsJsonFile(absoluteConfigJsonFilePath) {
|
||||||
|
const fileContentAsText = await readFile(absoluteConfigJsonFilePath, 'utf8');
|
||||||
|
const parsedJsonData = JSON.parse(fileContentAsText);
|
||||||
|
return parsedJsonData;
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
133
scripts/verify-build-artifacts.js
Normal file
133
scripts/verify-build-artifacts.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Description:
|
||||||
|
* This script verifies the existence and content of build artifacts based on the
|
||||||
|
* provided CLI flags. It exists with exit code `0` if all verifications pass, otherwise
|
||||||
|
* with exit code `1`.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/verify-build-artifacts.js [options]
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* --electron-unbundled Verify artifacts for the unbundled Electron application.
|
||||||
|
* --electron-bundled Verify artifacts for the bundled Electron application.
|
||||||
|
* --web Verify artifacts for the web application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { access, readdir } from 'fs/promises';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
||||||
|
const PRINT_DIST_DIR_SCRIPT_BASE_COMMAND = 'node scripts/print-dist-dir';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const buildConfigs = getBuildVerificationConfigs();
|
||||||
|
if (!anyCommandsFound(Object.keys(buildConfigs))) {
|
||||||
|
die(`No valid command found in process arguments. Expected one of: ${Object.keys(buildConfigs).join(', ')}`);
|
||||||
|
}
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
for (const [command, config] of Object.entries(buildConfigs)) {
|
||||||
|
if (PROCESS_ARGUMENTS.includes(command)) {
|
||||||
|
const distDir = await executePrintDistDirScript(config.printDistDirScriptArgument);
|
||||||
|
await verifyDirectoryExists(distDir);
|
||||||
|
await verifyNonEmptyDirectory(distDir);
|
||||||
|
await verifyFilesExist(distDir, config.filePatterns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable no-await-in-loop */
|
||||||
|
console.log('✅ Build completed successfully and all expected artifacts are in place.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBuildVerificationConfigs() {
|
||||||
|
return {
|
||||||
|
'--electron-unbundled': {
|
||||||
|
printDistDirScriptArgument: '--electron-unbundled',
|
||||||
|
filePatterns: [
|
||||||
|
/main[/\\]index\.cjs/,
|
||||||
|
/preload[/\\]index\.cjs/,
|
||||||
|
/renderer[/\\]index\.htm(l)?/,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'--electron-bundled': {
|
||||||
|
printDistDirScriptArgument: '--electron-bundled',
|
||||||
|
filePatterns: [
|
||||||
|
/latest.*\.yml/, // generates latest.yml for auto-updates
|
||||||
|
/.*-\d+\.\d+\.\d+\..*/, // a file with extension and semantic version (packaged application)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'--web': {
|
||||||
|
printDistDirScriptArgument: '--web',
|
||||||
|
filePatterns: [
|
||||||
|
/index\.htm(l)?/,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function anyCommandsFound(commands) {
|
||||||
|
return PROCESS_ARGUMENTS.some((arg) => commands.includes(arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyDirectoryExists(directoryPath) {
|
||||||
|
try {
|
||||||
|
await access(directoryPath);
|
||||||
|
} catch (error) {
|
||||||
|
die(`Directory does not exist at \`${directoryPath}\`:\n\t${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyNonEmptyDirectory(directoryPath) {
|
||||||
|
const files = await readdir(directoryPath);
|
||||||
|
if (files.length === 0) {
|
||||||
|
die(`Directory is empty at \`${directoryPath}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyFilesExist(directoryPath, filePatterns) {
|
||||||
|
const files = await listAllFilesRecursively(directoryPath);
|
||||||
|
for (const pattern of filePatterns) {
|
||||||
|
const match = files.some((file) => pattern.test(file));
|
||||||
|
if (!match) {
|
||||||
|
die(
|
||||||
|
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
||||||
|
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listAllFilesRecursively(directoryPath) {
|
||||||
|
const dir = await readdir(directoryPath, { withFileTypes: true });
|
||||||
|
const files = await Promise.all(dir.map(async (dirent) => {
|
||||||
|
const absolutePath = resolve(directoryPath, dirent.name);
|
||||||
|
if (dirent.isDirectory()) {
|
||||||
|
return listAllFilesRecursively(absolutePath);
|
||||||
|
}
|
||||||
|
return absolutePath;
|
||||||
|
}));
|
||||||
|
return files.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePrintDistDirScript(flag) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const commandToRun = `${PRINT_DIST_DIR_SCRIPT_BASE_COMMAND} ${flag}`;
|
||||||
|
|
||||||
|
exec(commandToRun, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(`Execution failed with error: ${error}`));
|
||||||
|
} else if (stderr) {
|
||||||
|
reject(new Error(`Execution failed with stderr: ${stderr}`));
|
||||||
|
} else {
|
||||||
|
resolve(stdout.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function die(...message) {
|
||||||
|
console.error(...message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export type Constructible<T, TArgs extends unknown[] = never> = {
|
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||||
prototype: T;
|
prototype: T;
|
||||||
apply: (this: unknown, args: TArgs) => void;
|
apply: (this: unknown, args: TArgs) => void;
|
||||||
|
readonly name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropertyKeys<T> = {
|
export type PropertyKeys<T> = {
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
import { IApplicationFactory } from '../IApplicationFactory';
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
import { ApplicationContext } from './ApplicationContext';
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
|
||||||
export async function buildContext(
|
export async function buildContext(
|
||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = Environment.CurrentEnvironment,
|
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
): Promise<IApplicationContext> {
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('missing factory'); }
|
if (!factory) { throw new Error('missing factory'); }
|
||||||
if (!environment) { throw new Error('missing environment'); }
|
if (!environment) { throw new Error('missing environment'); }
|
||||||
const app = await factory.getApp();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment);
|
const os = getInitialOs(app, environment.os);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
||||||
const currentOs = environment.os;
|
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import MacOsData from '@/application/collections/macos.yaml';
|
|||||||
import LinuxData from '@/application/collections/linux.yaml';
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
categoryParser = parseCategoryCollection,
|
categoryParser = parseCategoryCollection,
|
||||||
informationParser = parseProjectInformation,
|
informationParser = parseProjectInformation,
|
||||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||||
collectionsData = PreParsedCollections,
|
collectionsData = PreParsedCollections,
|
||||||
): IApplication {
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData);
|
validateCollectionsData(collectionsData);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { ConstructorArguments } from '@/TypeHelpers';
|
import { ConstructorArguments } from '@/TypeHelpers';
|
||||||
|
|
||||||
export function
|
export function
|
||||||
parseProjectInformation(
|
parseProjectInformation(
|
||||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||||
createProjectInformation: ProjectInformationFactory = (
|
createProjectInformation: ProjectInformationFactory = (
|
||||||
...args
|
...args
|
||||||
) => new ProjectInformation(...args),
|
) => new ProjectInformation(...args),
|
||||||
|
|||||||
@@ -3558,82 +3558,98 @@ functions:
|
|||||||
parameters:
|
parameters:
|
||||||
- name: prefName
|
- name: prefName
|
||||||
- name: jsonValue
|
- name: jsonValue
|
||||||
# prefs.js file (https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file) exists at
|
docs: |-
|
||||||
# - Default installation:
|
This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
|
||||||
# ~/.mozilla/firefox/<profile-name>/prefs.js
|
|
||||||
# - Flatpak installation:
|
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
|
||||||
# ~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/prefs.js
|
|
||||||
# - Snap installation:
|
- Default: `~/.mozilla/firefox/<profile-name>/user.js`
|
||||||
# ~/snap/firefox/common/.mozilla/firefox/<profile-name>/prefs.js
|
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
|
||||||
|
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
|
||||||
|
|
||||||
|
While the `user.js` file is optional [2], if it's present, the Firefox application will prioritize its settings over
|
||||||
|
those in `prefs.js` upon startup [1][2]. To prevent potential profile corruption, Mozilla advises against editing
|
||||||
|
`prefs.js` directly [2].
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
|
||||||
|
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
|
||||||
code: |-
|
code: |-
|
||||||
pref_name='{{ $prefName }}'
|
pref_name='{{ $prefName }}'
|
||||||
pref_value='{{ $jsonValue }}'
|
pref_value='{{ $jsonValue }}'
|
||||||
echo "Setting preference \"$pref_name\" to \"$pref_value\"."
|
echo "Setting preference \"$pref_name\" to \"$pref_value\"."
|
||||||
pref_file_paths=(
|
declare -a profile_paths=(
|
||||||
~/.mozilla/firefox/*/prefs.js
|
~/.mozilla/firefox/*/
|
||||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js
|
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||||
~/snap/firefox/common/.mozilla/firefox/*/prefs.js
|
~/snap/firefox/common/.mozilla/firefox/*/
|
||||||
)
|
)
|
||||||
declare -i total_files_found=0
|
declare -i total_profiles_found=0
|
||||||
for pref_file in "${pref_file_paths[@]}"; do
|
for profile_dir in "${profile_paths[@]}"; do
|
||||||
if [ -f "$pref_file" ]; then
|
if [ ! -d "$profile_dir" ]; then
|
||||||
((total_files_found++))
|
continue
|
||||||
echo "$pref_file:"
|
fi
|
||||||
pref_start="user_pref(\"$pref_name\","
|
if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
|
||||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
continue # Not a profile folder
|
||||||
if ! grep --quiet "^$pref_start" "${pref_file}"; then
|
fi
|
||||||
echo $'\t'"Preference is not configured before."
|
((total_profiles_found++))
|
||||||
echo -n $'\n'"$pref_line" >> "$pref_file"
|
user_js_file="${profile_dir}user.js"
|
||||||
echo $'\t'"Successfully configured."
|
echo "$user_js_file:"
|
||||||
else
|
if [ ! -f "$user_js_file" ]; then
|
||||||
if grep --quiet "^$pref_line$" "${pref_file}"; then
|
touch "$user_js_file"
|
||||||
echo $'\t'"Skipping. Preference is already configured as expected."
|
echo $'\t''Created new user.js file'
|
||||||
else
|
fi
|
||||||
sed --in-place "/^$pref_start/d" "$pref_file"
|
pref_start="user_pref(\"$pref_name\","
|
||||||
echo $'\t'"Deleted assignment with unexpected value."
|
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||||
echo -n $'\n'"$pref_line" >> "$pref_file"
|
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
||||||
echo $'\t'"Successfully reconfigured with expected value."
|
echo -n $'\n'"$pref_line" >> "$user_js_file"
|
||||||
fi
|
echo $'\t'"Successfully added a new preference in $user_js_file."
|
||||||
fi
|
elif grep --quiet "^$pref_line$" "$user_js_file"; then
|
||||||
|
echo $'\t'"Skipping, preference is already set as expected in $user_js_file."
|
||||||
|
else
|
||||||
|
sed --in-place "/^$pref_start/c\\$pref_line" "$user_js_file"
|
||||||
|
echo $'\t'"Successfully replaced the existing incorrect preference in $user_js_file."
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ "$total_files_found" -eq 0 ]; then
|
if [ "$total_profiles_found" -eq 0 ]; then
|
||||||
echo "No changes, no preference file is found."
|
echo 'No profile folders are found, no changes are made.'
|
||||||
else
|
else
|
||||||
echo "Ensured that $total_files_found profiles are compilant."
|
echo "Preferences verified in $total_profiles_found profiles."
|
||||||
fi
|
fi
|
||||||
revertCode: |-
|
revertCode: |-
|
||||||
pref_name='{{ $prefName }}'
|
pref_name='{{ $prefName }}'
|
||||||
pref_value='{{ $jsonValue }}'
|
pref_value='{{ $jsonValue }}'
|
||||||
echo "Restoring \"$pref_name\" to its default."
|
echo "Reverting preference: \"$pref_name\" to its default."
|
||||||
pref_file_paths=(
|
declare -a profile_paths=(
|
||||||
~/.mozilla/firefox/*/prefs.js
|
~/.mozilla/firefox/*/
|
||||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js
|
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||||
~/snap/firefox/common/.mozilla/firefox/*/prefs.js
|
~/snap/firefox/common/.mozilla/firefox/*/
|
||||||
)
|
)
|
||||||
declare -i total_files_found=0
|
declare -i total_profiles_found=0
|
||||||
for pref_file in "${pref_file_paths[@]}"; do
|
for profile_dir in "${profile_paths[@]}"; do
|
||||||
if [ -f "$pref_file" ]; then
|
user_js_file="${profile_dir}user.js"
|
||||||
((total_files_found++))
|
if [ ! -f "$user_js_file" ]; then
|
||||||
echo "$pref_file:"
|
continue
|
||||||
pref_start="user_pref(\"$pref_name\","
|
fi
|
||||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
((total_profiles_found++))
|
||||||
if ! grep --quiet "^$pref_start" "${pref_file}"; then
|
echo "$user_js_file:"
|
||||||
echo $'\t'"Skipping. Preference is not configured before."
|
pref_start="user_pref(\"$pref_name\","
|
||||||
else
|
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||||
if grep --quiet "^$pref_line$" "${pref_file}"; then
|
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
||||||
sed --in-place "/^$pref_line/d" "$pref_file"
|
echo $'\t''Skipping, preference was not configured before.'
|
||||||
echo $'\t'"Successfully restored preference value to its default."
|
elif grep --quiet "^$pref_line$" "${user_js_file}"; then
|
||||||
else
|
sed --in-place "/^$pref_line/d" "$user_js_file"
|
||||||
echo $'\t'"Skipping, the preference has value that is not configured by privacy.sexy."
|
echo $'\t''Successfully reverted preference to default.'
|
||||||
fi
|
if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
|
||||||
|
rm "$user_js_file"
|
||||||
|
echo $'\t''Removed user.js file as it became empty.'
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ "$total_files_found" -eq 0 ]; then
|
if [ "$total_profiles_found" -eq 0 ]; then
|
||||||
echo "No changes, no preference file is found."
|
echo 'No reversion was necessary.'
|
||||||
else
|
else
|
||||||
echo "Ensured that $total_files_found profiles are compilant."
|
echo "Preferences verified in $total_profiles_found profiles."
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: RenameFile
|
name: RenameFile
|
||||||
|
|||||||
@@ -2456,7 +2456,7 @@ actions:
|
|||||||
recommend: standard
|
recommend: standard
|
||||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.EdgeUI::DisableRecentApps
|
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.EdgeUI::DisableRecentApps
|
||||||
code: reg add "HKCU\Software\Policies\Microsoft\Windows\EdgeUI" /v "DisableRecentApps" /t REG_DWORD /d 1 /f
|
code: reg add "HKCU\Software\Policies\Microsoft\Windows\EdgeUI" /v "DisableRecentApps" /t REG_DWORD /d 1 /f
|
||||||
revertCode: reg add "HKCU\Software\Policies\Microsoft\Windows\EdgeUI" /v "DisableRecentApps" /t REG_DWORD /d 0/f
|
revertCode: reg add "HKCU\Software\Policies\Microsoft\Windows\EdgeUI" /v "DisableRecentApps" /t REG_DWORD /d 0 /f
|
||||||
-
|
-
|
||||||
name: Turn off backtracking
|
name: Turn off backtracking
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -3871,7 +3871,7 @@ actions:
|
|||||||
code: reg add "HKLM\Software\Policies\Microsoft\Windows Defender\Scan" /v "DisableRestorePoint" /t REG_DWORD /d "1" /f
|
code: reg add "HKLM\Software\Policies\Microsoft\Windows Defender\Scan" /v "DisableRestorePoint" /t REG_DWORD /d "1" /f
|
||||||
revertCode: reg delete "HKLM\Software\Policies\Microsoft\Windows Defender\Scan" /v "DisableRestorePoint" /f 2>nul
|
revertCode: reg delete "HKLM\Software\Policies\Microsoft\Windows Defender\Scan" /v "DisableRestorePoint" /f 2>nul
|
||||||
-
|
-
|
||||||
name: Set minumum time for keeping files in scan history folder
|
name: Set minimum time for keeping files in scan history folder
|
||||||
docs:
|
docs:
|
||||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_PurgeItemsAfterDelay
|
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_PurgeItemsAfterDelay
|
||||||
# Managing with MpPreference module:
|
# Managing with MpPreference module:
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations';
|
||||||
|
|
||||||
export class CodeRunner {
|
export class CodeRunner {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly environment = Environment.CurrentEnvironment,
|
private readonly system = getWindowInjectedSystemOperations(),
|
||||||
|
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
) {
|
) {
|
||||||
if (!environment.system) {
|
if (!system) {
|
||||||
throw new Error('missing system operations');
|
throw new Error('missing system operations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||||
const { system } = this.environment;
|
const { os } = this.environment;
|
||||||
const dir = system.location.combinePaths(
|
const dir = this.system.location.combinePaths(
|
||||||
system.operatingSystem.getTempDirectory(),
|
this.system.operatingSystem.getTempDirectory(),
|
||||||
folderName,
|
folderName,
|
||||||
);
|
);
|
||||||
await system.fileSystem.createDirectory(dir, true);
|
await this.system.fileSystem.createDirectory(dir, true);
|
||||||
const filePath = system.location.combinePaths(dir, `run.${fileExtension}`);
|
const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`);
|
||||||
await system.fileSystem.writeToFile(filePath, code);
|
await this.system.fileSystem.writeToFile(filePath, code);
|
||||||
await system.fileSystem.setFilePermissions(filePath, '755');
|
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||||
const command = getExecuteCommand(filePath, this.environment);
|
const command = getExecuteCommand(filePath, os);
|
||||||
system.command.execute(command);
|
this.system.command.execute(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
function getExecuteCommand(
|
||||||
switch (environment.os) {
|
scriptPath: string,
|
||||||
|
currentOperatingSystem: OperatingSystem,
|
||||||
|
): string {
|
||||||
|
switch (currentOperatingSystem) {
|
||||||
case OperatingSystem.Linux:
|
case OperatingSystem.Linux:
|
||||||
return `x-terminal-emulator -e '${scriptPath}'`;
|
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||||
case OperatingSystem.macOS:
|
case OperatingSystem.macOS:
|
||||||
@@ -37,6 +42,6 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
|
|||||||
case OperatingSystem.Windows:
|
case OperatingSystem.Windows:
|
||||||
return scriptPath;
|
return scriptPath;
|
||||||
default:
|
default:
|
||||||
throw Error(`unsupported os: ${OperatingSystem[environment.os]}`);
|
throw Error(`unsupported os: ${OperatingSystem[currentOperatingSystem]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
|
||||||
|
|
||||||
export interface IEnvironment {
|
|
||||||
readonly isDesktop: boolean;
|
|
||||||
readonly os: OperatingSystem | undefined;
|
|
||||||
readonly system: ISystemOperations | undefined;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { ISystemOperations } from './SystemOperations/ISystemOperations';
|
|
||||||
|
|
||||||
export type WindowVariables = {
|
|
||||||
system: ISystemOperations;
|
|
||||||
isDesktop: boolean;
|
|
||||||
os: OperatingSystem;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
interface Window extends WindowVariables { }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { IEnvironmentVariablesFactory } from './IEnvironmentVariablesFactory';
|
||||||
|
import { validateEnvironmentVariables } from './EnvironmentVariablesValidator';
|
||||||
|
import { ViteEnvironmentVariables } from './Vite/ViteEnvironmentVariables';
|
||||||
|
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||||
|
|
||||||
|
export class EnvironmentVariablesFactory implements IEnvironmentVariablesFactory {
|
||||||
|
public static readonly Current = new EnvironmentVariablesFactory();
|
||||||
|
|
||||||
|
public readonly instance: IEnvironmentVariables;
|
||||||
|
|
||||||
|
protected constructor(validator: EnvironmentVariablesValidator = validateEnvironmentVariables) {
|
||||||
|
const environment = new ViteEnvironmentVariables();
|
||||||
|
validator(environment);
|
||||||
|
this.instance = environment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnvironmentVariablesValidator = typeof validateEnvironmentVariables;
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||||
|
|
||||||
/* Validation is externalized to keep the environment objects simple */
|
/* Validation is externalized to keep the environment objects simple */
|
||||||
export function validateMetadata(metadata: IAppMetadata): void {
|
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
||||||
if (!metadata) {
|
if (!environment) {
|
||||||
throw new Error('missing metadata');
|
throw new Error('missing environment');
|
||||||
}
|
}
|
||||||
const keyValues = capturePropertyValues(metadata);
|
const keyValues = capturePropertyValues(environment);
|
||||||
if (!Object.keys(keyValues).length) {
|
if (!Object.keys(keyValues).length) {
|
||||||
throw new Error('Unable to capture metadata key/value pairs');
|
throw new Error('Unable to capture key/value pairs');
|
||||||
}
|
}
|
||||||
const keysMissingValue = getMissingMetadataKeys(keyValues);
|
const keysMissingValue = getKeysMissingValues(keyValues);
|
||||||
if (keysMissingValue.length > 0) {
|
if (keysMissingValue.length > 0) {
|
||||||
throw new Error(`Metadata keys missing: ${keysMissingValue.join(', ')}`);
|
throw new Error(`Environment keys missing: ${keysMissingValue.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMissingMetadataKeys(keyValuePairs: Record<string, unknown>): string[] {
|
function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[] {
|
||||||
return Object.entries(keyValuePairs)
|
return Object.entries(keyValuePairs)
|
||||||
.reduce((acc, [key, value]) => {
|
.reduce((acc, [key, value]) => {
|
||||||
if (!value) {
|
if (!value && typeof value !== 'boolean') {
|
||||||
acc.push(key);
|
acc.push(key);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Represents essential metadata about the application.
|
* Represents essential metadata about the application.
|
||||||
*
|
|
||||||
* Designed to decouple the process of retrieving metadata
|
|
||||||
* (e.g., from the build environment) from the rest of the application.
|
|
||||||
*/
|
*/
|
||||||
export interface IAppMetadata {
|
export interface IAppMetadata {
|
||||||
readonly version: string;
|
readonly version: string;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IAppMetadata } from './IAppMetadata';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Designed to decouple the process of retrieving environment variables
|
||||||
|
* (e.g., from the build environment) from the rest of the application.
|
||||||
|
*/
|
||||||
|
export interface IEnvironmentVariables extends IAppMetadata {
|
||||||
|
readonly isNonProduction: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||||
|
|
||||||
|
export interface IEnvironmentVariablesFactory {
|
||||||
|
readonly instance: IEnvironmentVariables;
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
// Only variables prefixed with VITE_ are exposed to Vite-processed code
|
// Only variables prefixed with VITE_ are exposed to Vite-processed code
|
||||||
export const VITE_ENVIRONMENT_KEYS = {
|
export const VITE_USER_DEFINED_ENVIRONMENT_KEYS = {
|
||||||
VERSION: 'VITE_APP_VERSION',
|
VERSION: 'VITE_APP_VERSION',
|
||||||
NAME: 'VITE_APP_NAME',
|
NAME: 'VITE_APP_NAME',
|
||||||
SLOGAN: 'VITE_APP_SLOGAN',
|
SLOGAN: 'VITE_APP_SLOGAN',
|
||||||
REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL',
|
REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL',
|
||||||
HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL',
|
HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const VITE_ENVIRONMENT_KEYS = {
|
||||||
|
...VITE_USER_DEFINED_ENVIRONMENT_KEYS,
|
||||||
|
DEV: 'DEV',
|
||||||
|
} as const;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IAppMetadata } from '../IAppMetadata';
|
import { IEnvironmentVariables } from '../IEnvironmentVariables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the application's metadata using Vite's environment variables.
|
* Provides the application's environment variables.
|
||||||
*/
|
*/
|
||||||
export class ViteAppMetadata implements IAppMetadata {
|
export class ViteEnvironmentVariables implements IEnvironmentVariables {
|
||||||
// Ensure the use of import.meta.env prefix for the following properties.
|
// Ensure the use of import.meta.env prefix for the following properties.
|
||||||
// Vue will replace these statically during production builds.
|
// Vue will replace these statically during production builds.
|
||||||
|
|
||||||
@@ -26,4 +26,8 @@ export class ViteAppMetadata implements IAppMetadata {
|
|||||||
public get homepageUrl(): string {
|
public get homepageUrl(): string {
|
||||||
return import.meta.env.VITE_APP_HOMEPAGE_URL;
|
return import.meta.env.VITE_APP_HOMEPAGE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isNonProduction(): boolean {
|
||||||
|
return import.meta.env.DEV;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
import { IEventSubscription } from './IEventSource';
|
|
||||||
import { IEventSubscriptionCollection } from './IEventSubscriptionCollection';
|
import { IEventSubscriptionCollection } from './IEventSubscriptionCollection';
|
||||||
|
import { IEventSubscription } from './IEventSource';
|
||||||
|
|
||||||
export class EventSubscriptionCollection implements IEventSubscriptionCollection {
|
export class EventSubscriptionCollection implements IEventSubscriptionCollection {
|
||||||
private readonly subscriptions = new Array<IEventSubscription>();
|
private readonly subscriptions = new Array<IEventSubscription>();
|
||||||
|
|
||||||
public register(...subscriptions: IEventSubscription[]) {
|
public get subscriptionCount() {
|
||||||
|
return this.subscriptions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(subscriptions: IEventSubscription[]) {
|
||||||
|
if (!subscriptions || subscriptions.length === 0) {
|
||||||
|
throw new Error('missing subscriptions');
|
||||||
|
}
|
||||||
|
if (subscriptions.some((subscription) => !subscription)) {
|
||||||
|
throw new Error('missing subscription in list');
|
||||||
|
}
|
||||||
this.subscriptions.push(...subscriptions);
|
this.subscriptions.push(...subscriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,4 +22,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection
|
|||||||
this.subscriptions.forEach((listener) => listener.unsubscribe());
|
this.subscriptions.forEach((listener) => listener.unsubscribe());
|
||||||
this.subscriptions.splice(0, this.subscriptions.length);
|
this.subscriptions.splice(0, this.subscriptions.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public unsubscribeAllAndRegister(subscriptions: IEventSubscription[]) {
|
||||||
|
this.unsubscribeAll();
|
||||||
|
this.register(subscriptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
|
|
||||||
export interface IEventSubscriptionCollection {
|
export interface IEventSubscriptionCollection {
|
||||||
register(...subscriptions: IEventSubscription[]);
|
readonly subscriptionCount: number;
|
||||||
|
|
||||||
unsubscribeAll();
|
register(subscriptions: IEventSubscription[]): void;
|
||||||
|
unsubscribeAll(): void;
|
||||||
|
unsubscribeAllAndRegister(subscriptions: IEventSubscription[]);
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/infrastructure/Log/ConsoleLogger.ts
Normal file
13
src/infrastructure/Log/ConsoleLogger.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export class ConsoleLogger implements ILogger {
|
||||||
|
constructor(private readonly globalConsole: Partial<Console> = console) {
|
||||||
|
if (!globalConsole) {
|
||||||
|
throw new Error('missing console');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(...params: unknown[]): void {
|
||||||
|
this.globalConsole.info(...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/infrastructure/Log/ElectronLogger.ts
Normal file
12
src/infrastructure/Log/ElectronLogger.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ElectronLog } from 'electron-log';
|
||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
// Using plain-function rather than class so it can be used in Electron's context-bridging.
|
||||||
|
export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
|
||||||
|
if (!logger) {
|
||||||
|
throw new Error('missing logger');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
info: (...params) => logger.info(...params),
|
||||||
|
};
|
||||||
|
}
|
||||||
3
src/infrastructure/Log/ILogger.ts
Normal file
3
src/infrastructure/Log/ILogger.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ILogger {
|
||||||
|
info (...params: unknown[]): void;
|
||||||
|
}
|
||||||
5
src/infrastructure/Log/ILoggerFactory.ts
Normal file
5
src/infrastructure/Log/ILoggerFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export interface ILoggerFactory {
|
||||||
|
readonly logger: ILogger;
|
||||||
|
}
|
||||||
5
src/infrastructure/Log/NoopLogger.ts
Normal file
5
src/infrastructure/Log/NoopLogger.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export class NoopLogger implements ILogger {
|
||||||
|
public info(): void { /* NOOP */ }
|
||||||
|
}
|
||||||
20
src/infrastructure/Log/WindowInjectedLogger.ts
Normal file
20
src/infrastructure/Log/WindowInjectedLogger.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export class WindowInjectedLogger implements ILogger {
|
||||||
|
private readonly logger: ILogger;
|
||||||
|
|
||||||
|
constructor(windowVariables: WindowVariables = window) {
|
||||||
|
if (!windowVariables) {
|
||||||
|
throw new Error('missing window');
|
||||||
|
}
|
||||||
|
if (!windowVariables.log) {
|
||||||
|
throw new Error('missing log');
|
||||||
|
}
|
||||||
|
this.logger = windowVariables.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(...params: unknown[]): void {
|
||||||
|
this.logger.info(...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { IAppMetadata } from './IAppMetadata';
|
|
||||||
import { IAppMetadataFactory } from './IAppMetadataFactory';
|
|
||||||
import { validateMetadata } from './MetadataValidator';
|
|
||||||
import { ViteAppMetadata } from './Vite/ViteAppMetadata';
|
|
||||||
|
|
||||||
export class AppMetadataFactory implements IAppMetadataFactory {
|
|
||||||
public static readonly Current = new AppMetadataFactory();
|
|
||||||
|
|
||||||
public readonly instance: IAppMetadata;
|
|
||||||
|
|
||||||
protected constructor(validator: MetadataValidator = validateMetadata) {
|
|
||||||
const metadata = new ViteAppMetadata();
|
|
||||||
validator(metadata);
|
|
||||||
this.instance = metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MetadataValidator = typeof validateMetadata;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { IAppMetadata } from './IAppMetadata';
|
|
||||||
|
|
||||||
export interface IAppMetadataFactory {
|
|
||||||
readonly instance: IAppMetadata;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export interface IRuntimeEnvironment {
|
||||||
|
readonly isDesktop: boolean;
|
||||||
|
readonly os: OperatingSystem | undefined;
|
||||||
|
readonly isNonProduction: boolean;
|
||||||
|
}
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
|
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||||
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
import { IEnvironment } from './IEnvironment';
|
import { IRuntimeEnvironment } from './IRuntimeEnvironment';
|
||||||
import { WindowVariables } from './WindowVariables';
|
|
||||||
import { validateWindowVariables } from './WindowVariablesValidator';
|
|
||||||
|
|
||||||
export class Environment implements IEnvironment {
|
export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||||
public static readonly CurrentEnvironment: IEnvironment = new Environment(window);
|
public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window);
|
||||||
|
|
||||||
public readonly isDesktop: boolean;
|
public readonly isDesktop: boolean;
|
||||||
|
|
||||||
public readonly os: OperatingSystem | undefined;
|
public readonly os: OperatingSystem | undefined;
|
||||||
|
|
||||||
public readonly system: ISystemOperations | undefined;
|
public readonly isNonProduction: boolean;
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
window: Partial<Window>,
|
window: Partial<Window>,
|
||||||
|
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||||
windowValidator: WindowValidator = validateWindowVariables,
|
|
||||||
) {
|
) {
|
||||||
if (!window) {
|
if (!window) {
|
||||||
throw new Error('missing window');
|
throw new Error('missing window');
|
||||||
}
|
}
|
||||||
windowValidator(window);
|
this.isNonProduction = environmentVariables.isNonProduction;
|
||||||
this.isDesktop = isDesktop(window);
|
this.isDesktop = isDesktop(window);
|
||||||
if (this.isDesktop) {
|
if (this.isDesktop) {
|
||||||
this.os = window?.os;
|
this.os = window?.os;
|
||||||
@@ -34,7 +34,6 @@ export class Environment implements IEnvironment {
|
|||||||
this.os = browserOsDetector.detect(userAgent);
|
this.os = browserOsDetector.detect(userAgent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.system = window?.system;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,5 +44,3 @@ function getUserAgent(window: Partial<Window>): string {
|
|||||||
function isDesktop(window: Partial<WindowVariables>): boolean {
|
function isDesktop(window: Partial<WindowVariables>): boolean {
|
||||||
return window?.isDesktop === true;
|
return window?.isDesktop === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WindowValidator = typeof validateWindowVariables;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface ISanityCheckOptions {
|
export interface ISanityCheckOptions {
|
||||||
readonly validateMetadata: boolean;
|
readonly validateEnvironmentVariables: boolean;
|
||||||
readonly validateEnvironment: boolean;
|
readonly validateWindowVariables: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
||||||
import { ISanityValidator } from './Common/ISanityValidator';
|
import { ISanityValidator } from './Common/ISanityValidator';
|
||||||
import { MetadataValidator } from './Validators/MetadataValidator';
|
import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
|
||||||
|
|
||||||
const DefaultSanityValidators: ISanityValidator[] = [
|
const DefaultSanityValidators: ISanityValidator[] = [
|
||||||
new MetadataValidator(),
|
new EnvironmentVariablesValidator(),
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Helps to fail-fast on errors */
|
/* Helps to fail-fast on errors */
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
|
||||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
|
||||||
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
|
||||||
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
|
||||||
|
|
||||||
export class EnvironmentValidator extends FactoryValidator<IEnvironment> {
|
|
||||||
constructor(factory: FactoryFunction<IEnvironment> = () => Environment.CurrentEnvironment) {
|
|
||||||
super(factory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override name = 'environment';
|
|
||||||
|
|
||||||
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
|
||||||
return options.validateEnvironment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||||
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
|
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||||
|
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
||||||
|
|
||||||
|
export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironmentVariables> {
|
||||||
|
constructor(
|
||||||
|
factory: FactoryFunction<IEnvironmentVariables> = () => {
|
||||||
|
return EnvironmentVariablesFactory.Current.instance;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
super(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override name = 'environment variables';
|
||||||
|
|
||||||
|
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||||
|
return options.validateEnvironmentVariables;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
|
||||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
|
||||||
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
|
||||||
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
|
||||||
|
|
||||||
export class MetadataValidator extends FactoryValidator<IAppMetadata> {
|
|
||||||
constructor(factory: FactoryFunction<IAppMetadata> = () => AppMetadataFactory.Current.instance) {
|
|
||||||
super(factory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override name = 'metadata';
|
|
||||||
|
|
||||||
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
|
||||||
return options.validateMetadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
|
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||||
|
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
||||||
|
|
||||||
|
export class WindowVariablesValidator extends FactoryValidator<WindowVariables> {
|
||||||
|
constructor(factory: FactoryFunction<WindowVariables> = () => window) {
|
||||||
|
super(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override name = 'window variables';
|
||||||
|
|
||||||
|
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||||
|
return options.validateWindowVariables;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||||
|
import { ISystemOperations } from './ISystemOperations';
|
||||||
|
|
||||||
|
export function getWindowInjectedSystemOperations(
|
||||||
|
windowVariables: Partial<WindowVariables> = window,
|
||||||
|
): ISystemOperations {
|
||||||
|
if (!windowVariables) {
|
||||||
|
throw new Error('missing window');
|
||||||
|
}
|
||||||
|
if (!windowVariables.system) {
|
||||||
|
throw new Error('missing system');
|
||||||
|
}
|
||||||
|
return windowVariables.system;
|
||||||
|
}
|
||||||
11
src/infrastructure/WindowVariables/WindowVariables.ts
Normal file
11
src/infrastructure/WindowVariables/WindowVariables.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||||
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
|
|
||||||
|
/* Primary entry point for platform-specific injections */
|
||||||
|
export interface WindowVariables {
|
||||||
|
readonly system: ISystemOperations;
|
||||||
|
readonly isDesktop: boolean;
|
||||||
|
readonly os: OperatingSystem;
|
||||||
|
readonly log: ILogger;
|
||||||
|
}
|
||||||
@@ -6,11 +6,8 @@ import { WindowVariables } from './WindowVariables';
|
|||||||
* Checks for consistency in runtime environment properties injected by Electron preloader.
|
* Checks for consistency in runtime environment properties injected by Electron preloader.
|
||||||
*/
|
*/
|
||||||
export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||||
if (!variables) {
|
|
||||||
throw new Error('missing variables');
|
|
||||||
}
|
|
||||||
if (!isObject(variables)) {
|
if (!isObject(variables)) {
|
||||||
throw new Error(`window is not an object but ${typeof variables}`);
|
throw new Error('window is not an object');
|
||||||
}
|
}
|
||||||
const errors = [...testEveryProperty(variables)];
|
const errors = [...testEveryProperty(variables)];
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
@@ -25,6 +22,7 @@ function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<strin
|
|||||||
os: testOperatingSystem(variables.os),
|
os: testOperatingSystem(variables.os),
|
||||||
isDesktop: testIsDesktop(variables.isDesktop),
|
isDesktop: testIsDesktop(variables.isDesktop),
|
||||||
system: testSystem(variables),
|
system: testSystem(variables),
|
||||||
|
log: testLogger(variables),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [propertyName, testResult] of Object.entries(tests)) {
|
for (const [propertyName, testResult] of Object.entries(tests)) {
|
||||||
@@ -47,11 +45,18 @@ function testOperatingSystem(os: unknown): boolean {
|
|||||||
.includes(os);
|
.includes(os);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testLogger(variables: Partial<WindowVariables>): boolean {
|
||||||
|
if (!variables.isDesktop) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isObject(variables.log);
|
||||||
|
}
|
||||||
|
|
||||||
function testSystem(variables: Partial<WindowVariables>): boolean {
|
function testSystem(variables: Partial<WindowVariables>): boolean {
|
||||||
if (!variables.isDesktop) {
|
if (!variables.isDesktop) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return variables.system !== undefined && isObject(variables.system);
|
return isObject(variables.system);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testIsDesktop(isDesktop: unknown): boolean {
|
function testIsDesktop(isDesktop: unknown): boolean {
|
||||||
@@ -70,7 +75,7 @@ function isBoolean(variable: unknown): variable is boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isObject(variable: unknown): variable is object {
|
function isObject(variable: unknown): variable is object {
|
||||||
return typeof variable === 'object'
|
return Boolean(variable) // the data type of null is an object
|
||||||
&& variable !== null // the data type of null is an object
|
&& typeof variable === 'object'
|
||||||
&& !Array.isArray(variable);
|
&& !Array.isArray(variable);
|
||||||
}
|
}
|
||||||
6
src/infrastructure/WindowVariables/window-variables.d.ts
vendored
Normal file
6
src/infrastructure/WindowVariables/window-variables.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { WindowVariables } from './WindowVariables';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface Window extends WindowVariables { }
|
||||||
|
}
|
||||||
@@ -44,4 +44,10 @@
|
|||||||
transform: translateY($offset-upward);
|
transform: translateY($offset-upward);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin reset-ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,4 +9,3 @@
|
|||||||
@forward "./components/card";
|
@forward "./components/card";
|
||||||
|
|
||||||
@forward "./third-party-extensions/tooltip.scss";
|
@forward "./third-party-extensions/tooltip.scss";
|
||||||
@forward "./third-party-extensions/tree.scss";
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// Overrides base styling for LiquorTree
|
|
||||||
@use "@/presentation/assets/styles/colors" as *;
|
|
||||||
@use "@/presentation/assets/styles/mixins" as *;
|
|
||||||
|
|
||||||
$color-tree-bg : $color-primary-darker;
|
|
||||||
$color-node-arrow : $color-on-primary;
|
|
||||||
$color-node-fg : $color-on-primary;
|
|
||||||
$color-node-hover-bg : $color-primary-dark;
|
|
||||||
$color-node-keyboard-bg : $color-surface;
|
|
||||||
$color-node-keyboard-fg : $color-on-surface;
|
|
||||||
$color-node-checkbox-bg-checked : $color-secondary;
|
|
||||||
$color-node-checkbox-bg-unchecked : $color-primary-darkest;
|
|
||||||
$color-node-checkbox-border-checked : $color-secondary;
|
|
||||||
$color-node-checkbox-border-unchecked : $color-on-primary;
|
|
||||||
$color-node-checkbox-tick-checked : $color-on-secondary;
|
|
||||||
|
|
||||||
.tree {
|
|
||||||
background: $color-tree-bg;
|
|
||||||
&-node {
|
|
||||||
white-space: normal !important;
|
|
||||||
> .tree-content {
|
|
||||||
> .tree-anchor {
|
|
||||||
> span {
|
|
||||||
color: $color-node-fg;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
display: block; // so it takes full width to allow aligning items inside
|
|
||||||
}
|
|
||||||
@include hover-or-touch {
|
|
||||||
background: $color-node-hover-bg !important;
|
|
||||||
}
|
|
||||||
background: $color-tree-bg !important; // If not styled, it gets white background on mobile.
|
|
||||||
}
|
|
||||||
&.selected { // When using keyboard navigation it highlights current item and its child items
|
|
||||||
background: $color-node-keyboard-bg;
|
|
||||||
.tree-text {
|
|
||||||
color: $color-node-keyboard-fg !important; // $block
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-checkbox {
|
|
||||||
border-color: $color-node-checkbox-border-unchecked !important;
|
|
||||||
&.checked {
|
|
||||||
background: $color-node-checkbox-bg-checked !important;
|
|
||||||
border-color: $color-node-checkbox-border-checked !important;
|
|
||||||
&:after {
|
|
||||||
border-color: $color-node-checkbox-tick-checked !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.indeterminate {
|
|
||||||
border-color: $color-node-checkbox-border-unchecked !important;
|
|
||||||
}
|
|
||||||
background: $color-node-checkbox-bg-unchecked !important;
|
|
||||||
}
|
|
||||||
&-arrow {
|
|
||||||
&.has-child {
|
|
||||||
&.rtl:after, &:after {
|
|
||||||
border-color: $color-node-arrow !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { TreeBootstrapper } from './Modules/TreeBootstrapper';
|
|
||||||
import { IconBootstrapper } from './Modules/IconBootstrapper';
|
import { IconBootstrapper } from './Modules/IconBootstrapper';
|
||||||
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
||||||
import { VueBootstrapper } from './Modules/VueBootstrapper';
|
import { VueBootstrapper } from './Modules/VueBootstrapper';
|
||||||
import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
|
import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
|
||||||
|
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator';
|
||||||
|
import { AppInitializationLogger } from './Modules/AppInitializationLogger';
|
||||||
|
|
||||||
export class ApplicationBootstrapper implements IVueBootstrapper {
|
export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||||
public bootstrap(vue: VueConstructor): void {
|
public bootstrap(vue: VueConstructor): void {
|
||||||
@@ -15,9 +16,10 @@ export class ApplicationBootstrapper implements IVueBootstrapper {
|
|||||||
private static getAllBootstrappers(): IVueBootstrapper[] {
|
private static getAllBootstrappers(): IVueBootstrapper[] {
|
||||||
return [
|
return [
|
||||||
new IconBootstrapper(),
|
new IconBootstrapper(),
|
||||||
new TreeBootstrapper(),
|
|
||||||
new VueBootstrapper(),
|
new VueBootstrapper(),
|
||||||
new TooltipBootstrapper(),
|
new TooltipBootstrapper(),
|
||||||
|
new RuntimeSanityValidator(),
|
||||||
|
new AppInitializationLogger(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/presentation/bootstrapping/ClientLoggerFactory.ts
Normal file
25
src/presentation/bootstrapping/ClientLoggerFactory.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
|
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
||||||
|
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
||||||
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
|
import { ILoggerFactory } from '@/infrastructure/Log/ILoggerFactory';
|
||||||
|
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||||
|
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
||||||
|
|
||||||
|
export class ClientLoggerFactory implements ILoggerFactory {
|
||||||
|
public static readonly Current: ILoggerFactory = new ClientLoggerFactory();
|
||||||
|
|
||||||
|
public readonly logger: ILogger;
|
||||||
|
|
||||||
|
protected constructor(environment: IRuntimeEnvironment = RuntimeEnvironment.CurrentEnvironment) {
|
||||||
|
if (environment.isDesktop) {
|
||||||
|
this.logger = new WindowInjectedLogger();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (environment.isNonProduction) {
|
||||||
|
this.logger = new ConsoleLogger();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger = new NoopLogger();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,31 @@
|
|||||||
import { InjectionKey, provide } from 'vue';
|
import { InjectionKey, provide, inject } from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
import {
|
import { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||||
useCollectionStateKey, useApplicationKey, useEnvironmentKey,
|
|
||||||
} from '@/presentation/injectionSymbols';
|
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
|
||||||
export function provideDependencies(context: IApplicationContext) {
|
export function provideDependencies(
|
||||||
registerSingleton(useApplicationKey, useApplication(context.app));
|
context: IApplicationContext,
|
||||||
registerTransient(useCollectionStateKey, () => useCollectionState(context));
|
api: VueDependencyInjectionApi = { provide, inject },
|
||||||
registerSingleton(useEnvironmentKey, Environment.CurrentEnvironment);
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerSingleton<T>(
|
|
||||||
key: InjectionKey<T>,
|
|
||||||
value: T,
|
|
||||||
) {
|
) {
|
||||||
provide(key, value);
|
const registerSingleton = <T>(key: InjectionKey<T>, value: T) => api.provide(key, value);
|
||||||
|
const registerTransient = <T>(
|
||||||
|
key: InjectionKey<() => T>,
|
||||||
|
factory: () => T,
|
||||||
|
) => api.provide(key, factory);
|
||||||
|
|
||||||
|
registerSingleton(InjectionKeys.useApplication, useApplication(context.app));
|
||||||
|
registerSingleton(InjectionKeys.useRuntimeEnvironment, RuntimeEnvironment.CurrentEnvironment);
|
||||||
|
registerTransient(InjectionKeys.useAutoUnsubscribedEvents, () => useAutoUnsubscribedEvents());
|
||||||
|
registerTransient(InjectionKeys.useCollectionState, () => {
|
||||||
|
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
return useCollectionState(context, events);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerTransient<T>(
|
export interface VueDependencyInjectionApi {
|
||||||
key: InjectionKey<() => T>,
|
provide<T>(key: InjectionKey<T>, value: T): void;
|
||||||
factory: () => T,
|
inject<T>(key: InjectionKey<T>): T;
|
||||||
) {
|
|
||||||
provide(key, factory);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
|
import { IVueBootstrapper } from '../IVueBootstrapper';
|
||||||
|
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
||||||
|
|
||||||
|
export class AppInitializationLogger implements IVueBootstrapper {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ILogger = ClientLoggerFactory.Current.logger,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public bootstrap(): void {
|
||||||
|
// Do not remove [APP_INIT]; it's a marker used in tests.
|
||||||
|
this.logger.info('[APP_INIT] Application is initialized.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
|
import { IVueBootstrapper } from '../IVueBootstrapper';
|
||||||
|
|
||||||
|
export class RuntimeSanityValidator implements IVueBootstrapper {
|
||||||
|
constructor(private readonly validator = validateRuntimeSanity) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public bootstrap(): void {
|
||||||
|
this.validator({
|
||||||
|
validateEnvironmentVariables: true,
|
||||||
|
validateWindowVariables: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import LiquorTree from 'liquor-tree';
|
|
||||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
|
||||||
|
|
||||||
export class TreeBootstrapper implements IVueBootstrapper {
|
|
||||||
public bootstrap(vue: VueConstructor): void {
|
|
||||||
vue.use(LiquorTree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@ import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeBu
|
|||||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
|
||||||
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
||||||
|
|
||||||
const singletonAppContext = await buildContext();
|
const singletonAppContext = await buildContext();
|
||||||
@@ -33,10 +32,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
||||||
validateRuntimeSanity({
|
|
||||||
validateMetadata: true,
|
|
||||||
validateEnvironment: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import {
|
|||||||
defineComponent, PropType, computed,
|
defineComponent, PropType, computed,
|
||||||
inject,
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useApplicationKey } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import CodeInstruction from './CodeInstruction.vue';
|
import CodeInstruction from './CodeInstruction.vue';
|
||||||
@@ -77,7 +77,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { info } = inject(useApplicationKey);
|
const { info } = inject(InjectionKeys.useApplication);
|
||||||
|
|
||||||
const appName = computed<string>(() => info.name);
|
const appName = computed<string>(() => info.name);
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,10 @@
|
|||||||
import {
|
import {
|
||||||
defineComponent, ref, computed, inject,
|
defineComponent, ref, computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionStateKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
@@ -54,12 +53,13 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const {
|
const {
|
||||||
currentState, currentContext, onStateChange, events,
|
currentState, currentContext, onStateChange,
|
||||||
} = inject(useCollectionStateKey)();
|
} = inject(InjectionKeys.useCollectionState)();
|
||||||
const { isDesktop } = inject(useEnvironmentKey);
|
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||||
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const areInstructionsVisible = ref(false);
|
const areInstructionsVisible = ref(false);
|
||||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop));
|
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
||||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||||
const hasCode = ref(false);
|
const hasCode = ref(false);
|
||||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||||
@@ -82,15 +82,18 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onStateChange((newState) => {
|
onStateChange((newState) => {
|
||||||
|
updateCurrentCode(newState.code.current);
|
||||||
subscribeToCodeChanges(newState.code);
|
subscribeToCodeChanges(newState.code);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
function subscribeToCodeChanges(code: IApplicationCode) {
|
function subscribeToCodeChanges(code: IApplicationCode) {
|
||||||
hasCode.value = code.current && code.current.length > 0;
|
events.unsubscribeAllAndRegister([
|
||||||
events.unsubscribeAll();
|
code.changed.on((newCode) => updateCurrentCode(newCode.code)),
|
||||||
events.register(code.changed.on((newCode) => {
|
]);
|
||||||
hasCode.value = newCode && newCode.code.length > 0;
|
}
|
||||||
}));
|
|
||||||
|
function updateCurrentCode(code: string) {
|
||||||
|
hasCode.value = code && code.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCurrentCode(): Promise<IApplicationCode> {
|
async function getCurrentCode(): Promise<IApplicationCode> {
|
||||||
@@ -122,8 +125,12 @@ function getDownloadInstructions(
|
|||||||
return getInstructions(os, fileName);
|
return getInstructions(os, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanRunState(selectedOs: OperatingSystem, isDesktopVersion: boolean): boolean {
|
function getCanRunState(
|
||||||
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
|
selectedOs: OperatingSystem,
|
||||||
|
isDesktopVersion: boolean,
|
||||||
|
hostOs: OperatingSystem,
|
||||||
|
): boolean {
|
||||||
|
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||||
return isDesktopVersion && isRunningOnSelectedOs;
|
return isDesktopVersion && isRunningOnSelectedOs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import {
|
import {
|
||||||
defineComponent, onUnmounted, onMounted, inject,
|
defineComponent, onUnmounted, onMounted, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
@@ -37,7 +37,8 @@ export default defineComponent({
|
|||||||
NonCollapsing,
|
NonCollapsing,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { onStateChange, currentState, events } = inject(useCollectionStateKey)();
|
const { onStateChange, currentState } = inject(InjectionKeys.useCollectionState)();
|
||||||
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const editorId = 'codeEditor';
|
const editorId = 'codeEditor';
|
||||||
let editor: ace.Ace.Editor | undefined;
|
let editor: ace.Ace.Editor | undefined;
|
||||||
@@ -61,20 +62,20 @@ export default defineComponent({
|
|||||||
newState.collection.scripting.language,
|
newState.collection.scripting.language,
|
||||||
);
|
);
|
||||||
const appCode = newState.code;
|
const appCode = newState.code;
|
||||||
const innerCode = appCode.current || getDefaultCode(newState.collection.scripting.language);
|
updateCode(appCode.current, newState.collection.scripting.language);
|
||||||
editor.setValue(innerCode, 1);
|
events.unsubscribeAllAndRegister([
|
||||||
events.unsubscribeAll();
|
appCode.changed.on((code) => handleCodeChange(code)),
|
||||||
events.register(appCode.changed.on((code) => updateCode(code)));
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCode(event: ICodeChangedEvent) {
|
function updateCode(code: string, language: ScriptingLanguage) {
|
||||||
|
const innerCode = code || getDefaultCode(language);
|
||||||
|
editor.setValue(innerCode, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCodeChange(event: ICodeChangedEvent) {
|
||||||
removeCurrentHighlighting();
|
removeCurrentHighlighting();
|
||||||
if (event.isEmpty()) {
|
updateCode(event.code, currentState.value.collection.scripting.language);
|
||||||
const defaultCode = getDefaultCode(currentState.value.collection.scripting.language);
|
|
||||||
editor.setValue(defaultCode, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editor.setValue(event.code, 1);
|
|
||||||
if (event.addedScripts?.length > 0) {
|
if (event.addedScripts?.length > 0) {
|
||||||
reactToChanges(event, event.addedScripts);
|
reactToChanges(event, event.addedScripts);
|
||||||
} else if (event.changedScripts?.length > 0) {
|
} else if (event.changedScripts?.length > 0) {
|
||||||
|
|||||||
@@ -66,9 +66,10 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, inject } from 'vue';
|
import { defineComponent, ref, inject } from 'vue';
|
||||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
import MenuOptionList from '../MenuOptionList.vue';
|
import MenuOptionList from '../MenuOptionList.vue';
|
||||||
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
||||||
import { SelectionType, SelectionTypeHandler } from './SelectionTypeHandler';
|
import { SelectionType, SelectionTypeHandler } from './SelectionTypeHandler';
|
||||||
@@ -80,28 +81,27 @@ export default defineComponent({
|
|||||||
TooltipWrapper,
|
TooltipWrapper,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { modifyCurrentState, onStateChange, events } = inject(useCollectionStateKey)();
|
const { modifyCurrentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||||
|
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
|
||||||
const currentSelection = ref(SelectionType.None);
|
const currentSelection = ref(SelectionType.None);
|
||||||
|
|
||||||
let selectionTypeHandler: SelectionTypeHandler;
|
let selectionTypeHandler: SelectionTypeHandler;
|
||||||
|
|
||||||
onStateChange(() => {
|
onStateChange(() => {
|
||||||
unregisterMutators();
|
|
||||||
|
|
||||||
modifyCurrentState((state) => {
|
modifyCurrentState((state) => {
|
||||||
registerStateMutator(state);
|
selectionTypeHandler = new SelectionTypeHandler(state);
|
||||||
|
updateSelections();
|
||||||
|
events.unsubscribeAllAndRegister([
|
||||||
|
subscribeAndUpdateSelections(state),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
function unregisterMutators() {
|
function subscribeAndUpdateSelections(
|
||||||
events.unsubscribeAll();
|
state: ICategoryCollectionState,
|
||||||
}
|
): IEventSubscription {
|
||||||
|
return state.selection.changed.on(() => updateSelections());
|
||||||
function registerStateMutator(state: ICategoryCollectionState) {
|
|
||||||
selectionTypeHandler = new SelectionTypeHandler(state);
|
|
||||||
updateSelections();
|
|
||||||
events.register(state.selection.changed.on(() => updateSelections()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectType(type: SelectionType) {
|
function selectType(type: SelectionType) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import {
|
import {
|
||||||
defineComponent, computed, inject,
|
defineComponent, computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useApplicationKey, useCollectionStateKey } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import MenuOptionList from './MenuOptionList.vue';
|
import MenuOptionList from './MenuOptionList.vue';
|
||||||
import MenuOptionListItem from './MenuOptionListItem.vue';
|
import MenuOptionListItem from './MenuOptionListItem.vue';
|
||||||
@@ -30,8 +30,8 @@ export default defineComponent({
|
|||||||
MenuOptionListItem,
|
MenuOptionListItem,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { modifyCurrentContext, currentState } = inject(useCollectionStateKey)();
|
const { modifyCurrentContext, currentState } = inject(InjectionKeys.useCollectionState)();
|
||||||
const { application } = inject(useApplicationKey);
|
const { application } = inject(InjectionKeys.useApplication);
|
||||||
|
|
||||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
||||||
application.getSupportedOsList() ?? [])
|
application.getSupportedOsList() ?? [])
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user