Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9e0001ef8 | ||
|
|
62f8bfac2f | ||
|
|
75c9b51bf2 | ||
|
|
ec98d8417f | ||
|
|
736590558b | ||
|
|
6e40edd3f8 | ||
|
|
5f11c8d98f | ||
|
|
08737698c2 | ||
|
|
04b3133500 | ||
|
|
0d15992d56 |
@@ -1 +1,2 @@
|
|||||||
dist/
|
dist/
|
||||||
|
dist_electron/
|
||||||
@@ -6,10 +6,10 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
|
es2022: true, // add globals and sets parserOptions.ecmaVersion to 2022
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
// Vue specific rules, eslint-plugin-vue
|
// Vue specific rules, eslint-plugin-vue
|
||||||
// Added by Vue CLI
|
|
||||||
'plugin:vue/essential',
|
'plugin:vue/essential',
|
||||||
|
|
||||||
// Extends eslint-config-airbnb
|
// Extends eslint-config-airbnb
|
||||||
@@ -17,42 +17,14 @@ module.exports = {
|
|||||||
|
|
||||||
// Extends @typescript-eslint/recommended
|
// Extends @typescript-eslint/recommended
|
||||||
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
// Added by Vue CLI
|
|
||||||
'@vue/typescript/recommended',
|
'@vue/typescript/recommended',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 12, // ECMA 2021
|
|
||||||
/*
|
|
||||||
Having 'latest' leads to:
|
|
||||||
```
|
|
||||||
Parsing error: ecmaVersion must be a number. Received value of type string instead
|
|
||||||
```
|
|
||||||
For .js files in the project
|
|
||||||
*/
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
...getOwnRules(),
|
...getOwnRules(),
|
||||||
...getTurnedOffBrokenRules(),
|
...getTurnedOffBrokenRules(),
|
||||||
...getOpinionatedRuleOverrides(),
|
...getOpinionatedRuleOverrides(),
|
||||||
...getTodoRules(),
|
...getTodoRules(),
|
||||||
},
|
},
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'**/__tests__/*.{j,t}s?(x)',
|
|
||||||
'**/tests/unit/**/*.spec.{j,t}s?(x)',
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
mocha: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/tests/**/*.{j,t}s?(x)'],
|
|
||||||
rules: {
|
|
||||||
'no-console': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getOwnRules() {
|
function getOwnRules() {
|
||||||
|
|||||||
26
.github/workflows/checks.build.yaml
vendored
26
.github/workflows/checks.build.yaml
vendored
@@ -9,7 +9,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu, windows ]
|
os: [ macos, ubuntu, windows ]
|
||||||
mode: [ development, test, production ]
|
mode: [
|
||||||
|
# Vite mode: https://vitejs.dev/guide/env-and-mode.html
|
||||||
|
development, # Used by `dev` command
|
||||||
|
production, # Used by `build` command
|
||||||
|
# Vitest mode: https://vitest.dev/guide/cli.html
|
||||||
|
test, # Used by Vitest
|
||||||
|
]
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -26,12 +32,15 @@ jobs:
|
|||||||
name: Build
|
name: Build
|
||||||
run: npm run build -- --mode ${{ matrix.mode }}
|
run: npm run build -- --mode ${{ matrix.mode }}
|
||||||
|
|
||||||
# A new job is used due to environments/modes different from Vue CLI, https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
|
||||||
build-desktop:
|
build-desktop:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu, windows ]
|
os: [ macos, ubuntu, windows ]
|
||||||
mode: [ development, production ] # "test" is not supported https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1627
|
mode: [
|
||||||
|
# electron-vite modes: https://electron-vite.org/guide/env-and-mode.html#global-env-variables
|
||||||
|
development, # Used by `dev` command
|
||||||
|
production, # Used by `build` and `preview` commands
|
||||||
|
]
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -45,14 +54,11 @@ jobs:
|
|||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Install cross-env
|
name: Prebuild
|
||||||
# Used to set NODE_ENV due to https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
|
run: npm run electron:prebuild -- --mode ${{ matrix.mode }}
|
||||||
run: npm install --global cross-env
|
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
run: |-
|
run: npm run electron:build -- --publish never
|
||||||
cross-env-shell NODE_ENV=${{ matrix.mode }}
|
|
||||||
npm run electron:build -- --publish never --mode ${{ matrix.mode }}
|
|
||||||
|
|
||||||
create-icons:
|
create-icons:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -72,4 +78,4 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Create icons
|
name: Create icons
|
||||||
run: npm run create-icons
|
run: npm run icons:build
|
||||||
|
|||||||
67
.github/workflows/checks.desktop-runtime-errors.yaml
vendored
Normal file
67
.github/workflows/checks.desktop-runtime-errors.yaml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: checks.desktop-runtime-errors
|
||||||
|
# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows).
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-desktop:
|
||||||
|
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: Configure Ubuntu
|
||||||
|
if: matrix.os == 'ubuntu'
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
# Configure AppImage dependencies
|
||||||
|
sudo apt install -y libfuse2
|
||||||
|
|
||||||
|
# Configure DBUS (fixes `Failed to connect to the bus: Could not parse server address: Unknown address type`)
|
||||||
|
if ! command -v 'dbus-launch' &> /dev/null; then
|
||||||
|
echo 'DBUS does not exist, installing...'
|
||||||
|
sudo apt install -y dbus-x11 # Gives both dbus and dbus-launch utility
|
||||||
|
fi
|
||||||
|
sudo systemctl start dbus
|
||||||
|
DBUS_LAUNCH_OUTPUT=$(dbus-launch)
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "${DBUS_LAUNCH_OUTPUT}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo 'Error: dbus-launch command did not execute successfully. Exiting.' >&2
|
||||||
|
echo "${DBUS_LAUNCH_OUTPUT}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure fake (virtual) display
|
||||||
|
sudo apt install -y xvfb
|
||||||
|
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
|
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Install ImageMagick for screenshots
|
||||||
|
sudo apt install -y imagemagick
|
||||||
|
|
||||||
|
# Install xdotool and xprop (from x11-utils) for window title capturing
|
||||||
|
sudo apt install -y xdotool x11-utils
|
||||||
|
-
|
||||||
|
name: Test
|
||||||
|
shell: bash
|
||||||
|
run: node ./scripts/check-desktop-runtime-errors --screenshot
|
||||||
|
-
|
||||||
|
name: Upload screenshot
|
||||||
|
if: always() # Run even if previous step fails
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: screenshot-${{ matrix.os }}
|
||||||
|
path: screenshot.png
|
||||||
23
.github/workflows/release.desktop.yaml
vendored
23
.github/workflows/release.desktop.yaml
vendored
@@ -13,20 +13,29 @@ jobs:
|
|||||||
fail-fast: false # So publish runs for other OSes if one fails
|
fail-fast: false # So publish runs for other OSes if one fails
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
-
|
||||||
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: master # otherwise it defaults to the version tag missing bump commit
|
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||||
fetch-depth: 0 # fetch all history
|
fetch-depth: 0 # fetch all history
|
||||||
- name: Checkout to bump commit
|
-
|
||||||
|
name: Checkout to bump commit
|
||||||
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||||
- name: Setup node
|
-
|
||||||
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
- name: Install dependencies
|
-
|
||||||
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run unit tests
|
-
|
||||||
|
name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
- name: Publish desktop app
|
-
|
||||||
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
name: Prebuild
|
||||||
|
run: npm run electron:prebuild
|
||||||
|
-
|
||||||
|
name: Build and publish
|
||||||
|
run: npm run electron:build -- --publish always
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
EP_GH_IGNORE_TIME: true # Otherwise publishing fails if GitHub release is more than 2 hours old https://github.com/electron-userland/electron-builder/issues/2074
|
||||||
|
|||||||
2
.github/workflows/tests.e2e.yaml
vendored
2
.github/workflows/tests.e2e.yaml
vendored
@@ -23,4 +23,4 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Run e2e tests
|
name: Run e2e tests
|
||||||
run: npm run test:e2e -- --headless
|
run: npm run test:cy:run
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,6 +5,3 @@ dist/
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
# Cypress
|
|
||||||
/tests/e2e/screenshots
|
|
||||||
/tests/e2e/videos
|
|
||||||
|
|||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -11,8 +11,8 @@
|
|||||||
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||||
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||||
// Vue
|
// Vue
|
||||||
"jcbuisson.vue", // Highlights syntax.
|
"Vue.volar", // Official Vue extensions
|
||||||
"octref.vetur", // Adds Vetur, Vue tooling support.
|
"Vue.vscode-typescript-vue-plugin", // Official TypeScript Vue Plugin
|
||||||
// Scripting
|
// Scripting
|
||||||
"timonwong.shellcheck", // Lints bash files.
|
"timonwong.shellcheck", // Lints bash files.
|
||||||
"ms-vscode.powershell", // Lints PowerShell files.
|
"ms-vscode.powershell", // Lints PowerShell files.
|
||||||
|
|||||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.1 (2023-08-17)
|
||||||
|
|
||||||
|
* Transition to eslint-config-airbnb-with-typescript | [ff84f56](https://github.com/undergroundwires/privacy.sexy/commit/ff84f5676e496dd7ec5b3599e34ec9627d181ea2)
|
||||||
|
* Improve user privacy with secure outbound links | [3a594ac](https://github.com/undergroundwires/privacy.sexy/commit/3a594ac7fd708dc1e98155ffb9b21acd4e1fcf2d)
|
||||||
|
* Refactor Vue components using Composition API #230 | [1b9be8f](https://github.com/undergroundwires/privacy.sexy/commit/1b9be8fe2d72d8fb5cf1fed6dcc0b9777171aa98)
|
||||||
|
* Fix failing security tests | [3bc8da4](https://github.com/undergroundwires/privacy.sexy/commit/3bc8da4cbf1e2bd758dc3fffe4b1e62dc3beb7b3)
|
||||||
|
* Improve Defender scripts #201 | [061afad](https://github.com/undergroundwires/privacy.sexy/commit/061afad9673a41454c2421c318898f2b4f4cf504)
|
||||||
|
* Fix failing tests due to failed error logging | [986ba07](https://github.com/undergroundwires/privacy.sexy/commit/986ba078a643de6acbee50fff9cf77494ca7ea7f)
|
||||||
|
* Implement custom lightweight modal #230 | [9e5491f](https://github.com/undergroundwires/privacy.sexy/commit/9e5491fdbf2d9d40d974f5ad0e879a6d5c6d1e55)
|
||||||
|
* Refactor usage of tooltips for flexibility | [bc91237](https://github.com/undergroundwires/privacy.sexy/commit/bc91237d7c54bdcd15c5c39a55def50d172bb659)
|
||||||
|
* Fix revert toggle partial rendering | [39e650c](https://github.com/undergroundwires/privacy.sexy/commit/39e650cf110bee6b1b21d9b2902b36b0e2568d54)
|
||||||
|
* Increase testability through dependency injection | [ae75059](https://github.com/undergroundwires/privacy.sexy/commit/ae75059cc14db41f55dd2056f528442c7d319dd2)
|
||||||
|
* Refactor filter (search query) event handling | [6a20d80](https://github.com/undergroundwires/privacy.sexy/commit/6a20d804dc365d22c1248d787f9912271f508eeb)
|
||||||
|
* Migrate to ES6 modules | [a14929a](https://github.com/undergroundwires/privacy.sexy/commit/a14929a13cc6260b514692d9b4f1cdf5fb85d8b2)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.0...0.12.1)
|
||||||
|
|
||||||
## 0.12.0 (2023-08-03)
|
## 0.12.0 (2023-08-03)
|
||||||
|
|
||||||
* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87)
|
* Improve script/category name validation | [b210aad](https://github.com/undergroundwires/privacy.sexy/commit/b210aaddf26629179f77fe19f62f65d8a0ca2b87)
|
||||||
|
|||||||
@@ -70,6 +70,12 @@
|
|||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.desktop-runtime-errors.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
alt="Status of runtime error checks for the desktop application"
|
||||||
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.desktop-runtime-errors/badge.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
<!-- Release -->
|
<!-- Release -->
|
||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
import setupPlugins from './tests/e2e/plugins/index.js';
|
import ViteConfig from './vite.config';
|
||||||
|
|
||||||
|
const CYPRESS_BASE_DIR = 'tests/e2e/';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
fixturesFolder: 'tests/e2e/fixtures',
|
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
|
||||||
screenshotsFolder: 'tests/e2e/screenshots',
|
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
|
||||||
videosFolder: 'tests/e2e/videos',
|
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
||||||
return setupPlugins(on, config);
|
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||||
},
|
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
||||||
specPattern: 'tests/e2e/specs/**/*.cy.{js,jsx,ts,tsx}',
|
|
||||||
supportFile: 'tests/e2e/support/index.js',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Application layer enables [data-driven programming](https://en.wikipedia.org/wik
|
|||||||
|
|
||||||
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts).
|
Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts).
|
||||||
|
|
||||||
A webpack loader loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime.
|
||||||
|
|
||||||
Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function).
|
Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function).
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ Application is
|
|||||||
|
|
||||||
Application uses highly decoupled models & services in different DDD layers:
|
Application uses highly decoupled models & services in different DDD layers:
|
||||||
|
|
||||||
- presentation layer (see [presentation.md](./presentation.md)),
|
**Application layer** (see [application.md](./application.md)):
|
||||||
- application layer (see [application.md](./application.md)),
|
|
||||||
- and domain layer.
|
|
||||||
|
|
||||||
Application layer depends on and consumes domain layer. [Presentation layer](./presentation.md) consumes and depends on application layer along with domain layer. Application and presentation layers can communicate through domain model.
|
- Coordinates application activities and consumes the domain layer.
|
||||||
|
|
||||||
|
**Presentation layer** (see [presentation.md](./presentation.md)):
|
||||||
|
|
||||||
|
- Handles UI/UX, consumes both the application and domain layers.
|
||||||
|
- May communicate directly with the infrastructure layer for technical needs, but avoids domain logic.
|
||||||
|
|
||||||
|
**Domain layer**:
|
||||||
|
|
||||||
|
- Serves as the system's core and central truth.
|
||||||
|
- Facilitates communication between the application and presentation layers through the domain model.
|
||||||
|
|
||||||
|
**Infrastructure layer**:
|
||||||
|
|
||||||
|
- Manages technical implementations without dependencies on other layers or domain knowledge.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ You could run other types of tests as well, but they may take longer time and ov
|
|||||||
|
|
||||||
- Run unit tests: `npm run test:unit`
|
- Run unit tests: `npm run test:unit`
|
||||||
- Run integration tests: `npm run test:integration`
|
- Run integration tests: `npm run test:integration`
|
||||||
- Run e2e (end-to-end) tests
|
- Run end-to-end (e2e) tests:
|
||||||
- Interactive mode with GUI: `npm run test:e2e`
|
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
||||||
- Headless mode without GUI: `npm run test:e2e -- --headless`
|
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
||||||
|
|
||||||
📖 Read more about testing in [tests](./tests.md).
|
📖 Read more about testing in [tests](./tests.md).
|
||||||
|
|
||||||
@@ -35,11 +35,25 @@ You could run other types of tests as well, but they may take longer time and ov
|
|||||||
|
|
||||||
### Running
|
### Running
|
||||||
|
|
||||||
- Run in local server: `npm run serve`
|
**Web:**
|
||||||
|
|
||||||
|
- Run in local server: `npm run dev`
|
||||||
- 💡 Meant for local development with features such as hot-reloading.
|
- 💡 Meant for local development with features such as hot-reloading.
|
||||||
- Run using Docker:
|
- Preview production build: `npm run preview`
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
- Start a local web server that serves the built solution from `./dist`.
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
- 💡 Run `npm run build` before `npm run preview`.
|
||||||
|
|
||||||
|
**Desktop apps:**
|
||||||
|
|
||||||
|
- `npm run electron:dev`: The command will build the main process and preload scripts source code, and start a dev server for the renderer, and start the Electron app.
|
||||||
|
- `npm run electron:preview`: The command will build the main process, preload scripts and renderer source code, and start the Electron app to preview.
|
||||||
|
- `npm run electron:prebuild`: The command will build the main process, preload scripts and renderer source code. Usually before packaging the Electron application, you need to execute this command.
|
||||||
|
- `npm run electron:build`: Prebuilds the Electron application, packages and publishes it through `electron-builder`.
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
|
||||||
|
1. Build: `docker build -t undergroundwires/privacy.sexy:latest .`
|
||||||
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest`
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
|
|||||||
@@ -10,24 +10,25 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
|
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Shared hooks for state access
|
- [**`hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
||||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
|
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains reusable styles coupled to a Vue/HTML component.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles that override third-party components used.
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles for Vue components.
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Primary Sass file, passes along all other styles, should be the single file used from other components.
|
- [**`vendors-extensions/`**](./../src/presentation/assets/styles/third-party-extensions): Contains styles for third-party components.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Electron configuration for the desktop application.
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [**`/public/`**](./../public/): Contains static assets that are directly copied and do not go through webpack.
|
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [**`/vue.config.cjs`**](./../vue.config.cjs): Global Vue CLI configurations loaded by `@vue/cli-service`.
|
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): PostCSS configurations used by Vue CLI internally.
|
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||||
- [**`/babel.config.cjs`**](./../babel.config.cjs): Babel configurations for polyfills used by `@vue/cli-plugin-babel`.
|
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||||
|
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||||
|
|
||||||
## Visual design best-practices
|
## Visual design best-practices
|
||||||
|
|
||||||
@@ -86,6 +87,10 @@ Shared components include:
|
|||||||
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is utilized for rendering modal windows.
|
- [ModalDialog.vue](./../src/presentation/components/Shared/Modal/ModalDialog.vue) is utilized for rendering modal windows.
|
||||||
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue) acts as a wrapper for rendering tooltips.
|
- [TooltipWrapper.vue](./../src/presentation/components/Shared/TooltipWrapper.vue) acts as a wrapper for rendering tooltips.
|
||||||
|
|
||||||
|
## Desktop builds
|
||||||
|
|
||||||
|
Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages.
|
||||||
|
|
||||||
## Sass naming convention
|
## Sass naming convention
|
||||||
|
|
||||||
- Use lowercase for variables/functions/mixins, e.g.:
|
- Use lowercase for variables/functions/mixins, e.g.:
|
||||||
|
|||||||
123
docs/tests.md
123
docs/tests.md
@@ -5,78 +5,79 @@ There are different types of tests executed:
|
|||||||
1. [Unit tests](#unit-tests)
|
1. [Unit tests](#unit-tests)
|
||||||
2. [Integration tests](#integration-tests)
|
2. [Integration tests](#integration-tests)
|
||||||
3. [End-to-end (E2E) tests](#e2e-tests)
|
3. [End-to-end (E2E) tests](#e2e-tests)
|
||||||
|
4. [Automated checks](#automated-checks)
|
||||||
|
|
||||||
Common aspects for all tests:
|
## Unit and integration tests
|
||||||
|
|
||||||
- They use [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
|
- They utilize [Vitest](https://vitest.dev/).
|
||||||
- Their files end with `.spec.{ts|js}` suffix.
|
- Test files are suffixed with `.spec.ts`.
|
||||||
|
|
||||||
💡 You can use path/module alias `@/tests` in import statements.
|
|
||||||
|
|
||||||
## Unit tests
|
|
||||||
|
|
||||||
- Unit tests test each component in isolation.
|
|
||||||
- All unit tests goes under [`./tests/unit`](./../tests/unit).
|
|
||||||
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
|
|
||||||
- Unit tests include also Vue component tests using `@vue/test-utils`.
|
|
||||||
|
|
||||||
### Unit tests structure
|
|
||||||
|
|
||||||
- [`./src/`](./../src/)
|
|
||||||
- Includes source code that unit tests will test.
|
|
||||||
- [`./tests/unit/`](./../tests/unit/)
|
|
||||||
- Includes test code.
|
|
||||||
- Tests follow same folder structure as [`./src/`](./../src).
|
|
||||||
- E.g. if system under test lies in [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) then its tests would be in test would be at [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
|
||||||
- [`shared/`](./../tests/unit/shared/)
|
|
||||||
- Includes common functionality that's shared across unit tests.
|
|
||||||
- [`Assertions/`](./../tests/unit/shared/Assertions):
|
|
||||||
- Common assertions that extend [Chai Assertion Library](https://www.chaijs.com/).
|
|
||||||
- Asserting functions should start with `expect` prefix.
|
|
||||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
|
||||||
- Shared test cases.
|
|
||||||
- Functions that calls `it()` from [Mocha test framework](https://mochajs.org/) should have `it` prefix.
|
|
||||||
- E.g. `itEachAbsentCollectionValue()`.
|
|
||||||
- [`Stubs/`](./../tests/unit/shared/Stubs)
|
|
||||||
- Includes stubs to be able to test components in isolation.
|
|
||||||
- Stubs have minimal and dummy behavior to be functional, they may also have spying or mocking functions.
|
|
||||||
|
|
||||||
### Unit tests naming
|
|
||||||
|
|
||||||
- Each test suite first describe the system under test.
|
|
||||||
- E.g. tests for class `Application.ts` are all inside `Application.spec.ts`.
|
|
||||||
- `describe` blocks tests for same function (if applicable).
|
|
||||||
- E.g. test for `run()` are inside `describe('run', () => ..)`.
|
|
||||||
|
|
||||||
### Act, arrange, assert
|
### Act, arrange, assert
|
||||||
|
|
||||||
- Tests use act, arrange and assert (AAA) pattern when applicable.
|
- Tests implement the act, arrange, and assert (AAA) pattern.
|
||||||
- **Arrange**
|
- **Arrange**
|
||||||
- Sets up the test case.
|
- Sets up the test scenario and environment.
|
||||||
- Starts with comment line `// arrange`.
|
- Begins with comment line `// arrange`.
|
||||||
- **Act**
|
- **Act**
|
||||||
- Executes the actual test.
|
- Executes the actual test.
|
||||||
- Starts with comment line `// act`.
|
- Begins with comment line `// act`.
|
||||||
- **Assert**
|
- **Assert**
|
||||||
- Elicit some sort of expectation.
|
- Sets an expectation for the test's outcome.
|
||||||
- Starts with comment line `// assert`.
|
- Begins with comment line `// assert`.
|
||||||
|
|
||||||
## Integration tests
|
### Unit tests
|
||||||
|
|
||||||
- Tests functionality of a component in combination with others (not isolated).
|
- Evaluate individual components in isolation.
|
||||||
- Ensure dependencies to third parties work as expected.
|
- Located in [`./tests/unit`](./../tests/unit).
|
||||||
- Defined in [./tests/integration](./../tests/integration).
|
- Achieve isolation using [stubs](./../tests/unit/shared/Stubs).
|
||||||
|
- Include Vue component tests, enabled by `@vue/test-utils`.
|
||||||
|
|
||||||
|
#### Unit tests naming
|
||||||
|
|
||||||
|
- Test suites start with a description of the component or system under test.
|
||||||
|
- E.g., tests for `Application.ts` are contained in `Application.spec.ts`.
|
||||||
|
- Whenever possible, `describe` blocks group tests of the same function.
|
||||||
|
- E.g., tests for `run()` are inside `describe('run', () => ...)`.
|
||||||
|
|
||||||
|
### Integration tests
|
||||||
|
|
||||||
|
- Assess the combined functionality of components.
|
||||||
|
- They verify that third-party dependencies function as anticipated.
|
||||||
|
|
||||||
## E2E tests
|
## E2E tests
|
||||||
|
|
||||||
- Test the functionality and performance of a running application.
|
- Examine the live web application's functionality and performance.
|
||||||
- Vue CLI plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) configures E2E tests.
|
- Uses Cypress to run the tests.
|
||||||
- Test names and folders have logical structure based on tests executed.
|
|
||||||
- The structure is following:
|
## Automated checks
|
||||||
- [`cypress.config.ts`](./../cypress.config.ts): Cypress configuration file.
|
|
||||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
|
These checks validate various qualities like runtime execution, building process, security testing, etc.
|
||||||
- [`/specs/`](./../tests/e2e/specs/): Test files named with `.spec.js` extension.
|
|
||||||
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before loading project.
|
- Use [various tools](./../package.json) and [scripts](./../scripts).
|
||||||
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
|
- Are automatically executed as [GitHub workflows](./../.github/workflows).
|
||||||
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
|
|
||||||
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
## Tests structure
|
||||||
|
|
||||||
|
- [`package.json`](./../package.json): Defines test commands and includes tools used in tests.
|
||||||
|
- [`vite.config.ts`](./../vite.config.ts): Configures `vitest` for unit and integration tests.
|
||||||
|
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||||
|
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||||
|
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||||
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
|
- Stores unit test code.
|
||||||
|
- The directory structure mirrors [`./src/`](./../src).
|
||||||
|
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||||
|
- [`shared/`](./../tests/unit/shared/)
|
||||||
|
- Contains shared unit test functionalities.
|
||||||
|
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
||||||
|
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||||
|
- Shared test cases.
|
||||||
|
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||||
|
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
||||||
|
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
||||||
|
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
||||||
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
||||||
|
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
|
||||||
|
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
||||||
|
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||||
|
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||||
|
|||||||
31
electron-builder.yml
Normal file
31
electron-builder.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# -------
|
||||||
|
# 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
|
||||||
68
electron.vite.config.ts
Normal file
68
electron.vite.config.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { mergeConfig, UserConfig } from 'vite';
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||||
|
import { getAliasesFromTsConfig, getClientEnvironmentVariables } from './vite-config-helper';
|
||||||
|
import { createVueConfig } from './vite.config';
|
||||||
|
|
||||||
|
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/');
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: getSharedElectronConfig({
|
||||||
|
distDirSubfolder: 'main',
|
||||||
|
entryFilePath: MAIN_ENTRY_FILE,
|
||||||
|
}),
|
||||||
|
preload: getSharedElectronConfig({
|
||||||
|
distDirSubfolder: 'preload',
|
||||||
|
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||||
|
}),
|
||||||
|
renderer: mergeConfig(
|
||||||
|
createVueConfig({
|
||||||
|
supportLegacyBrowsers: false,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
build: {
|
||||||
|
outDir: resolve(DIST_DIR, 'renderer'),
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: WEB_INDEX_HTML_PATH,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSharedElectronConfig(options: {
|
||||||
|
readonly distDirSubfolder: string;
|
||||||
|
readonly entryFilePath: string;
|
||||||
|
}): UserConfig {
|
||||||
|
return {
|
||||||
|
build: {
|
||||||
|
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
||||||
|
lib: {
|
||||||
|
entry: options.entryFilePath,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: '[name].cjs', // This is needed so `type="module"` works
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
define: {
|
||||||
|
...getClientEnvironmentVariables(),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
...getAliasesFromTsConfig(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePathFromProjectRoot(pathSegment: string) {
|
||||||
|
return resolve(__dirname, pathSegment);
|
||||||
|
}
|
||||||
@@ -4,9 +4,6 @@ This folder contains image files and other resources related to images.
|
|||||||
|
|
||||||
## logo.svg
|
## logo.svg
|
||||||
|
|
||||||
[logo.svg](./logo.svg) is the master logo from which all other icons or images are created from.
|
[`logo.svg`](./logo.svg) serves as the primary logo from which all other icons and images are derived.
|
||||||
It should be the only file that will be changed manually.
|
Only modify this file manually.
|
||||||
|
After making changes, execute `npm run build:icons` to regenerate logo files in various formats.
|
||||||
[`logo-update.mjs`](./logo-update.mjs) script in this folder updates all the logo files.
|
|
||||||
It should be executed everytime the logo is changed.
|
|
||||||
It automates recreation of logo files in different formats.
|
|
||||||
|
|||||||
25629
package-lock.json
generated
25629
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@@ -1,30 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.0",
|
"version": "0.12.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Now you have the choice",
|
"slogan": "Now you have the choice",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"test:unit": "vue-cli-service test:unit --include ./tests/bootstrap/setup.ts",
|
"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: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",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||||
"create-icons": "node img/logo-update.mjs",
|
"icons:build": "node scripts/logo-update.js",
|
||||||
"electron:build": "vue-cli-service electron:build",
|
"electron:dev": "electron-vite dev",
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
"electron:preview": "electron-vite preview",
|
||||||
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
|
"electron:prebuild": "electron-vite build",
|
||||||
|
"electron:build": "electron-builder",
|
||||||
|
"lint:eslint": "eslint .",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps",
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\" --include ./tests/bootstrap/setup.ts"
|
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "./dist_electron/main/index.cjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
@@ -33,7 +38,6 @@
|
|||||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.23.4",
|
"ace-builds": "^1.23.4",
|
||||||
"core-js": "^3.32.0",
|
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-progressbar": "^2.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@@ -45,23 +49,18 @@
|
|||||||
"vue": "^2.7.14"
|
"vue": "^2.7.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
"@rushstack/eslint-patch": "^1.3.2",
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
"@types/ace": "^0.0.48",
|
"@types/ace": "^0.0.48",
|
||||||
"@types/chai": "^4.3.5",
|
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@types/mocha": "^10.0.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vue/cli-plugin-e2e-cypress": "~5.0.8",
|
"@vitejs/plugin-vue2": "^2.2.0",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
|
||||||
"@vue/cli-plugin-typescript": "~5.0.8",
|
|
||||||
"@vue/cli-plugin-unit-mocha": "~5.0.8",
|
|
||||||
"@vue/cli-service": "~5.0.8",
|
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/test-utils": "^1.3.6",
|
"@vue/test-utils": "^1.3.6",
|
||||||
"chai": "^4.3.7",
|
"autoprefixer": "^10.4.15",
|
||||||
"cypress": "^12.17.2",
|
"cypress": "^12.17.2",
|
||||||
"electron": "^25.3.2",
|
"electron": "^25.3.2",
|
||||||
"electron-builder": "^24.6.3",
|
"electron-builder": "^24.6.3",
|
||||||
@@ -69,32 +68,32 @@
|
|||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-log": "^4.4.8",
|
"electron-log": "^4.4.8",
|
||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
|
"electron-vite": "^1.0.27",
|
||||||
"eslint": "^8.46.0",
|
"eslint": "^8.46.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-cypress": "^2.14.0",
|
||||||
"eslint-plugin-vue": "^9.6.0",
|
"eslint-plugin-vue": "^9.6.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||||
"icon-gen": "^3.0.1",
|
"icon-gen": "^3.0.1",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"jsdom": "^22.1.0",
|
||||||
"markdownlint-cli": "^0.35.0",
|
"markdownlint-cli": "^0.35.0",
|
||||||
|
"postcss": "^8.4.28",
|
||||||
"remark-cli": "^11.0.0",
|
"remark-cli": "^11.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^5.1.2",
|
"remark-preset-lint-consistent": "^5.1.2",
|
||||||
"remark-validate-links": "^12.1.1",
|
"remark-validate-links": "^12.1.1",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.64.1",
|
||||||
"sass-loader": "^13.3.2",
|
"start-server-and-test": "^2.0.0",
|
||||||
"svgexport": "^0.4.2",
|
"svgexport": "^0.4.2",
|
||||||
"ts-loader": "^9.4.4",
|
"terser": "^5.19.2",
|
||||||
"tslib": "~2.4.0",
|
"tslib": "~2.4.0",
|
||||||
"typescript": "~4.6.2",
|
"typescript": "~4.6.2",
|
||||||
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
|
"vite": "^4.4.9",
|
||||||
|
"vitest": "^0.34.2",
|
||||||
|
"vue-tsc": "^1.8.8",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
|
||||||
"vue-cli-plugin-electron-builder": {
|
|
||||||
"electron-builder": "^24.6.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
|
"terser": "Used by @vitejs/plugin-legacy for minification",
|
||||||
"typescript": [
|
"typescript": [
|
||||||
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
"Cannot upgrade to 5.X.X due to unmaintained @vue/cli-plugin-typescript, https://github.com/vuejs/vue-cli/issues/7401",
|
||||||
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
"Cannot upgrade to > 4.6.X otherwise unit tests do not work, https://github.com/evanw/node-source-map-support/issues/252"
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
module.exports = {
|
const autoprefixer = require('autoprefixer');
|
||||||
plugins: {
|
|
||||||
autoprefixer: {},
|
module.exports = () => {
|
||||||
},
|
return {
|
||||||
|
plugins: [
|
||||||
|
autoprefixer(),
|
||||||
|
],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
10
scripts/check-desktop-runtime-errors/.eslintrc.cjs
Normal file
10
scripts/check-desktop-runtime-errors/.eslintrc.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
require('@rushstack/eslint-patch/modern-module-resolution.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/extensions': ['error', 'always'],
|
||||||
|
},
|
||||||
|
};
|
||||||
35
scripts/check-desktop-runtime-errors/README.md
Normal file
35
scripts/check-desktop-runtime-errors/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# check-desktop-runtime-errors
|
||||||
|
|
||||||
|
This script automates the processes of:
|
||||||
|
|
||||||
|
1) Building
|
||||||
|
2) Packaging
|
||||||
|
3) Installing
|
||||||
|
4) Executing
|
||||||
|
5) Verifying Electron distributions
|
||||||
|
|
||||||
|
It runs the application for a duration and detects runtime errors in the packaged application via:
|
||||||
|
|
||||||
|
- **Log verification**: Checking application logs for errors and validating successful application initialization.
|
||||||
|
- **`stderr` monitoring**: Continuous listening to the `stderr` stream for unexpected errors.
|
||||||
|
- **Window title inspection**: Checking for window titles that indicate crashes before logging becomes possible.
|
||||||
|
|
||||||
|
Upon error, the script captures a screenshot (if `--screenshot` is provided) and terminates.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node ./scripts/check-desktop-runtime-errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
- `--build`: Clears the electron distribution directory and forces a rebuild of the Electron app.
|
||||||
|
- `--screenshot`: Takes a screenshot of the desktop environment after running the application.
|
||||||
|
|
||||||
|
This module provides utilities for building, executing, and validating Electron desktop apps.
|
||||||
|
It can be used to automate checking for runtime errors during development.
|
||||||
|
|
||||||
|
## Configs
|
||||||
|
|
||||||
|
Configurations are defined in [`config.js`](./config.js).
|
||||||
55
scripts/check-desktop-runtime-errors/app/app-logs.js
Normal file
55
scripts/check-desktop-runtime-errors/app/app-logs.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
126
scripts/check-desktop-runtime-errors/app/check-for-errors.js
Normal file
126
scripts/check-desktop-runtime-errors/app/check-for-errors.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
34
scripts/check-desktop-runtime-errors/app/extractors/linux.js
Normal file
34
scripts/check-desktop-runtime-errors/app/extractors/linux.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
scripts/check-desktop-runtime-errors/app/extractors/macos.js
Normal file
66
scripts/check-desktop-runtime-errors/app/extractors/macos.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { runCommand } from '../../utils/run-command.js';
|
||||||
|
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
||||||
|
import { log, die, LOG_LEVELS } from '../../utils/log.js';
|
||||||
|
|
||||||
|
export async function prepareMacOsApp(desktopDistPath) {
|
||||||
|
const { absolutePath: dmgPath } = await findSingleFileByExtension('dmg', desktopDistPath);
|
||||||
|
const { mountPath } = await mountDmg(dmgPath);
|
||||||
|
const appPath = await findMacAppExecutablePath(mountPath);
|
||||||
|
return {
|
||||||
|
appExecutablePath: appPath,
|
||||||
|
cleanup: async () => {
|
||||||
|
log('Cleaning up resources...');
|
||||||
|
await detachMount(mountPath);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountDmg(dmgFile) {
|
||||||
|
const { stdout: hdiutilOutput, error } = await runCommand(`hdiutil attach '${dmgFile}'`);
|
||||||
|
if (error) {
|
||||||
|
die(`Failed to mount DMG file at ${dmgFile}.\n${error}`);
|
||||||
|
}
|
||||||
|
const mountPathMatch = hdiutilOutput.match(/\/Volumes\/[^\n]+/);
|
||||||
|
const mountPath = mountPathMatch ? mountPathMatch[0] : null;
|
||||||
|
return {
|
||||||
|
mountPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findMacAppExecutablePath(mountPath) {
|
||||||
|
const { stdout: findOutput, error } = await runCommand(
|
||||||
|
`find '${mountPath}' -maxdepth 1 -type d -name "*.app"`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
die(`Failed to find executable path at mount path ${mountPath}\n${error}`);
|
||||||
|
}
|
||||||
|
const appFolder = findOutput.trim();
|
||||||
|
const appName = appFolder.split('/').pop().replace('.app', '');
|
||||||
|
const appPath = `${appFolder}/Contents/MacOS/${appName}`;
|
||||||
|
if (await exists(appPath)) {
|
||||||
|
log(`Application is located at ${appPath}`);
|
||||||
|
} else {
|
||||||
|
die(`Application does not exist at ${appPath}`);
|
||||||
|
}
|
||||||
|
return appPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detachMount(mountPath, retries = 5) {
|
||||||
|
const { error } = await runCommand(`hdiutil detach '${mountPath}'`);
|
||||||
|
if (error) {
|
||||||
|
if (retries <= 0) {
|
||||||
|
log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LOG_LEVELS.WARN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sleep(500);
|
||||||
|
await detachMount(mountPath, retries - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(`Successfully detached from ${mountPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(milliseconds) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, milliseconds);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
164
scripts/check-desktop-runtime-errors/app/runner.js
Normal file
164
scripts/check-desktop-runtime-errors/app/runner.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { log, LOG_LEVELS, die } from '../utils/log.js';
|
||||||
|
import { captureScreen } from './system-capture/screen-capture.js';
|
||||||
|
import { captureWindowTitles } from './system-capture/window-title-capture.js';
|
||||||
|
|
||||||
|
const TERMINATION_GRACE_PERIOD_IN_SECONDS = 60;
|
||||||
|
const TERMINATION_CHECK_INTERVAL_IN_MS = 1000;
|
||||||
|
const WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS = 100;
|
||||||
|
|
||||||
|
export function runApplication(
|
||||||
|
appFile,
|
||||||
|
executionDurationInSeconds,
|
||||||
|
enableScreenshot,
|
||||||
|
screenshotPath,
|
||||||
|
) {
|
||||||
|
if (!appFile) {
|
||||||
|
throw new Error('Missing app file');
|
||||||
|
}
|
||||||
|
|
||||||
|
logDetails(appFile, executionDurationInSeconds);
|
||||||
|
|
||||||
|
const processDetails = {
|
||||||
|
stderrData: '',
|
||||||
|
stdoutData: '',
|
||||||
|
explicitlyKilled: false,
|
||||||
|
windowTitles: [],
|
||||||
|
isCrashed: false,
|
||||||
|
isDone: false,
|
||||||
|
process: undefined,
|
||||||
|
resolve: () => { /* NOOP */ },
|
||||||
|
};
|
||||||
|
|
||||||
|
const process = spawn(appFile);
|
||||||
|
processDetails.process = process;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
processDetails.resolve = resolve;
|
||||||
|
handleTitleCapture(process.pid, processDetails);
|
||||||
|
handleProcessEvents(
|
||||||
|
processDetails,
|
||||||
|
enableScreenshot,
|
||||||
|
screenshotPath,
|
||||||
|
executionDurationInSeconds,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function logDetails(appFile, executionDurationInSeconds) {
|
||||||
|
log(
|
||||||
|
[
|
||||||
|
'Executing the app to check for errors...',
|
||||||
|
`Maximum execution time: ${executionDurationInSeconds}`,
|
||||||
|
`Application path: ${appFile}`,
|
||||||
|
].join('\n\t'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTitleCapture(processId, processDetails) {
|
||||||
|
const capture = async () => {
|
||||||
|
const titles = await captureWindowTitles(processId);
|
||||||
|
|
||||||
|
(titles || []).forEach((title) => {
|
||||||
|
if (!title || !title.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!processDetails.windowTitles.includes(title)) {
|
||||||
|
log(`New window title captured: ${title}`);
|
||||||
|
processDetails.windowTitles.push(title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!processDetails.isDone) {
|
||||||
|
setTimeout(capture, WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
capture();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProcessEvents(
|
||||||
|
processDetails,
|
||||||
|
enableScreenshot,
|
||||||
|
screenshotPath,
|
||||||
|
executionDurationInSeconds,
|
||||||
|
) {
|
||||||
|
const { process } = processDetails;
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
processDetails.stderrData += data.toString();
|
||||||
|
});
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
processDetails.stdoutData += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
die(`An issue spawning the child process: ${error}`, LOG_LEVELS.ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', async (code) => {
|
||||||
|
await onProcessExit(code, processDetails, enableScreenshot, screenshotPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath);
|
||||||
|
}, executionDurationInSeconds * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onProcessExit(code, processDetails, enableScreenshot, screenshotPath) {
|
||||||
|
log(`Application exited ${code === null || Number.isNaN(code) ? '.' : `with code ${code}`}`);
|
||||||
|
|
||||||
|
if (processDetails.explicitlyKilled) return;
|
||||||
|
|
||||||
|
processDetails.isCrashed = true;
|
||||||
|
|
||||||
|
if (enableScreenshot) {
|
||||||
|
await captureScreen(screenshotPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
finishProcess(processDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath) {
|
||||||
|
if (enableScreenshot) {
|
||||||
|
await captureScreen(screenshotPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
processDetails.explicitlyKilled = true;
|
||||||
|
await terminateGracefully(process);
|
||||||
|
finishProcess(processDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishProcess(processDetails) {
|
||||||
|
processDetails.isDone = true;
|
||||||
|
processDetails.resolve({
|
||||||
|
stderr: processDetails.stderrData,
|
||||||
|
stdout: processDetails.stdoutData,
|
||||||
|
windowTitles: [...processDetails.windowTitles],
|
||||||
|
isCrashed: processDetails.isCrashed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function terminateGracefully(process) {
|
||||||
|
let elapsedSeconds = 0;
|
||||||
|
log('Attempting to terminate the process gracefully...');
|
||||||
|
process.kill('SIGTERM');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
elapsedSeconds += TERMINATION_CHECK_INTERVAL_IN_MS / 1000;
|
||||||
|
|
||||||
|
if (!process.killed) {
|
||||||
|
if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) {
|
||||||
|
process.kill('SIGKILL');
|
||||||
|
log('Process did not terminate gracefully within the grace period. Forcing termination.', LOG_LEVELS.WARN);
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('Process terminated gracefully.');
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, TERMINATION_CHECK_INTERVAL_IN_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { unlink } from 'fs/promises';
|
||||||
|
import { runCommand } from '../../utils/run-command.js';
|
||||||
|
import { log, LOG_LEVELS } from '../../utils/log.js';
|
||||||
|
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from '../../utils/platform.js';
|
||||||
|
import { exists } from '../../utils/io.js';
|
||||||
|
|
||||||
|
export async function captureScreen(imagePath) {
|
||||||
|
if (!imagePath) {
|
||||||
|
throw new Error('Path for screenshot not provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await exists(imagePath)) {
|
||||||
|
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LOG_LEVELS.WARN);
|
||||||
|
unlink(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformCommands = {
|
||||||
|
[SUPPORTED_PLATFORMS.MAC]: `screencapture -x ${imagePath}`,
|
||||||
|
[SUPPORTED_PLATFORMS.LINUX]: `import -window root ${imagePath}`,
|
||||||
|
[SUPPORTED_PLATFORMS.WINDOWS]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandForPlatform = platformCommands[CURRENT_PLATFORM];
|
||||||
|
|
||||||
|
if (!commandForPlatform) {
|
||||||
|
log(`Screenshot capture not supported on: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Capturing screenshot to ${imagePath} using command:\n\t> ${commandForPlatform}`);
|
||||||
|
|
||||||
|
const { error } = await runCommand(commandForPlatform);
|
||||||
|
if (error) {
|
||||||
|
log(`Failed to capture screenshot.\n${error}`, LOG_LEVELS.WARN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(`Captured screenshot to ${imagePath}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScreenshotPowershellScript(imagePath) {
|
||||||
|
return `
|
||||||
|
$ProgressPreference = 'SilentlyContinue' # Do not pollute stderr
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
||||||
|
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap $screenBounds.Width, $screenBounds.Height
|
||||||
|
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$graphics.CopyFromScreen([System.Drawing.Point]::Empty, [System.Drawing.Point]::Empty, $screenBounds.Size)
|
||||||
|
|
||||||
|
$bmp.Save('${imagePath}')
|
||||||
|
$graphics.Dispose()
|
||||||
|
$bmp.Dispose()
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeForPowershell(script) {
|
||||||
|
const buffer = Buffer.from(script, 'utf-16le');
|
||||||
|
return buffer.toString('base64');
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { runCommand } from '../../utils/run-command.js';
|
||||||
|
import { log, LOG_LEVELS } from '../../utils/log.js';
|
||||||
|
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../../utils/platform.js';
|
||||||
|
|
||||||
|
export async function captureWindowTitles(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
|
const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM];
|
||||||
|
if (!captureFunction) {
|
||||||
|
log(`Cannot capture window title, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return captureFunction(processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowTitleCaptureFunctions = {
|
||||||
|
[SUPPORTED_PLATFORMS.MAC]: captureTitlesOnMac,
|
||||||
|
[SUPPORTED_PLATFORMS.LINUX]: captureTitlesOnLinux,
|
||||||
|
[SUPPORTED_PLATFORMS.WINDOWS]: captureTitlesOnWindows,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function captureTitlesOnWindows(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
|
const { stdout: tasklistOutput, error } = await runCommand(
|
||||||
|
`tasklist /FI "PID eq ${processId}" /fo list /v`,
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
log(`Failed to retrieve window title.\n${error}`, LOG_LEVELS.WARN);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const match = tasklistOutput.match(/Window Title:\s*(.*)/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
const title = match[1].trim();
|
||||||
|
if (title === 'N/A') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [title];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureTitlesOnLinux(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
|
const { stdout: windowIdsOutput, error: windowIdError } = await runCommand(
|
||||||
|
`xdotool search --pid '${processId}'`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (windowIdError || !windowIdsOutput) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowIds = windowIdsOutput.trim().split('\n');
|
||||||
|
|
||||||
|
const titles = await Promise.all(windowIds.map(async (windowId) => {
|
||||||
|
const { stdout: titleOutput, error: titleError } = await runCommand(
|
||||||
|
`xprop -id ${windowId} | grep "WM_NAME(STRING)" | cut -d '=' -f 2 | sed 's/^[[:space:]]*"\\(.*\\)"[[:space:]]*$/\\1/'`,
|
||||||
|
);
|
||||||
|
if (titleError || !titleOutput) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return titleOutput.trim();
|
||||||
|
}));
|
||||||
|
|
||||||
|
return titles.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasAssistiveAccessOnMac = true;
|
||||||
|
|
||||||
|
async function captureTitlesOnMac(processId) {
|
||||||
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
if (!hasAssistiveAccessOnMac) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const script = `
|
||||||
|
tell application "System Events"
|
||||||
|
try
|
||||||
|
set targetProcess to first process whose unix id is ${processId}
|
||||||
|
on error
|
||||||
|
return
|
||||||
|
end try
|
||||||
|
tell targetProcess
|
||||||
|
if (count of windows) > 0 then
|
||||||
|
set window_name to name of front window
|
||||||
|
return window_name
|
||||||
|
end if
|
||||||
|
end tell
|
||||||
|
end tell
|
||||||
|
`;
|
||||||
|
const argument = script.trim()
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((line) => `-e '${line.trim()}'`)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
const { stdout: titleOutput, error } = await runCommand(`osascript ${argument}`);
|
||||||
|
if (error) {
|
||||||
|
let errorMessage = '';
|
||||||
|
if (error.includes('-25211')) {
|
||||||
|
errorMessage += 'Capturing window title requires assistive access. You do not have it.\n';
|
||||||
|
hasAssistiveAccessOnMac = false;
|
||||||
|
}
|
||||||
|
errorMessage += error;
|
||||||
|
log(errorMessage, LOG_LEVELS.WARN);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const title = titleOutput?.trim();
|
||||||
|
if (!title) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [title];
|
||||||
|
}
|
||||||
20
scripts/check-desktop-runtime-errors/cli-args.js
Normal file
20
scripts/check-desktop-runtime-errors/cli-args.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
7
scripts/check-desktop-runtime-errors/config.js
Normal file
7
scripts/check-desktop-runtime-errors/config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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');
|
||||||
3
scripts/check-desktop-runtime-errors/index.js
Normal file
3
scripts/check-desktop-runtime-errors/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { main } from './main.js';
|
||||||
|
|
||||||
|
await main();
|
||||||
68
scripts/check-desktop-runtime-errors/main.js
Normal file
68
scripts/check-desktop-runtime-errors/main.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
scripts/check-desktop-runtime-errors/utils/io.js
Normal file
48
scripts/check-desktop-runtime-errors/utils/io.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
39
scripts/check-desktop-runtime-errors/utils/log.js
Normal file
39
scripts/check-desktop-runtime-errors/utils/log.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
87
scripts/check-desktop-runtime-errors/utils/npm.js
Normal file
87
scripts/check-desktop-runtime-errors/utils/npm.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
scripts/check-desktop-runtime-errors/utils/platform.js
Normal file
9
scripts/check-desktop-runtime-errors/utils/platform.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { platform } from 'os';
|
||||||
|
|
||||||
|
export const SUPPORTED_PLATFORMS = {
|
||||||
|
MAC: 'darwin',
|
||||||
|
LINUX: 'linux',
|
||||||
|
WINDOWS: 'win32',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CURRENT_PLATFORM = platform();
|
||||||
44
scripts/check-desktop-runtime-errors/utils/run-command.js
Normal file
44
scripts/check-desktop-runtime-errors/utils/run-command.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { exec } from 'child_process';
|
||||||
|
import { indentText } from './text.js';
|
||||||
|
|
||||||
|
const TIMEOUT_IN_SECONDS = 180;
|
||||||
|
const MAX_OUTPUT_BUFFER_SIZE = 1024 * 1024; // 1 MB
|
||||||
|
|
||||||
|
export function runCommand(commandString, options) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
options = {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
timeout: TIMEOUT_IN_SECONDS * 1000,
|
||||||
|
maxBuffer: MAX_OUTPUT_BUFFER_SIZE * 2,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
exec(commandString, options, (error, stdout, stderr) => {
|
||||||
|
let errorText;
|
||||||
|
if (error || stderr?.length > 0) {
|
||||||
|
errorText = formatError(commandString, error, stdout, stderr);
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
stdout,
|
||||||
|
error: errorText,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(commandString, error, stdout, stderr) {
|
||||||
|
const errorParts = [
|
||||||
|
'Error while running command.',
|
||||||
|
`Command:\n${indentText(commandString, 1)}`,
|
||||||
|
];
|
||||||
|
if (error?.toString().trim()) {
|
||||||
|
errorParts.push(`Error:\n${indentText(error.toString(), 1)}`);
|
||||||
|
}
|
||||||
|
if (stderr?.toString().trim()) {
|
||||||
|
errorParts.push(`stderr:\n${indentText(stderr, 1)}`);
|
||||||
|
}
|
||||||
|
if (stdout?.toString().trim()) {
|
||||||
|
errorParts.push(`stdout:\n${indentText(stdout, 1)}`);
|
||||||
|
}
|
||||||
|
return errorParts.join('\n---\n');
|
||||||
|
}
|
||||||
19
scripts/check-desktop-runtime-errors/utils/text.js
Normal file
19
scripts/check-desktop-runtime-errors/utils/text.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export function indentText(text, indentLevel = 1) {
|
||||||
|
validateText(text);
|
||||||
|
const indentation = '\t'.repeat(indentLevel);
|
||||||
|
return splitTextIntoLines(text)
|
||||||
|
.map((line) => (line ? `${indentation}${line}` : line))
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitTextIntoLines(text) {
|
||||||
|
validateText(text);
|
||||||
|
return text
|
||||||
|
.split(/[\r\n]+/);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateText(text) {
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ class Paths {
|
|||||||
constructor(selfDirectory) {
|
constructor(selfDirectory) {
|
||||||
const projectRoot = resolve(selfDirectory, '../');
|
const projectRoot = resolve(selfDirectory, '../');
|
||||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
this.publicDirectory = join(projectRoot, 'public');
|
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
|||||||
await ensureFolderExists(electronIconsDir);
|
await ensureFolderExists(electronIconsDir);
|
||||||
const temporaryDir = await mkdtemp('icon-');
|
const temporaryDir = await mkdtemp('icon-');
|
||||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by icon-builder
|
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
||||||
await runCommand(
|
await runCommand(
|
||||||
'npx',
|
'npx',
|
||||||
'svgexport',
|
'svgexport',
|
||||||
15
src/TypeHelpers.ts
Normal file
15
src/TypeHelpers.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||||
|
prototype: T;
|
||||||
|
apply: (this: unknown, args: TArgs) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropertyKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
export type ConstructorArguments<T> =
|
||||||
|
T extends new (...args: infer U) => unknown ? U : never;
|
||||||
|
|
||||||
|
export type FunctionKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
|
||||||
|
}[keyof T];
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { Environment } from '../Environment/Environment';
|
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||||
import { IEnvironment } from '../Environment/IEnvironment';
|
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
import { IApplicationFactory } from '../IApplicationFactory';
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
import { ApplicationContext } from './ApplicationContext';
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
|
||||||
import { IEnvironment } from './IEnvironment';
|
|
||||||
|
|
||||||
export interface IEnvironmentVariables {
|
|
||||||
readonly window: Window & typeof globalThis;
|
|
||||||
readonly process: NodeJS.Process;
|
|
||||||
readonly navigator: Navigator;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Environment implements IEnvironment {
|
|
||||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
|
||||||
window,
|
|
||||||
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
|
||||||
navigator,
|
|
||||||
});
|
|
||||||
|
|
||||||
public readonly isDesktop: boolean;
|
|
||||||
|
|
||||||
public readonly os: OperatingSystem;
|
|
||||||
|
|
||||||
protected constructor(
|
|
||||||
variables: IEnvironmentVariables,
|
|
||||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
|
||||||
) {
|
|
||||||
if (!variables) {
|
|
||||||
throw new Error('variables is null or empty');
|
|
||||||
}
|
|
||||||
this.isDesktop = isDesktop(variables);
|
|
||||||
if (this.isDesktop) {
|
|
||||||
this.os = getDesktopOsType(getProcessPlatform(variables));
|
|
||||||
} else {
|
|
||||||
const userAgent = getUserAgent(variables);
|
|
||||||
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserAgent(variables: IEnvironmentVariables): string {
|
|
||||||
if (!variables.window || !variables.window.navigator) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return variables.window.navigator.userAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
|
||||||
if (!variables.process || !variables.process.platform) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return variables.process.platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
|
||||||
// https://nodejs.org/api/process.html#process_process_platform
|
|
||||||
switch (processPlatform) {
|
|
||||||
case 'darwin':
|
|
||||||
return OperatingSystem.macOS;
|
|
||||||
case 'win32':
|
|
||||||
return OperatingSystem.Windows;
|
|
||||||
case 'linux':
|
|
||||||
return OperatingSystem.Linux;
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
|
||||||
// More: https://github.com/electron/electron/issues/2288
|
|
||||||
// Renderer process
|
|
||||||
if (variables.window
|
|
||||||
&& variables.window.process
|
|
||||||
&& variables.window.process.type === 'renderer') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Main process
|
|
||||||
if (variables.process
|
|
||||||
&& variables.process.versions
|
|
||||||
&& Boolean(variables.process.versions.electron)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Detect the user agent when the `nodeIntegration` option is set to true
|
|
||||||
if (variables.navigator
|
|
||||||
&& variables.navigator.userAgent
|
|
||||||
&& variables.navigator.userAgent.includes('Electron')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
export interface IEnvironment {
|
|
||||||
readonly isDesktop: boolean;
|
|
||||||
readonly os: OperatingSystem;
|
|
||||||
}
|
|
||||||
@@ -7,16 +7,19 @@ import MacOsData from '@/application/collections/macos.yaml';
|
|||||||
import LinuxData from '@/application/collections/linux.yaml';
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
|
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||||
|
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
parser = CategoryCollectionParser,
|
categoryParser = parseCategoryCollection,
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
informationParser = parseProjectInformation,
|
||||||
|
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
||||||
collectionsData = PreParsedCollections,
|
collectionsData = PreParsedCollections,
|
||||||
): IApplication {
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData);
|
validateCollectionsData(collectionsData);
|
||||||
const information = parseProjectInformation(processEnv);
|
const information = informationParser(metadata);
|
||||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
const collections = collectionsData.map((collection) => categoryParser(collection, information));
|
||||||
const app = new Application(information, collections);
|
const app = new Application(information, collections);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -24,16 +27,12 @@ export function parseApplication(
|
|||||||
export type CategoryCollectionParserType
|
export type CategoryCollectionParserType
|
||||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||||
|
|
||||||
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
|
||||||
return parseCategoryCollection(file, info);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData [] = [
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
WindowsData, MacOsData, LinuxData,
|
WindowsData, MacOsData, LinuxData,
|
||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections || !collections.length) {
|
if (!collections?.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
if (collections.some((collection) => !collection)) {
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
|
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
||||||
|
import { ConstructorArguments } from '@/TypeHelpers';
|
||||||
|
|
||||||
export function parseProjectInformation(
|
export function
|
||||||
environment: NodeJS.ProcessEnv | VueAppEnvironment,
|
parseProjectInformation(
|
||||||
|
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
||||||
|
createProjectInformation: ProjectInformationFactory = (
|
||||||
|
...args
|
||||||
|
) => new ProjectInformation(...args),
|
||||||
): IProjectInformation {
|
): IProjectInformation {
|
||||||
const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]);
|
const version = new Version(
|
||||||
return new ProjectInformation(
|
metadata.version,
|
||||||
environment[VueAppEnvironmentKeys.VUE_APP_NAME],
|
);
|
||||||
|
return createProjectInformation(
|
||||||
|
metadata.name,
|
||||||
version,
|
version,
|
||||||
environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN],
|
metadata.slogan,
|
||||||
environment[VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL],
|
metadata.repositoryUrl,
|
||||||
environment[VueAppEnvironmentKeys.VUE_APP_HOMEPAGE_URL],
|
metadata.homepageUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VueAppEnvironmentKeys = {
|
export type ProjectInformationFactory = (
|
||||||
VUE_APP_VERSION: 'VUE_APP_VERSION',
|
...args: ConstructorArguments<typeof ProjectInformation>
|
||||||
VUE_APP_NAME: 'VUE_APP_NAME',
|
) => IProjectInformation;
|
||||||
VUE_APP_SLOGAN: 'VUE_APP_SLOGAN',
|
|
||||||
VUE_APP_REPOSITORY_URL: 'VUE_APP_REPOSITORY_URL',
|
|
||||||
VUE_APP_HOMEPAGE_URL: 'VUE_APP_HOMEPAGE_URL',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type VueAppEnvironment = {
|
|
||||||
[K in keyof typeof VueAppEnvironmentKeys]: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4908,7 +4908,7 @@ actions:
|
|||||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "1" /f
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "1" /f
|
||||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "3" /f
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "3" /f
|
||||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /f 2>nul
|
||||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime /f 2>nul
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /f 2>nul
|
||||||
-
|
-
|
||||||
function: DisableService
|
function: DisableService
|
||||||
parameters:
|
parameters:
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import os from 'os';
|
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
import child_process from 'child_process';
|
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export class CodeRunner {
|
export class CodeRunner {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly node = getNodeJs(),
|
|
||||||
private readonly environment = Environment.CurrentEnvironment,
|
private readonly environment = Environment.CurrentEnvironment,
|
||||||
) {
|
) {
|
||||||
|
if (!environment.system) {
|
||||||
|
throw new Error('missing system operations');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||||
const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
|
const { system } = this.environment;
|
||||||
await this.node.fs.promises.mkdir(dir, { recursive: true });
|
const dir = system.location.combinePaths(
|
||||||
const filePath = this.node.path.join(dir, `run.${fileExtension}`);
|
system.operatingSystem.getTempDirectory(),
|
||||||
await this.node.fs.promises.writeFile(filePath, code);
|
folderName,
|
||||||
await this.node.fs.promises.chmod(filePath, '755');
|
);
|
||||||
|
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);
|
const command = getExecuteCommand(filePath, this.environment);
|
||||||
this.node.child_process.exec(command);
|
system.command.execute(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,43 +40,3 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
|
|||||||
throw Error(`unsupported os: ${OperatingSystem[environment.os]}`);
|
throw Error(`unsupported os: ${OperatingSystem[environment.os]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeJs(): INodeJs {
|
|
||||||
return {
|
|
||||||
os, path, fs, child_process,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodeJs {
|
|
||||||
os: INodeOs;
|
|
||||||
path: INodePath;
|
|
||||||
fs: INodeFs;
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
child_process: INodeChildProcess;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodeOs {
|
|
||||||
tmpdir(): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodePath {
|
|
||||||
join(...paths: string[]): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodeChildProcess {
|
|
||||||
exec(command: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodeFs {
|
|
||||||
readonly promises: INodeFsPromises;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface INodeFsPromisesMakeDirectoryOptions {
|
|
||||||
recursive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface INodeFsPromises { // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v13/fs.d.ts
|
|
||||||
chmod(path: string, mode: string | number): Promise<void>;
|
|
||||||
mkdir(path: string, options: INodeFsPromisesMakeDirectoryOptions): Promise<string>;
|
|
||||||
writeFile(path: string, data: string): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|||||||
49
src/infrastructure/Environment/Environment.ts
Normal file
49
src/infrastructure/Environment/Environment.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
||||||
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
|
import { IEnvironment } from './IEnvironment';
|
||||||
|
import { WindowVariables } from './WindowVariables';
|
||||||
|
import { validateWindowVariables } from './WindowVariablesValidator';
|
||||||
|
|
||||||
|
export class Environment implements IEnvironment {
|
||||||
|
public static readonly CurrentEnvironment: IEnvironment = new Environment(window);
|
||||||
|
|
||||||
|
public readonly isDesktop: boolean;
|
||||||
|
|
||||||
|
public readonly os: OperatingSystem | undefined;
|
||||||
|
|
||||||
|
public readonly system: ISystemOperations | undefined;
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
window: Partial<Window>,
|
||||||
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||||
|
windowValidator: WindowValidator = validateWindowVariables,
|
||||||
|
) {
|
||||||
|
if (!window) {
|
||||||
|
throw new Error('missing window');
|
||||||
|
}
|
||||||
|
windowValidator(window);
|
||||||
|
this.isDesktop = isDesktop(window);
|
||||||
|
if (this.isDesktop) {
|
||||||
|
this.os = window?.os;
|
||||||
|
} else {
|
||||||
|
this.os = undefined;
|
||||||
|
const userAgent = getUserAgent(window);
|
||||||
|
if (userAgent) {
|
||||||
|
this.os = browserOsDetector.detect(userAgent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.system = window?.system;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserAgent(window: Partial<Window>): string {
|
||||||
|
return window?.navigator?.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDesktop(window: Partial<WindowVariables>): boolean {
|
||||||
|
return window?.isDesktop === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WindowValidator = typeof validateWindowVariables;
|
||||||
8
src/infrastructure/Environment/IEnvironment.ts
Normal file
8
src/infrastructure/Environment/IEnvironment.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export interface ISystemOperations {
|
||||||
|
readonly operatingSystem: IOperatingSystemOps;
|
||||||
|
readonly location: ILocationOps;
|
||||||
|
readonly fileSystem: IFileSystemOps;
|
||||||
|
readonly command: ICommandOps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOperatingSystemOps {
|
||||||
|
getTempDirectory(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILocationOps {
|
||||||
|
combinePaths(...pathSegments: string[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICommandOps {
|
||||||
|
execute(command: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFileSystemOps {
|
||||||
|
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
||||||
|
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<string>;
|
||||||
|
writeToFile(filePath: string, data: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { chmod, mkdir, writeFile } from 'fs/promises';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { ISystemOperations } from './ISystemOperations';
|
||||||
|
|
||||||
|
export function createNodeSystemOperations(): ISystemOperations {
|
||||||
|
return {
|
||||||
|
operatingSystem: {
|
||||||
|
getTempDirectory: () => tmpdir(),
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
combinePaths: (...pathSegments) => join(...pathSegments),
|
||||||
|
},
|
||||||
|
fileSystem: {
|
||||||
|
setFilePermissions: (
|
||||||
|
filePath: string,
|
||||||
|
mode: string | number,
|
||||||
|
) => chmod(filePath, mode),
|
||||||
|
createDirectory: (
|
||||||
|
directoryPath: string,
|
||||||
|
isRecursive?: boolean,
|
||||||
|
) => mkdir(directoryPath, { recursive: isRecursive }),
|
||||||
|
writeToFile: (
|
||||||
|
filePath: string,
|
||||||
|
data: string,
|
||||||
|
) => writeFile(filePath, data),
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
execute: (command) => exec(command),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
13
src/infrastructure/Environment/WindowVariables.ts
Normal file
13
src/infrastructure/Environment/WindowVariables.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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 { }
|
||||||
|
}
|
||||||
76
src/infrastructure/Environment/WindowVariablesValidator.ts
Normal file
76
src/infrastructure/Environment/WindowVariablesValidator.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { PropertyKeys } from '@/TypeHelpers';
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
const errors = [...testEveryProperty(variables)];
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(errors.join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
|
||||||
|
const tests: {
|
||||||
|
[K in PropertyKeys<WindowVariables>]: boolean;
|
||||||
|
} = {
|
||||||
|
os: testOperatingSystem(variables.os),
|
||||||
|
isDesktop: testIsDesktop(variables.isDesktop),
|
||||||
|
system: testSystem(variables),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [propertyName, testResult] of Object.entries(tests)) {
|
||||||
|
if (!testResult) {
|
||||||
|
const propertyValue = variables[propertyName as keyof WindowVariables];
|
||||||
|
yield `Unexpected ${propertyName} (${typeof propertyValue})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testOperatingSystem(os: unknown): boolean {
|
||||||
|
if (os === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!isNumber(os)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object
|
||||||
|
.values(OperatingSystem)
|
||||||
|
.includes(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSystem(variables: Partial<WindowVariables>): boolean {
|
||||||
|
if (!variables.isDesktop) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return variables.system !== undefined && isObject(variables.system);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testIsDesktop(isDesktop: unknown): boolean {
|
||||||
|
if (isDesktop === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isBoolean(isDesktop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumber(variable: unknown): variable is number {
|
||||||
|
return typeof variable === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBoolean(variable: unknown): variable is boolean {
|
||||||
|
return typeof variable === 'boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(variable: unknown): variable is object {
|
||||||
|
return typeof variable === 'object'
|
||||||
|
&& variable !== null // the data type of null is an object
|
||||||
|
&& !Array.isArray(variable);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IEventSubscription } from './IEventSource';
|
import { IEventSubscription } from './IEventSource';
|
||||||
|
import { IEventSubscriptionCollection } from './IEventSubscriptionCollection';
|
||||||
|
|
||||||
export class EventSubscriptionCollection {
|
export class EventSubscriptionCollection implements IEventSubscriptionCollection {
|
||||||
private readonly subscriptions = new Array<IEventSubscription>();
|
private readonly subscriptions = new Array<IEventSubscription>();
|
||||||
|
|
||||||
public register(...subscriptions: IEventSubscription[]) {
|
public register(...subscriptions: IEventSubscription[]) {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
|
|
||||||
|
export interface IEventSubscriptionCollection {
|
||||||
|
register(...subscriptions: IEventSubscription[]);
|
||||||
|
|
||||||
|
unsubscribeAll();
|
||||||
|
}
|
||||||
18
src/infrastructure/Metadata/AppMetadataFactory.ts
Normal file
18
src/infrastructure/Metadata/AppMetadataFactory.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
13
src/infrastructure/Metadata/IAppMetadata.ts
Normal file
13
src/infrastructure/Metadata/IAppMetadata.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
readonly name: string;
|
||||||
|
readonly slogan: string;
|
||||||
|
readonly repositoryUrl: string;
|
||||||
|
readonly homepageUrl: string;
|
||||||
|
}
|
||||||
5
src/infrastructure/Metadata/IAppMetadataFactory.ts
Normal file
5
src/infrastructure/Metadata/IAppMetadataFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { IAppMetadata } from './IAppMetadata';
|
||||||
|
|
||||||
|
export interface IAppMetadataFactory {
|
||||||
|
readonly instance: IAppMetadata;
|
||||||
|
}
|
||||||
50
src/infrastructure/Metadata/MetadataValidator.ts
Normal file
50
src/infrastructure/Metadata/MetadataValidator.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
||||||
|
|
||||||
|
/* Validation is externalized to keep the environment objects simple */
|
||||||
|
export function validateMetadata(metadata: IAppMetadata): void {
|
||||||
|
if (!metadata) {
|
||||||
|
throw new Error('missing metadata');
|
||||||
|
}
|
||||||
|
const keyValues = capturePropertyValues(metadata);
|
||||||
|
if (!Object.keys(keyValues).length) {
|
||||||
|
throw new Error('Unable to capture metadata key/value pairs');
|
||||||
|
}
|
||||||
|
const keysMissingValue = getMissingMetadataKeys(keyValues);
|
||||||
|
if (keysMissingValue.length > 0) {
|
||||||
|
throw new Error(`Metadata keys missing: ${keysMissingValue.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMissingMetadataKeys(keyValuePairs: Record<string, unknown>): string[] {
|
||||||
|
return Object.entries(keyValuePairs)
|
||||||
|
.reduce((acc, [key, value]) => {
|
||||||
|
if (!value) {
|
||||||
|
acc.push(key);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, new Array<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures values of properties and getters from the provided instance.
|
||||||
|
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
||||||
|
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
||||||
|
*/
|
||||||
|
function capturePropertyValues(instance: unknown): Record<string, unknown> {
|
||||||
|
const obj: Record<string, unknown> = {};
|
||||||
|
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
||||||
|
|
||||||
|
// Capture regular properties from the instance
|
||||||
|
for (const [key, value] of Object.entries(instance)) {
|
||||||
|
obj[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture getter properties from the instance's prototype
|
||||||
|
for (const [key, descriptor] of Object.entries(descriptors)) {
|
||||||
|
if (typeof descriptor.get === 'function') {
|
||||||
|
obj[key] = descriptor.get.call(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
29
src/infrastructure/Metadata/Vite/ViteAppMetadata.ts
Normal file
29
src/infrastructure/Metadata/Vite/ViteAppMetadata.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { IAppMetadata } from '../IAppMetadata';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the application's metadata using Vite's environment variables.
|
||||||
|
*/
|
||||||
|
export class ViteAppMetadata implements IAppMetadata {
|
||||||
|
// Ensure the use of import.meta.env prefix for the following properties.
|
||||||
|
// Vue will replace these statically during production builds.
|
||||||
|
|
||||||
|
public get version(): string {
|
||||||
|
return import.meta.env.VITE_APP_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get name(): string {
|
||||||
|
return import.meta.env.VITE_APP_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get slogan(): string {
|
||||||
|
return import.meta.env.VITE_APP_SLOGAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get repositoryUrl(): string {
|
||||||
|
return import.meta.env.VITE_APP_REPOSITORY_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get homepageUrl(): string {
|
||||||
|
return import.meta.env.VITE_APP_HOMEPAGE_URL;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/infrastructure/Metadata/Vite/ViteEnvironmentKeys.ts
Normal file
8
src/infrastructure/Metadata/Vite/ViteEnvironmentKeys.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Only variables prefixed with VITE_ are exposed to Vite-processed code
|
||||||
|
export const VITE_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;
|
||||||
11
src/infrastructure/Metadata/Vite/vite-env.d.ts
vendored
Normal file
11
src/infrastructure/Metadata/Vite/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
import { VITE_ENVIRONMENT_KEYS } from './ViteEnvironmentKeys';
|
||||||
|
|
||||||
|
export type ViteEnvironmentVariables = {
|
||||||
|
readonly [K in keyof typeof VITE_ENVIRONMENT_KEYS]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ViteEnvironmentVariables
|
||||||
|
}
|
||||||
31
src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts
Normal file
31
src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||||
|
import { ISanityValidator } from './ISanityValidator';
|
||||||
|
|
||||||
|
export type FactoryFunction<T> = () => T;
|
||||||
|
|
||||||
|
export abstract class FactoryValidator<T> implements ISanityValidator {
|
||||||
|
private readonly factory: FactoryFunction<T>;
|
||||||
|
|
||||||
|
protected constructor(factory: FactoryFunction<T>) {
|
||||||
|
if (!factory) {
|
||||||
|
throw new Error('missing factory');
|
||||||
|
}
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract shouldValidate(options: ISanityCheckOptions): boolean;
|
||||||
|
|
||||||
|
public abstract name: string;
|
||||||
|
|
||||||
|
public* collectErrors(): Iterable<string> {
|
||||||
|
try {
|
||||||
|
const value = this.factory();
|
||||||
|
if (!value) {
|
||||||
|
// Do not remove this check, it ensures that the factory call is not optimized away.
|
||||||
|
yield 'Factory resulted in a falsy value';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
yield `Error in factory creation: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ISanityCheckOptions {
|
||||||
|
readonly validateMetadata: boolean;
|
||||||
|
readonly validateEnvironment: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ISanityCheckOptions } from './ISanityCheckOptions';
|
||||||
|
|
||||||
|
export interface ISanityValidator {
|
||||||
|
readonly name: string;
|
||||||
|
shouldValidate(options: ISanityCheckOptions): boolean;
|
||||||
|
collectErrors(): Iterable<string>;
|
||||||
|
}
|
||||||
50
src/infrastructure/RuntimeSanity/SanityChecks.ts
Normal file
50
src/infrastructure/RuntimeSanity/SanityChecks.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
||||||
|
import { ISanityValidator } from './Common/ISanityValidator';
|
||||||
|
import { MetadataValidator } from './Validators/MetadataValidator';
|
||||||
|
|
||||||
|
const DefaultSanityValidators: ISanityValidator[] = [
|
||||||
|
new MetadataValidator(),
|
||||||
|
];
|
||||||
|
|
||||||
|
/* Helps to fail-fast on errors */
|
||||||
|
export function validateRuntimeSanity(
|
||||||
|
options: ISanityCheckOptions,
|
||||||
|
validators: readonly ISanityValidator[] = DefaultSanityValidators,
|
||||||
|
): void {
|
||||||
|
validateContext(options, validators);
|
||||||
|
const errorMessages = validators.reduce((errors, validator) => {
|
||||||
|
if (validator.shouldValidate(options)) {
|
||||||
|
const errorMessage = getErrorMessage(validator);
|
||||||
|
if (errorMessage) {
|
||||||
|
errors.push(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}, new Array<string>());
|
||||||
|
if (errorMessages.length > 0) {
|
||||||
|
throw new Error(`Sanity check failed.\n${errorMessages.join('\n---\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateContext(
|
||||||
|
options: ISanityCheckOptions,
|
||||||
|
validators: readonly ISanityValidator[],
|
||||||
|
) {
|
||||||
|
if (!options) {
|
||||||
|
throw new Error('missing options');
|
||||||
|
}
|
||||||
|
if (!validators?.length) {
|
||||||
|
throw new Error('missing validators');
|
||||||
|
}
|
||||||
|
if (validators.some((validator) => !validator)) {
|
||||||
|
throw new Error('missing validator in validators');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(validator: ISanityValidator): string | undefined {
|
||||||
|
const errorMessages = [...validator.collectErrors()];
|
||||||
|
if (!errorMessages.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `${validator.name}:\n${errorMessages.join('\n')}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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,16 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/presentation/README.md
Normal file
3
src/presentation/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# presentation
|
||||||
|
|
||||||
|
See [`presentation.md`](./../../docs/presentation.md)
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
// https://google-webfonts-helper.herokuapp.com/fonts
|
// https://google-webfonts-helper.herokuapp.com/fonts
|
||||||
|
|
||||||
|
@use "@/presentation/assets/styles/vite-path" as *;
|
||||||
|
|
||||||
/* slabo-27px-regular - latin-ext_latin */
|
/* slabo-27px-regular - latin-ext_latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Slabo 27px';
|
font-family: 'Slabo 27px';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.eot'); /* IE9 Compat Modes */
|
src: url('#{$base-assets-path}/fonts/slabo-27px-v6-latin-ext_latin-regular.eot'); /* IE9 Compat Modes */
|
||||||
src: local('Slabo 27px'), local('Slabo27px-Regular'),
|
src: local('Slabo 27px'), local('Slabo27px-Regular'),
|
||||||
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
url('#{$base-assets-path}/fonts/slabo-27px-v6-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||||
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
url('#{$base-assets-path}/fonts/slabo-27px-v6-latin-ext_latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||||
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.woff') format('woff'), /* Modern Browsers */
|
url('#{$base-assets-path}/fonts/slabo-27px-v6-latin-ext_latin-regular.woff') format('woff'), /* Modern Browsers */
|
||||||
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
url('#{$base-assets-path}/fonts/slabo-27px-v6-latin-ext_latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||||
url('~@/presentation/assets/fonts/slabo-27px-v6-latin-ext_latin-regular.svg#Slabo27px') format('svg'); /* Legacy iOS */
|
url('#{$base-assets-path}/fonts/slabo-27px-v6-latin-ext_latin-regular.svg#Slabo27px') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* yesteryear-regular - latin */
|
/* yesteryear-regular - latin */
|
||||||
@@ -19,13 +21,13 @@
|
|||||||
font-family: 'Yesteryear';
|
font-family: 'Yesteryear';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.eot'); /* IE9 Compat Modes */
|
src: url('#{$base-assets-path}/fonts/yesteryear-v8-latin-regular.eot'); /* IE9 Compat Modes */
|
||||||
src: local('Yesteryear'), local('Yesteryear-Regular'),
|
src: local('Yesteryear'), local('Yesteryear-Regular'),
|
||||||
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
url('#{$base-assets-path}/fonts/yesteryear-v8-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||||
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
url('#{$base-assets-path}/fonts/yesteryear-v8-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||||
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.woff') format('woff'), /* Modern Browsers */
|
url('#{$base-assets-path}/fonts/yesteryear-v8-latin-regular.woff') format('woff'), /* Modern Browsers */
|
||||||
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
url('#{$base-assets-path}/fonts/yesteryear-v8-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||||
url('~@/presentation/assets/fonts/yesteryear-v8-latin-regular.svg#Yesteryear') format('svg'); /* Legacy iOS */
|
url('#{$base-assets-path}/fonts/yesteryear-v8-latin-regular.svg#Yesteryear') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
|
|
||||||
$font-normal : 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
$font-normal : 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@use "@/presentation/assets/styles/colors" as *;
|
@use "@/presentation/assets/styles/colors" as *;
|
||||||
@use "@/presentation/assets/styles/fonts" as *;
|
@use "@/presentation/assets/styles/fonts" as *;
|
||||||
@use "@/presentation/assets/styles/mixins" as *;
|
@use "@/presentation/assets/styles/mixins" as *;
|
||||||
|
@use "@/presentation/assets/styles/vite-path" as *;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
4
src/presentation/assets/styles/_vite-path.scss
Normal file
4
src/presentation/assets/styles/_vite-path.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Define paths specific to Vite's resolution system.
|
||||||
|
// Vite uses the "@" symbol to resolve its aliases for styles.
|
||||||
|
|
||||||
|
$base-assets-path: "@/presentation/assets/";
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
useCollectionStateKey, useApplicationKey, useEnvironmentKey,
|
useCollectionStateKey, useApplicationKey, useEnvironmentKey,
|
||||||
} from '@/presentation/injectionSymbols';
|
} from '@/presentation/injectionSymbols';
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||||
|
|
||||||
export function provideDependencies(context: IApplicationContext) {
|
export function provideDependencies(context: IApplicationContext) {
|
||||||
registerSingleton(useApplicationKey, useApplication(context.app));
|
registerSingleton(useApplicationKey, useApplication(context.app));
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeBu
|
|||||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||||
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
||||||
|
|
||||||
const singletonAppContext = await buildContext();
|
const singletonAppContext = await buildContext();
|
||||||
@@ -32,6 +33,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
||||||
|
validateRuntimeSanity({
|
||||||
|
validateMetadata: true,
|
||||||
|
validateEnvironment: true,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { useCollectionStateKey, useEnvironmentKey } from '@/presentation/injecti
|
|||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/infrastructure/Environment/Environment';
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ace from 'ace-builds';
|
import ace from 'ace-builds';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Following is here because `import 'ace-builds/webpack-resolver';` does not work with webpack 5.
|
Following is here because `import 'ace-builds/esm-resolver' imports all unused functionality
|
||||||
Related issue: https://github.com/ajaxorg/ace-builds/issues/211, PR: https://github.com/ajaxorg/ace-builds/pull/221
|
when built with Vite (`npm run build`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'ace-builds/src-noconflict/theme-github';
|
import 'ace-builds/src-noconflict/theme-github';
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ declare module 'liquor-tree' {
|
|||||||
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
|
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LiquorTree: PluginObject<Vue>;
|
interface LiquorTreeVueComponent extends PluginObject<Vue> {
|
||||||
|
install(Vue: VueConstructor<Vue>, options?: unknown);
|
||||||
|
}
|
||||||
export default LiquorTree;
|
export default LiquorTree;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ $text-size: 0.75em; // Lower looks bad on Firefox
|
|||||||
Use mask element instead of content/background-image etc.
|
Use mask element instead of content/background-image etc.
|
||||||
This way we can apply current font color to it to match the theme
|
This way we can apply current font color to it to match the theme
|
||||||
*/
|
*/
|
||||||
mask: url(~@/presentation/assets/icons/external-link.svg) no-repeat 50% 50%;
|
mask: url(@/presentation/assets/icons/external-link.svg) no-repeat 50% 50%;
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
content: '';
|
content: '';
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
@node:unchecked="nodeSelected($event)"
|
@node:unchecked="nodeSelected($event)"
|
||||||
ref="liquorTree"
|
ref="liquorTree"
|
||||||
>
|
>
|
||||||
<span class="tree-text" slot-scope="{ node }">
|
<template v-slot:default="{ node }">
|
||||||
<NodeContent :data="convertExistingToNode(node)" />
|
<span class="tree-text">
|
||||||
</span>
|
<NodeContent :data="convertExistingToNode(node)" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</LiquorTree>
|
</LiquorTree>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>Nooo 😢</span>
|
<span v-else>Nooo 😢</span>
|
||||||
@@ -192,4 +194,3 @@ async function tryUntilDefined<T>(
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
./Node/INodeContent
|
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scripts">
|
<div class="scripts">
|
||||||
<div v-if="!isSearching">
|
<div v-if="!isSearching">
|
||||||
<CardList v-if="currentView === ViewType.Cards" />
|
<template v-if="currentView === ViewType.Cards">
|
||||||
<div class="tree" v-else-if="currentView === ViewType.Tree">
|
<CardList />
|
||||||
<ScriptsTree />
|
</template>
|
||||||
</div>
|
<template v-else-if="currentView === ViewType.Tree">
|
||||||
|
<div class="tree">
|
||||||
|
<ScriptsTree />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else> <!-- Searching -->
|
<div v-else> <!-- Searching -->
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<div class="search__query">
|
<div class="search__query">
|
||||||
<div>Searching for "{{ trimmedSearchQuery }}"</div>
|
<div>Searching for "{{ trimmedSearchQuery }}"</div>
|
||||||
<div class="search__query__close-button">
|
<div
|
||||||
<font-awesome-icon
|
class="search__query__close-button"
|
||||||
:icon="['fas', 'times']"
|
v-on:click="clearSearchQuery()"
|
||||||
v-on:click="clearSearchQuery()" />
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'times']" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!searchHasMatches" class="search-no-matches">
|
<div v-if="!searchHasMatches" class="search-no-matches">
|
||||||
@@ -41,6 +46,7 @@ import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/Scri
|
|||||||
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
||||||
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
|
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
|
||||||
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
||||||
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -58,8 +64,8 @@ export default defineComponent({
|
|||||||
const { info } = inject(useApplicationKey);
|
const { info } = inject(useApplicationKey);
|
||||||
|
|
||||||
const repositoryUrl = computed<string>(() => info.repositoryWebUrl);
|
const repositoryUrl = computed<string>(() => info.repositoryWebUrl);
|
||||||
const searchQuery = ref<string>();
|
const searchQuery = ref<string | undefined>();
|
||||||
const isSearching = ref(false);
|
const isSearching = computed(() => Boolean(searchQuery.value));
|
||||||
const searchHasMatches = ref(false);
|
const searchHasMatches = ref(false);
|
||||||
const trimmedSearchQuery = computed(() => {
|
const trimmedSearchQuery = computed(() => {
|
||||||
const query = searchQuery.value;
|
const query = searchQuery.value;
|
||||||
@@ -72,8 +78,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
onStateChange((newState) => {
|
onStateChange((newState) => {
|
||||||
events.unsubscribeAll();
|
events.unsubscribeAll();
|
||||||
|
updateFromInitialFilter(newState.filter.currentFilter);
|
||||||
subscribeToFilterChanges(newState.filter);
|
subscribeToFilterChanges(newState.filter);
|
||||||
});
|
}, { immediate: true });
|
||||||
|
|
||||||
function clearSearchQuery() {
|
function clearSearchQuery() {
|
||||||
modifyCurrentState((state) => {
|
modifyCurrentState((state) => {
|
||||||
@@ -82,17 +89,21 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateFromInitialFilter(filter?: IFilterResult) {
|
||||||
|
searchQuery.value = filter?.query;
|
||||||
|
searchHasMatches.value = filter?.hasAnyMatches();
|
||||||
|
}
|
||||||
|
|
||||||
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
||||||
events.register(
|
events.register(
|
||||||
filter.filterChanged.on((event) => {
|
filter.filterChanged.on((event) => {
|
||||||
event.visit({
|
event.visit({
|
||||||
onApply: (newFilter) => {
|
onApply: (newFilter) => {
|
||||||
searchQuery.value = newFilter.query;
|
searchQuery.value = newFilter.query;
|
||||||
isSearching.value = true;
|
|
||||||
searchHasMatches.value = newFilter.hasAnyMatches();
|
searchHasMatches.value = newFilter.hasAnyMatches();
|
||||||
},
|
},
|
||||||
onClear: () => {
|
onClear: () => {
|
||||||
isSearching.value = false;
|
searchQuery.value = undefined;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ref, computed, readonly } from 'vue';
|
|||||||
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
|
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
|
||||||
|
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||||
|
|
||||||
export function useCollectionState(context: IApplicationContext) {
|
export function useCollectionState(context: IApplicationContext) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -18,13 +19,6 @@ export function useCollectionState(context: IApplicationContext) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
type NewStateEventHandler = (
|
|
||||||
newState: IReadOnlyCategoryCollectionState,
|
|
||||||
oldState: IReadOnlyCategoryCollectionState | undefined,
|
|
||||||
) => void;
|
|
||||||
interface IStateCallbackSettings {
|
|
||||||
readonly immediate: boolean;
|
|
||||||
}
|
|
||||||
const defaultSettings: IStateCallbackSettings = {
|
const defaultSettings: IStateCallbackSettings = {
|
||||||
immediate: false,
|
immediate: false,
|
||||||
};
|
};
|
||||||
@@ -49,9 +43,6 @@ export function useCollectionState(context: IApplicationContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateModifier = (
|
|
||||||
state: ICategoryCollectionState,
|
|
||||||
) => void;
|
|
||||||
function modifyCurrentState(mutator: StateModifier) {
|
function modifyCurrentState(mutator: StateModifier) {
|
||||||
if (!mutator) {
|
if (!mutator) {
|
||||||
throw new Error('missing state mutator');
|
throw new Error('missing state mutator');
|
||||||
@@ -59,9 +50,6 @@ export function useCollectionState(context: IApplicationContext) {
|
|||||||
mutator(context.state);
|
mutator(context.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextModifier = (
|
|
||||||
state: IApplicationContext,
|
|
||||||
) => void;
|
|
||||||
function modifyCurrentContext(mutator: ContextModifier) {
|
function modifyCurrentContext(mutator: ContextModifier) {
|
||||||
if (!mutator) {
|
if (!mutator) {
|
||||||
throw new Error('missing context mutator');
|
throw new Error('missing context mutator');
|
||||||
@@ -75,6 +63,23 @@ export function useCollectionState(context: IApplicationContext) {
|
|||||||
onStateChange,
|
onStateChange,
|
||||||
currentContext: context as IReadOnlyApplicationContext,
|
currentContext: context as IReadOnlyApplicationContext,
|
||||||
currentState: readonly(computed<IReadOnlyCategoryCollectionState>(() => currentState.value)),
|
currentState: readonly(computed<IReadOnlyCategoryCollectionState>(() => currentState.value)),
|
||||||
events,
|
events: events as IEventSubscriptionCollection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NewStateEventHandler = (
|
||||||
|
newState: IReadOnlyCategoryCollectionState,
|
||||||
|
oldState: IReadOnlyCategoryCollectionState | undefined,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export interface IStateCallbackSettings {
|
||||||
|
readonly immediate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StateModifier = (
|
||||||
|
state: ICategoryCollectionState,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type ContextModifier = (
|
||||||
|
state: IApplicationContext,
|
||||||
|
) => void;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IEnvironment } from '@/application/Environment/IEnvironment';
|
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
||||||
|
|
||||||
export function useEnvironment(environment: IEnvironment) {
|
export function useEnvironment(environment: IEnvironment) {
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
|
|||||||
16
src/presentation/electron/main/ElectronConfig.ts
Normal file
16
src/presentation/electron/main/ElectronConfig.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Abstraction for electron-vite specific logic and other Electron CLI helpers/wrappers.
|
||||||
|
* Allows for agnostic application design and centralizes adjustments when switching wrappers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="electron-vite/node" />
|
||||||
|
import { join } from 'path';
|
||||||
|
import appIcon from '@/presentation/public/icon.png?asset';
|
||||||
|
|
||||||
|
export const APP_ICON_PATH = appIcon;
|
||||||
|
|
||||||
|
export const RENDERER_URL = process.env.ELECTRON_RENDERER_URL;
|
||||||
|
|
||||||
|
export const RENDERER_HTML_PATH = join('file://', __dirname, '../renderer/index.html');
|
||||||
|
|
||||||
|
export const PRELOADER_SCRIPT_PATH = join(__dirname, '../preload/index.cjs');
|
||||||
@@ -6,6 +6,7 @@ import log from 'electron-log';
|
|||||||
import fetch from 'cross-fetch';
|
import fetch from 'cross-fetch';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ViteAppMetadata } from '@/infrastructure/Metadata/Vite/ViteAppMetadata';
|
||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { UpdateProgressBar } from './UpdateProgressBar';
|
import { UpdateProgressBar } from './UpdateProgressBar';
|
||||||
@@ -28,7 +29,7 @@ export async function handleManualUpdate(info: UpdateInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTargetProject(targetVersion: string) {
|
function getTargetProject(targetVersion: string) {
|
||||||
const existingProject = parseProjectInformation(process.env);
|
const existingProject = parseProjectInformation(new ViteAppMetadata());
|
||||||
const targetProject = new ProjectInformation(
|
const targetProject = new ProjectInformation(
|
||||||
existingProject.name,
|
existingProject.name,
|
||||||
new Version(targetVersion),
|
new Version(targetVersion),
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
// This is main process of Electron, started as first thing when app starts.
|
// Initializes Electron's main process, always runs in the background, and manages the main window.
|
||||||
// This script is running through entire life of the application.
|
|
||||||
// It doesn't have any windows which you can see on screen, opens the main window from here.
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import {
|
import {
|
||||||
app, protocol, BrowserWindow, shell, screen,
|
app, protocol, BrowserWindow, shell, screen,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
|
||||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
import { setupAutoUpdater } from './Update/Updater';
|
import { setupAutoUpdater } from './Update/Updater';
|
||||||
|
import {
|
||||||
|
APP_ICON_PATH, PRELOADER_SCRIPT_PATH, RENDERER_HTML_PATH, RENDERER_URL,
|
||||||
|
} from './ElectronConfig';
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
const isDevelopment = !app.isPackaged;
|
||||||
|
|
||||||
// Path of static assets, magic variable populated by electron
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle
|
|
||||||
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
|
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
@@ -26,10 +22,15 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
{ scheme: 'app', privileges: { secure: true, standard: true } },
|
{ scheme: 'app', privileges: { secure: true, standard: true } },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
log.transports.file.level = 'silly';
|
setupLogger();
|
||||||
if (!process.env.IS_TEST) {
|
validateRuntimeSanity({
|
||||||
Object.assign(console, log.functions); // override console.log, console.warn etc.
|
// Metadata is used by manual updates.
|
||||||
}
|
validateMetadata: true,
|
||||||
|
|
||||||
|
// Environment is populated by the preload script and is in the renderer's context;
|
||||||
|
// it's not directly accessible from the main process.
|
||||||
|
validateEnvironment: false,
|
||||||
|
});
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
@@ -38,14 +39,11 @@ function createWindow() {
|
|||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
contextIsolation: false, // To reach node https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1285
|
nodeIntegration: true, // disabling does not work with electron-vite, https://electron-vite.org/guide/dev.html#nodeintegration
|
||||||
// Use pluginOptions.nodeIntegration, leave this alone
|
contextIsolation: true,
|
||||||
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
|
preload: PRELOADER_SCRIPT_PATH,
|
||||||
nodeIntegration: (process.env
|
|
||||||
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
|
|
||||||
},
|
},
|
||||||
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
|
icon: APP_ICON_PATH,
|
||||||
icon: path.join(__static, 'icon.png'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
win.setMenuBarVisibility(false);
|
win.setMenuBarVisibility(false);
|
||||||
@@ -86,17 +84,12 @@ app.on('activate', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
|
||||||
// initialization and is ready to create browser windows.
|
|
||||||
// Some APIs can only be used after this event occurs.
|
|
||||||
app.on('ready', async () => {
|
app.on('ready', async () => {
|
||||||
if (isDevelopment && !process.env.IS_TEST) {
|
if (isDevelopment) {
|
||||||
// Install Vue Devtools
|
|
||||||
try {
|
try {
|
||||||
await installExtension(VUEJS_DEVTOOLS);
|
await installExtension(VUEJS_DEVTOOLS);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// eslint-disable-next-line no-console
|
log.error('Vue Devtools failed to install:', e.toString());
|
||||||
console.error('Vue Devtools failed to install:', e.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createWindow();
|
createWindow();
|
||||||
@@ -118,19 +111,19 @@ if (isDevelopment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadApplication(window: BrowserWindow) {
|
function loadApplication(window: BrowserWindow) {
|
||||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
if (RENDERER_URL) { // Populated in a dev server during development
|
||||||
// Load the url of the dev server if in development mode
|
loadUrlWithNodeWorkaround(win, RENDERER_URL);
|
||||||
loadUrlWithNodeWorkaround(win, process.env.WEBPACK_DEV_SERVER_URL as string);
|
} else {
|
||||||
if (!process.env.IS_TEST) {
|
loadUrlWithNodeWorkaround(win, RENDERER_HTML_PATH);
|
||||||
window.webContents.openDevTools();
|
}
|
||||||
}
|
if (isDevelopment) {
|
||||||
|
window.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
createProtocol('app');
|
|
||||||
// Load the index.html when not in development
|
|
||||||
loadUrlWithNodeWorkaround(win, 'app://./index.html');
|
|
||||||
const updater = setupAutoUpdater();
|
const updater = setupAutoUpdater();
|
||||||
updater.checkForUpdates();
|
updater.checkForUpdates();
|
||||||
}
|
}
|
||||||
|
// Do not remove [WINDOW_INIT]; it's a marker used in tests.
|
||||||
|
log.info('[WINDOW_INIT] Main window initialized and content loading.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
|
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
|
||||||
@@ -155,3 +148,10 @@ function getWindowSize(idealWidth: number, idealHeight: number) {
|
|||||||
height = Math.min(height, idealHeight);
|
height = Math.min(height, idealHeight);
|
||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupLogger(): void {
|
||||||
|
log.transports.file.level = 'silly';
|
||||||
|
if (!isDevelopment) {
|
||||||
|
Object.assign(console, log.functions); // override console.log, console.warn etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/presentation/electron/preload/NodeOsMapper.ts
Normal file
14
src/presentation/electron/preload/NodeOsMapper.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export function convertPlatformToOs(platform: NodeJS.Platform): OperatingSystem | undefined {
|
||||||
|
switch (platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return OperatingSystem.macOS;
|
||||||
|
case 'win32':
|
||||||
|
return OperatingSystem.Windows;
|
||||||
|
case 'linux':
|
||||||
|
return OperatingSystem.Linux;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/presentation/electron/preload/WindowVariablesProvider.ts
Normal file
14
src/presentation/electron/preload/WindowVariablesProvider.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createNodeSystemOperations } from '@/infrastructure/Environment/SystemOperations/NodeSystemOperations';
|
||||||
|
import { WindowVariables } from '@/infrastructure/Environment/WindowVariables';
|
||||||
|
import { convertPlatformToOs } from './NodeOsMapper';
|
||||||
|
|
||||||
|
export function provideWindowVariables(
|
||||||
|
createSystem = createNodeSystemOperations,
|
||||||
|
convertToOs = convertPlatformToOs,
|
||||||
|
): WindowVariables {
|
||||||
|
return {
|
||||||
|
system: createSystem(),
|
||||||
|
isDesktop: true,
|
||||||
|
os: convertToOs(process.platform),
|
||||||
|
};
|
||||||
|
}
|
||||||
23
src/presentation/electron/preload/index.ts
Normal file
23
src/presentation/electron/preload/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// This file is used to securely expose Electron APIs to the application.
|
||||||
|
|
||||||
|
import { contextBridge } from 'electron';
|
||||||
|
import log from 'electron-log';
|
||||||
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
|
import { provideWindowVariables } from './WindowVariablesProvider';
|
||||||
|
|
||||||
|
validateRuntimeSanity({
|
||||||
|
// Validate metadata as a preventive measure for fail-fast,
|
||||||
|
// even if it's not currently used in the preload script.
|
||||||
|
validateMetadata: true,
|
||||||
|
|
||||||
|
// The preload script cannot access variables on the window object.
|
||||||
|
validateEnvironment: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const windowVariables = provideWindowVariables();
|
||||||
|
Object.entries(windowVariables).forEach(([key, value]) => {
|
||||||
|
contextBridge.exposeInMainWorld(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not remove [PRELOAD_INIT]; it's a marker used in tests.
|
||||||
|
log.info('[PRELOAD_INIT] Preload script successfully initialized and executed.');
|
||||||
38
src/presentation/index.html
Normal file
38
src/presentation/index.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows, macOS and Linux</title>
|
||||||
|
<meta name="robots" content="index,follow" />
|
||||||
|
<meta name="description"
|
||||||
|
content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it." />
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
#javascriptDisabled {
|
||||||
|
background: #eceef1;
|
||||||
|
margin: 5rem auto;
|
||||||
|
max-width: 800px;
|
||||||
|
font-size: 7px;
|
||||||
|
padding: 3rem;
|
||||||
|
border: 1px solid#333a45;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 150%;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="javascriptDisabled">
|
||||||
|
<h1>Problem loading page</h1>
|
||||||
|
<p>The page does not work without JavaScript enabled. Please enable it to use privacy.sexy. There's no shady stuff
|
||||||
|
as 100% of the website is open source.</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user