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
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Build
|
||||
name: Build web
|
||||
run: npm run build -- --mode ${{ matrix.mode }}
|
||||
-
|
||||
name: Verify web build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --web
|
||||
|
||||
build-desktop:
|
||||
strategy:
|
||||
@@ -52,30 +55,16 @@ jobs:
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
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 }}
|
||||
-
|
||||
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
|
||||
|
||||
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
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
-
|
||||
name: Create icons
|
||||
run: npm run icons:build
|
||||
name: Verify bundled desktop build artifacts
|
||||
run: npm run check:verify-build-artifacts -- --electron-bundled
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-desktop:
|
||||
run-check:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
@@ -19,6 +19,9 @@ jobs:
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Configure Ubuntu
|
||||
if: matrix.os == 'ubuntu'
|
||||
@@ -57,7 +60,9 @@ jobs:
|
||||
-
|
||||
name: Test
|
||||
shell: bash
|
||||
run: node ./scripts/check-desktop-runtime-errors --screenshot
|
||||
run: |-
|
||||
export SCREENSHOT=true
|
||||
npm run check:desktop
|
||||
-
|
||||
name: Upload screenshot
|
||||
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 ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
- name: Checkout
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
-
|
||||
name: Install dependencies
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Lint
|
||||
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
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run unit tests
|
||||
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
|
||||
-
|
||||
name: "App: Install dependencies"
|
||||
run: npm ci
|
||||
working-directory: app
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
with:
|
||||
working-directory: app
|
||||
-
|
||||
name: "App: Run unit tests"
|
||||
run: npm run test:unit
|
||||
@@ -94,11 +95,21 @@ jobs:
|
||||
name: "App: Build"
|
||||
run: npm run build
|
||||
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"
|
||||
shell: bash
|
||||
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" \
|
||||
--folder app/dist \
|
||||
--folder "${web_output_dir}" \
|
||||
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
|
||||
--storage-class ONEZONE_IA \
|
||||
--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
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run e2e tests
|
||||
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
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run integration tests
|
||||
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
|
||||
-
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,5 @@
|
||||
node_modules
|
||||
dist/
|
||||
/dist-*/
|
||||
.vs
|
||||
.vscode/**/*
|
||||
!.vscode/extensions.json
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
!.vscode/extensions.json
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# 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)
|
||||
|
||||
* 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
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
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
|
||||
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
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
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.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
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
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
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:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
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
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
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.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
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
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
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
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
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.
|
||||
|
||||
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
|
||||
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>
|
||||
|
||||
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
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
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".
|
||||
If your software can interact with users remotely through a computer
|
||||
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
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
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.
|
||||
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/>.
|
||||
|
||||
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"
|
||||
/>
|
||||
</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 -->
|
||||
<br />
|
||||
<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)
|
||||
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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Install node >15.x.
|
||||
- Install dependencies using `npm install`.
|
||||
- Install Node >16.x.
|
||||
- Install dependencies using `npm install` (or [`npm run install-deps`](#utility-scripts) for more options).
|
||||
|
||||
### 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:
|
||||
- `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.
|
||||
- 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).
|
||||
|
||||
@@ -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`
|
||||
- (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)
|
||||
- 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)
|
||||
📖 For detailed options and behavior for any of the following scripts, please refer to the script file itself.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### 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
|
||||
|
||||
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.
|
||||
- [**`components/`**](./../src/presentation/components/): Contains 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.
|
||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||
- [**`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.
|
||||
- [**`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.
|
||||
@@ -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.
|
||||
|
||||
## 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.:
|
||||
- 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 { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
||||
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 PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||
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({
|
||||
main: getSharedElectronConfig({
|
||||
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
@@ -19,8 +19,6 @@
|
||||
"cross-fetch": "^4.0.0",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"install": "^0.13.0",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"markdown-it": "^13.0.1",
|
||||
"npm": "^9.8.1",
|
||||
"v-tooltip": "2.1.3",
|
||||
@@ -9425,14 +9423,6 @@
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
|
||||
@@ -10771,11 +10761,6 @@
|
||||
"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": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
|
||||
@@ -28223,11 +28208,6 @@
|
||||
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
|
||||
@@ -29256,11 +29236,6 @@
|
||||
"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": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
|
||||
|
||||
13
package.json
13
package.json
@@ -1,27 +1,31 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"private": true,
|
||||
"slogan": "Now you have the choice",
|
||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||
"author": "undergroundwires",
|
||||
"type": "module",
|
||||
"main": "./dist-electron-unbundled/main/index.cjs",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run --dir tests/unit",
|
||||
"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: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",
|
||||
"install-deps": "node scripts/npm-install.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:preview": "electron-vite preview",
|
||||
"electron:prebuild": "electron-vite build",
|
||||
"electron:build": "electron-builder",
|
||||
"lint:eslint": "eslint .",
|
||||
"lint:eslint": "eslint . --ignore-path .gitignore",
|
||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||
@@ -29,7 +33,6 @@
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"main": "./dist_electron/main/index.cjs",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
@@ -41,8 +44,6 @@
|
||||
"cross-fetch": "^4.0.0",
|
||||
"electron-progressbar": "^2.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"install": "^0.13.0",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"markdown-it": "^13.0.1",
|
||||
"npm": "^9.8.1",
|
||||
"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> = {
|
||||
prototype: T;
|
||||
apply: (this: unknown, args: TArgs) => void;
|
||||
readonly name: string;
|
||||
};
|
||||
|
||||
export type PropertyKeys<T> = {
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
|
||||
export async function buildContext(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = Environment.CurrentEnvironment,
|
||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||
): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('missing factory'); }
|
||||
if (!environment) { throw new Error('missing environment'); }
|
||||
const app = await factory.getApp();
|
||||
const os = getInitialOs(app, environment);
|
||||
const os = getInitialOs(app, environment.os);
|
||||
return new ApplicationContext(app, os);
|
||||
}
|
||||
|
||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||
const currentOs = environment.os;
|
||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
||||
const supportedOsList = app.getSupportedOsList();
|
||||
if (supportedOsList.includes(currentOs)) {
|
||||
return currentOs;
|
||||
|
||||
@@ -7,14 +7,14 @@ import MacOsData from '@/application/collections/macos.yaml';
|
||||
import LinuxData from '@/application/collections/linux.yaml';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
categoryParser = parseCategoryCollection,
|
||||
informationParser = parseProjectInformation,
|
||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||
collectionsData = PreParsedCollections,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { ConstructorArguments } from '@/TypeHelpers';
|
||||
|
||||
export function
|
||||
parseProjectInformation(
|
||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||
createProjectInformation: ProjectInformationFactory = (
|
||||
...args
|
||||
) => new ProjectInformation(...args),
|
||||
|
||||
@@ -3558,82 +3558,98 @@ functions:
|
||||
parameters:
|
||||
- name: prefName
|
||||
- name: jsonValue
|
||||
# prefs.js file (https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file) exists at
|
||||
# - Default installation:
|
||||
# ~/.mozilla/firefox/<profile-name>/prefs.js
|
||||
# - Flatpak installation:
|
||||
# ~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/prefs.js
|
||||
# - Snap installation:
|
||||
# ~/snap/firefox/common/.mozilla/firefox/<profile-name>/prefs.js
|
||||
docs: |-
|
||||
This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
|
||||
|
||||
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
|
||||
|
||||
- Default: `~/.mozilla/firefox/<profile-name>/user.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: |-
|
||||
pref_name='{{ $prefName }}'
|
||||
pref_value='{{ $jsonValue }}'
|
||||
echo "Setting preference \"$pref_name\" to \"$pref_value\"."
|
||||
pref_file_paths=(
|
||||
~/.mozilla/firefox/*/prefs.js
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js
|
||||
~/snap/firefox/common/.mozilla/firefox/*/prefs.js
|
||||
declare -a profile_paths=(
|
||||
~/.mozilla/firefox/*/
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||
~/snap/firefox/common/.mozilla/firefox/*/
|
||||
)
|
||||
declare -i total_files_found=0
|
||||
for pref_file in "${pref_file_paths[@]}"; do
|
||||
if [ -f "$pref_file" ]; then
|
||||
((total_files_found++))
|
||||
echo "$pref_file:"
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${pref_file}"; then
|
||||
echo $'\t'"Preference is not configured before."
|
||||
echo -n $'\n'"$pref_line" >> "$pref_file"
|
||||
echo $'\t'"Successfully configured."
|
||||
else
|
||||
if grep --quiet "^$pref_line$" "${pref_file}"; then
|
||||
echo $'\t'"Skipping. Preference is already configured as expected."
|
||||
else
|
||||
sed --in-place "/^$pref_start/d" "$pref_file"
|
||||
echo $'\t'"Deleted assignment with unexpected value."
|
||||
echo -n $'\n'"$pref_line" >> "$pref_file"
|
||||
echo $'\t'"Successfully reconfigured with expected value."
|
||||
fi
|
||||
fi
|
||||
declare -i total_profiles_found=0
|
||||
for profile_dir in "${profile_paths[@]}"; do
|
||||
if [ ! -d "$profile_dir" ]; then
|
||||
continue
|
||||
fi
|
||||
if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
|
||||
continue # Not a profile folder
|
||||
fi
|
||||
((total_profiles_found++))
|
||||
user_js_file="${profile_dir}user.js"
|
||||
echo "$user_js_file:"
|
||||
if [ ! -f "$user_js_file" ]; then
|
||||
touch "$user_js_file"
|
||||
echo $'\t''Created new user.js file'
|
||||
fi
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
||||
echo -n $'\n'"$pref_line" >> "$user_js_file"
|
||||
echo $'\t'"Successfully added a new preference in $user_js_file."
|
||||
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
|
||||
done
|
||||
if [ "$total_files_found" -eq 0 ]; then
|
||||
echo "No changes, no preference file is found."
|
||||
if [ "$total_profiles_found" -eq 0 ]; then
|
||||
echo 'No profile folders are found, no changes are made.'
|
||||
else
|
||||
echo "Ensured that $total_files_found profiles are compilant."
|
||||
echo "Preferences verified in $total_profiles_found profiles."
|
||||
fi
|
||||
revertCode: |-
|
||||
pref_name='{{ $prefName }}'
|
||||
pref_value='{{ $jsonValue }}'
|
||||
echo "Restoring \"$pref_name\" to its default."
|
||||
pref_file_paths=(
|
||||
~/.mozilla/firefox/*/prefs.js
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/prefs.js
|
||||
~/snap/firefox/common/.mozilla/firefox/*/prefs.js
|
||||
echo "Reverting preference: \"$pref_name\" to its default."
|
||||
declare -a profile_paths=(
|
||||
~/.mozilla/firefox/*/
|
||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||
~/snap/firefox/common/.mozilla/firefox/*/
|
||||
)
|
||||
declare -i total_files_found=0
|
||||
for pref_file in "${pref_file_paths[@]}"; do
|
||||
if [ -f "$pref_file" ]; then
|
||||
((total_files_found++))
|
||||
echo "$pref_file:"
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${pref_file}"; then
|
||||
echo $'\t'"Skipping. Preference is not configured before."
|
||||
else
|
||||
if grep --quiet "^$pref_line$" "${pref_file}"; then
|
||||
sed --in-place "/^$pref_line/d" "$pref_file"
|
||||
echo $'\t'"Successfully restored preference value to its default."
|
||||
else
|
||||
echo $'\t'"Skipping, the preference has value that is not configured by privacy.sexy."
|
||||
fi
|
||||
declare -i total_profiles_found=0
|
||||
for profile_dir in "${profile_paths[@]}"; do
|
||||
user_js_file="${profile_dir}user.js"
|
||||
if [ ! -f "$user_js_file" ]; then
|
||||
continue
|
||||
fi
|
||||
((total_profiles_found++))
|
||||
echo "$user_js_file:"
|
||||
pref_start="user_pref(\"$pref_name\","
|
||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
||||
echo $'\t''Skipping, preference was not configured before.'
|
||||
elif grep --quiet "^$pref_line$" "${user_js_file}"; then
|
||||
sed --in-place "/^$pref_line/d" "$user_js_file"
|
||||
echo $'\t''Successfully reverted preference to default.'
|
||||
if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
|
||||
rm "$user_js_file"
|
||||
echo $'\t''Removed user.js file as it became empty.'
|
||||
fi
|
||||
else
|
||||
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
|
||||
fi
|
||||
done
|
||||
if [ "$total_files_found" -eq 0 ]; then
|
||||
echo "No changes, no preference file is found."
|
||||
if [ "$total_profiles_found" -eq 0 ]; then
|
||||
echo 'No reversion was necessary.'
|
||||
else
|
||||
echo "Ensured that $total_files_found profiles are compilant."
|
||||
echo "Preferences verified in $total_profiles_found profiles."
|
||||
fi
|
||||
-
|
||||
name: RenameFile
|
||||
|
||||
@@ -2456,7 +2456,7 @@ actions:
|
||||
recommend: standard
|
||||
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
|
||||
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
|
||||
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
|
||||
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:
|
||||
- https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.WindowsDefender::Scan_PurgeItemsAfterDelay
|
||||
# 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 { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations';
|
||||
|
||||
export class CodeRunner {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||
const { system } = this.environment;
|
||||
const dir = system.location.combinePaths(
|
||||
system.operatingSystem.getTempDirectory(),
|
||||
const { os } = this.environment;
|
||||
const dir = this.system.location.combinePaths(
|
||||
this.system.operatingSystem.getTempDirectory(),
|
||||
folderName,
|
||||
);
|
||||
await system.fileSystem.createDirectory(dir, true);
|
||||
const filePath = system.location.combinePaths(dir, `run.${fileExtension}`);
|
||||
await system.fileSystem.writeToFile(filePath, code);
|
||||
await system.fileSystem.setFilePermissions(filePath, '755');
|
||||
const command = getExecuteCommand(filePath, this.environment);
|
||||
system.command.execute(command);
|
||||
await this.system.fileSystem.createDirectory(dir, true);
|
||||
const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`);
|
||||
await this.system.fileSystem.writeToFile(filePath, code);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
const command = getExecuteCommand(filePath, os);
|
||||
this.system.command.execute(command);
|
||||
}
|
||||
}
|
||||
|
||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||
switch (environment.os) {
|
||||
function getExecuteCommand(
|
||||
scriptPath: string,
|
||||
currentOperatingSystem: OperatingSystem,
|
||||
): string {
|
||||
switch (currentOperatingSystem) {
|
||||
case OperatingSystem.Linux:
|
||||
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||
case OperatingSystem.macOS:
|
||||
@@ -37,6 +42,6 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
|
||||
case OperatingSystem.Windows:
|
||||
return scriptPath;
|
||||
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 */
|
||||
export function validateMetadata(metadata: IAppMetadata): void {
|
||||
if (!metadata) {
|
||||
throw new Error('missing metadata');
|
||||
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
||||
if (!environment) {
|
||||
throw new Error('missing environment');
|
||||
}
|
||||
const keyValues = capturePropertyValues(metadata);
|
||||
const keyValues = capturePropertyValues(environment);
|
||||
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) {
|
||||
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)
|
||||
.reduce((acc, [key, value]) => {
|
||||
if (!value) {
|
||||
if (!value && typeof value !== 'boolean') {
|
||||
acc.push(key);
|
||||
}
|
||||
return acc;
|
||||
@@ -1,8 +1,5 @@
|
||||
/**
|
||||
* 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 {
|
||||
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
|
||||
export const VITE_ENVIRONMENT_KEYS = {
|
||||
export const VITE_USER_DEFINED_ENVIRONMENT_KEYS = {
|
||||
VERSION: 'VITE_APP_VERSION',
|
||||
NAME: 'VITE_APP_NAME',
|
||||
SLOGAN: 'VITE_APP_SLOGAN',
|
||||
REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL',
|
||||
HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL',
|
||||
} 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.
|
||||
// Vue will replace these statically during production builds.
|
||||
|
||||
@@ -26,4 +26,8 @@ export class ViteAppMetadata implements IAppMetadata {
|
||||
public get homepageUrl(): string {
|
||||
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 { IEventSubscription } from './IEventSource';
|
||||
|
||||
export class EventSubscriptionCollection implements IEventSubscriptionCollection {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -12,4 +22,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection
|
||||
this.subscriptions.forEach((listener) => listener.unsubscribe());
|
||||
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';
|
||||
|
||||
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 { 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 { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||
import { IEnvironment } from './IEnvironment';
|
||||
import { WindowVariables } from './WindowVariables';
|
||||
import { validateWindowVariables } from './WindowVariablesValidator';
|
||||
import { IRuntimeEnvironment } from './IRuntimeEnvironment';
|
||||
|
||||
export class Environment implements IEnvironment {
|
||||
public static readonly CurrentEnvironment: IEnvironment = new Environment(window);
|
||||
export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||
public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window);
|
||||
|
||||
public readonly isDesktop: boolean;
|
||||
|
||||
public readonly os: OperatingSystem | undefined;
|
||||
|
||||
public readonly system: ISystemOperations | undefined;
|
||||
public readonly isNonProduction: boolean;
|
||||
|
||||
protected constructor(
|
||||
window: Partial<Window>,
|
||||
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||
windowValidator: WindowValidator = validateWindowVariables,
|
||||
) {
|
||||
if (!window) {
|
||||
throw new Error('missing window');
|
||||
}
|
||||
windowValidator(window);
|
||||
this.isNonProduction = environmentVariables.isNonProduction;
|
||||
this.isDesktop = isDesktop(window);
|
||||
if (this.isDesktop) {
|
||||
this.os = window?.os;
|
||||
@@ -34,7 +34,6 @@ export class Environment implements IEnvironment {
|
||||
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 {
|
||||
return window?.isDesktop === true;
|
||||
}
|
||||
|
||||
export type WindowValidator = typeof validateWindowVariables;
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ISanityCheckOptions {
|
||||
readonly validateMetadata: boolean;
|
||||
readonly validateEnvironment: boolean;
|
||||
readonly validateEnvironmentVariables: boolean;
|
||||
readonly validateWindowVariables: boolean;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
||||
import { ISanityValidator } from './Common/ISanityValidator';
|
||||
import { MetadataValidator } from './Validators/MetadataValidator';
|
||||
import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
|
||||
|
||||
const DefaultSanityValidators: ISanityValidator[] = [
|
||||
new MetadataValidator(),
|
||||
new EnvironmentVariablesValidator(),
|
||||
];
|
||||
|
||||
/* 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.
|
||||
*/
|
||||
export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||
if (!variables) {
|
||||
throw new Error('missing 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)];
|
||||
if (errors.length > 0) {
|
||||
@@ -25,6 +22,7 @@ function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<strin
|
||||
os: testOperatingSystem(variables.os),
|
||||
isDesktop: testIsDesktop(variables.isDesktop),
|
||||
system: testSystem(variables),
|
||||
log: testLogger(variables),
|
||||
};
|
||||
|
||||
for (const [propertyName, testResult] of Object.entries(tests)) {
|
||||
@@ -47,11 +45,18 @@ function testOperatingSystem(os: unknown): boolean {
|
||||
.includes(os);
|
||||
}
|
||||
|
||||
function testLogger(variables: Partial<WindowVariables>): boolean {
|
||||
if (!variables.isDesktop) {
|
||||
return true;
|
||||
}
|
||||
return isObject(variables.log);
|
||||
}
|
||||
|
||||
function testSystem(variables: Partial<WindowVariables>): boolean {
|
||||
if (!variables.isDesktop) {
|
||||
return true;
|
||||
}
|
||||
return variables.system !== undefined && isObject(variables.system);
|
||||
return isObject(variables.system);
|
||||
}
|
||||
|
||||
function testIsDesktop(isDesktop: unknown): boolean {
|
||||
@@ -70,7 +75,7 @@ function isBoolean(variable: unknown): variable is boolean {
|
||||
}
|
||||
|
||||
function isObject(variable: unknown): variable is object {
|
||||
return typeof variable === 'object'
|
||||
&& variable !== null // the data type of null is an object
|
||||
return Boolean(variable) // the data type of null is an object
|
||||
&& typeof variable === 'object'
|
||||
&& !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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin reset-ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -9,4 +9,3 @@
|
||||
@forward "./components/card";
|
||||
|
||||
@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 { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
||||
import { VueBootstrapper } from './Modules/VueBootstrapper';
|
||||
import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
|
||||
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator';
|
||||
import { AppInitializationLogger } from './Modules/AppInitializationLogger';
|
||||
|
||||
export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
@@ -15,9 +16,10 @@ export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||
private static getAllBootstrappers(): IVueBootstrapper[] {
|
||||
return [
|
||||
new IconBootstrapper(),
|
||||
new TreeBootstrapper(),
|
||||
new VueBootstrapper(),
|
||||
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 { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||
import {
|
||||
useCollectionStateKey, useApplicationKey, useEnvironmentKey,
|
||||
} from '@/presentation/injectionSymbols';
|
||||
import { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||
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) {
|
||||
registerSingleton(useApplicationKey, useApplication(context.app));
|
||||
registerTransient(useCollectionStateKey, () => useCollectionState(context));
|
||||
registerSingleton(useEnvironmentKey, Environment.CurrentEnvironment);
|
||||
}
|
||||
|
||||
function registerSingleton<T>(
|
||||
key: InjectionKey<T>,
|
||||
value: T,
|
||||
export function provideDependencies(
|
||||
context: IApplicationContext,
|
||||
api: VueDependencyInjectionApi = { provide, inject },
|
||||
) {
|
||||
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>(
|
||||
key: InjectionKey<() => T>,
|
||||
factory: () => T,
|
||||
) {
|
||||
provide(key, factory);
|
||||
export interface VueDependencyInjectionApi {
|
||||
provide<T>(key: InjectionKey<T>, value: T): void;
|
||||
inject<T>(key: InjectionKey<T>): T;
|
||||
}
|
||||
|
||||
@@ -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 TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
||||
|
||||
const singletonAppContext = await buildContext();
|
||||
@@ -33,10 +32,6 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
||||
validateRuntimeSanity({
|
||||
validateMetadata: true,
|
||||
validateEnvironment: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
defineComponent, PropType, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useApplicationKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||
import CodeInstruction from './CodeInstruction.vue';
|
||||
@@ -77,7 +77,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { info } = inject(useApplicationKey);
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
|
||||
const appName = computed<string>(() => info.name);
|
||||
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
import {
|
||||
defineComponent, ref, computed, inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||
@@ -54,12 +53,13 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
currentState, currentContext, onStateChange, events,
|
||||
} = inject(useCollectionStateKey)();
|
||||
const { isDesktop } = inject(useEnvironmentKey);
|
||||
currentState, currentContext, onStateChange,
|
||||
} = inject(InjectionKeys.useCollectionState)();
|
||||
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
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 hasCode = ref(false);
|
||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||
@@ -82,15 +82,18 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
onStateChange((newState) => {
|
||||
updateCurrentCode(newState.code.current);
|
||||
subscribeToCodeChanges(newState.code);
|
||||
}, { immediate: true });
|
||||
|
||||
function subscribeToCodeChanges(code: IApplicationCode) {
|
||||
hasCode.value = code.current && code.current.length > 0;
|
||||
events.unsubscribeAll();
|
||||
events.register(code.changed.on((newCode) => {
|
||||
hasCode.value = newCode && newCode.code.length > 0;
|
||||
}));
|
||||
events.unsubscribeAllAndRegister([
|
||||
code.changed.on((newCode) => updateCurrentCode(newCode.code)),
|
||||
]);
|
||||
}
|
||||
|
||||
function updateCurrentCode(code: string) {
|
||||
hasCode.value = code && code.length > 0;
|
||||
}
|
||||
|
||||
async function getCurrentCode(): Promise<IApplicationCode> {
|
||||
@@ -122,8 +125,12 @@ function getDownloadInstructions(
|
||||
return getInstructions(os, fileName);
|
||||
}
|
||||
|
||||
function getCanRunState(selectedOs: OperatingSystem, isDesktopVersion: boolean): boolean {
|
||||
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
|
||||
function getCanRunState(
|
||||
selectedOs: OperatingSystem,
|
||||
isDesktopVersion: boolean,
|
||||
hostOs: OperatingSystem,
|
||||
): boolean {
|
||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||
return isDesktopVersion && isRunningOnSelectedOs;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import {
|
||||
defineComponent, onUnmounted, onMounted, inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
@@ -37,7 +37,8 @@ export default defineComponent({
|
||||
NonCollapsing,
|
||||
},
|
||||
setup(props) {
|
||||
const { onStateChange, currentState, events } = inject(useCollectionStateKey)();
|
||||
const { onStateChange, currentState } = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const editorId = 'codeEditor';
|
||||
let editor: ace.Ace.Editor | undefined;
|
||||
@@ -61,20 +62,20 @@ export default defineComponent({
|
||||
newState.collection.scripting.language,
|
||||
);
|
||||
const appCode = newState.code;
|
||||
const innerCode = appCode.current || getDefaultCode(newState.collection.scripting.language);
|
||||
editor.setValue(innerCode, 1);
|
||||
events.unsubscribeAll();
|
||||
events.register(appCode.changed.on((code) => updateCode(code)));
|
||||
updateCode(appCode.current, newState.collection.scripting.language);
|
||||
events.unsubscribeAllAndRegister([
|
||||
appCode.changed.on((code) => handleCodeChange(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();
|
||||
if (event.isEmpty()) {
|
||||
const defaultCode = getDefaultCode(currentState.value.collection.scripting.language);
|
||||
editor.setValue(defaultCode, 1);
|
||||
return;
|
||||
}
|
||||
editor.setValue(event.code, 1);
|
||||
updateCode(event.code, currentState.value.collection.scripting.language);
|
||||
if (event.addedScripts?.length > 0) {
|
||||
reactToChanges(event, event.addedScripts);
|
||||
} else if (event.changedScripts?.length > 0) {
|
||||
|
||||
@@ -66,9 +66,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
import MenuOptionList from '../MenuOptionList.vue';
|
||||
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
||||
import { SelectionType, SelectionTypeHandler } from './SelectionTypeHandler';
|
||||
@@ -80,28 +81,27 @@ export default defineComponent({
|
||||
TooltipWrapper,
|
||||
},
|
||||
setup() {
|
||||
const { modifyCurrentState, onStateChange, events } = inject(useCollectionStateKey)();
|
||||
const { modifyCurrentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const currentSelection = ref(SelectionType.None);
|
||||
|
||||
let selectionTypeHandler: SelectionTypeHandler;
|
||||
|
||||
onStateChange(() => {
|
||||
unregisterMutators();
|
||||
|
||||
modifyCurrentState((state) => {
|
||||
registerStateMutator(state);
|
||||
selectionTypeHandler = new SelectionTypeHandler(state);
|
||||
updateSelections();
|
||||
events.unsubscribeAllAndRegister([
|
||||
subscribeAndUpdateSelections(state),
|
||||
]);
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
function unregisterMutators() {
|
||||
events.unsubscribeAll();
|
||||
}
|
||||
|
||||
function registerStateMutator(state: ICategoryCollectionState) {
|
||||
selectionTypeHandler = new SelectionTypeHandler(state);
|
||||
updateSelections();
|
||||
events.register(state.selection.changed.on(() => updateSelections()));
|
||||
function subscribeAndUpdateSelections(
|
||||
state: ICategoryCollectionState,
|
||||
): IEventSubscription {
|
||||
return state.selection.changed.on(() => updateSelections());
|
||||
}
|
||||
|
||||
function selectType(type: SelectionType) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import {
|
||||
defineComponent, computed, inject,
|
||||
} from 'vue';
|
||||
import { useApplicationKey, useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import MenuOptionList from './MenuOptionList.vue';
|
||||
import MenuOptionListItem from './MenuOptionListItem.vue';
|
||||
@@ -30,8 +30,8 @@ export default defineComponent({
|
||||
MenuOptionListItem,
|
||||
},
|
||||
setup() {
|
||||
const { modifyCurrentContext, currentState } = inject(useCollectionStateKey)();
|
||||
const { application } = inject(useApplicationKey);
|
||||
const { modifyCurrentContext, currentState } = inject(InjectionKeys.useCollectionState)();
|
||||
const { application } = inject(InjectionKeys.useApplication);
|
||||
|
||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
||||
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