Improve desktop runtime execution tests
Test improvements: - Capture titles for all macOS windows, not just the frontmost. - Incorporate missing application log files. - Improve log clarity with enriched context. - Improve application termination on macOS by reducing grace period. - Ensure complete application termination on macOS. - Validate Vue application loading through an initial log. - Support ignoring environment-specific `stderr` errors. - Do not fail the test if working directory cannot be deleted. - Use retry pattern when installing dependencies due to network errors. Refactorings: - Migrate the test code to TypeScript. - Replace deprecated `rmdir` with `rm` for error-resistant directory removal. - Improve sanity checking by shifting from App.vue to Vue bootstrapper. - Centralize environment variable management with `EnvironmentVariables` construct. - Rename infrastructure/Environment to RuntimeEnvironment for clarity. - Isolate WindowVariables and SystemOperations from RuntimeEnvironment. - Inject logging via preloader. - Correct mislabeled RuntimeSanity tests. Configuration: - Introduce `npm run check:desktop` for simplified execution. - Omit `console.log` override due to `nodeIntegration` restrictions and reveal logging functionality using context-bridging.
This commit is contained in:
@@ -19,6 +19,9 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Configure Ubuntu
|
name: Configure Ubuntu
|
||||||
if: matrix.os == 'ubuntu'
|
if: matrix.os == 'ubuntu'
|
||||||
@@ -57,7 +60,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
shell: bash
|
shell: bash
|
||||||
run: node ./scripts/check-desktop-runtime-errors --screenshot
|
run: npm run check:desktop -- --screenshot
|
||||||
-
|
-
|
||||||
name: Upload screenshot
|
name: Upload screenshot
|
||||||
if: always() # Run even if previous step fails
|
if: always() # Run even if previous step fails
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ You could run other types of tests as well, but they may take longer time and ov
|
|||||||
- Run end-to-end (e2e) tests:
|
- Run end-to-end (e2e) tests:
|
||||||
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
- `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading.
|
||||||
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
- `npm run test:cy:run`: Run tests on the production build in a headless mode.
|
||||||
|
- Run runtime checks for packaged desktop applications: `npm run check:desktop`, see its [README.md](./../scripts/check-desktop-runtime-errors/README.md).
|
||||||
|
|
||||||
📖 Read more about testing in [tests](./tests.md).
|
📖 Read more about testing in [tests](./tests.md).
|
||||||
|
|
||||||
|
|||||||
238
package-lock.json
generated
238
package-lock.json
generated
@@ -6,7 +6,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.1",
|
"version": "0.12.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
"start-server-and-test": "^2.0.0",
|
"start-server-and-test": "^2.0.0",
|
||||||
"svgexport": "^0.4.2",
|
"svgexport": "^0.4.2",
|
||||||
"terser": "^5.19.2",
|
"terser": "^5.19.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "~2.4.0",
|
"tslib": "~2.4.0",
|
||||||
"typescript": "~4.6.2",
|
"typescript": "~4.6.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
@@ -1789,6 +1790,28 @@
|
|||||||
"node": ">=0.1.90"
|
"node": ">=0.1.90"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cypress/request": {
|
"node_modules/@cypress/request": {
|
||||||
"version": "2.88.12",
|
"version": "2.88.12",
|
||||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
||||||
@@ -3611,6 +3634,30 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/ace": {
|
"node_modules/@types/ace": {
|
||||||
"version": "0.0.48",
|
"version": "0.0.48",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz",
|
||||||
@@ -6070,6 +6117,12 @@
|
|||||||
"buffer": "^5.1.0"
|
"buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/cross-fetch": {
|
"node_modules/cross-fetch": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||||
@@ -11185,6 +11238,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/map-age-cleaner": {
|
"node_modules/map-age-cleaner": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
||||||
@@ -19602,6 +19661,64 @@
|
|||||||
"utf8-byte-length": "^1.0.1"
|
"utf8-byte-length": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||||
|
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-node/node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/ts-node/node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||||
@@ -20365,6 +20482,12 @@
|
|||||||
"vue-resize": "^1.0.1"
|
"vue-resize": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/validate-npm-package-license": {
|
"node_modules/validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
@@ -21289,6 +21412,15 @@
|
|||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
@@ -22509,6 +22641,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@cypress/request": {
|
"@cypress/request": {
|
||||||
"version": "2.88.12",
|
"version": "2.88.12",
|
||||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
||||||
@@ -23786,6 +23939,30 @@
|
|||||||
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
|
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@tsconfig/node10": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/ace": {
|
"@types/ace": {
|
||||||
"version": "0.0.48",
|
"version": "0.0.48",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz",
|
||||||
@@ -25660,6 +25837,12 @@
|
|||||||
"buffer": "^5.1.0"
|
"buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"cross-fetch": {
|
"cross-fetch": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||||
@@ -29569,6 +29752,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"map-age-cleaner": {
|
"map-age-cleaner": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
||||||
@@ -35555,6 +35744,41 @@
|
|||||||
"utf8-byte-length": "^1.0.1"
|
"utf8-byte-length": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"version": "10.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||||
|
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tsconfig-paths": {
|
"tsconfig-paths": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||||
@@ -36115,6 +36339,12 @@
|
|||||||
"vue-resize": "^1.0.1"
|
"vue-resize": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"validate-npm-package-license": {
|
"validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
@@ -36736,6 +36966,12 @@
|
|||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"yocto-queue": {
|
"yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||||
"icons:build": "node scripts/logo-update.js",
|
"icons:build": "node scripts/logo-update.js",
|
||||||
|
"check:desktop": "ts-node --experimentalSpecifierResolution node --esm scripts/check-desktop-runtime-errors/index.ts",
|
||||||
"electron:dev": "electron-vite dev",
|
"electron:dev": "electron-vite dev",
|
||||||
"electron:preview": "electron-vite preview",
|
"electron:preview": "electron-vite preview",
|
||||||
"electron:prebuild": "electron-vite build",
|
"electron:prebuild": "electron-vite build",
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
"start-server-and-test": "^2.0.0",
|
"start-server-and-test": "^2.0.0",
|
||||||
"svgexport": "^0.4.2",
|
"svgexport": "^0.4.2",
|
||||||
"terser": "^5.19.2",
|
"terser": "^5.19.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "~2.4.0",
|
"tslib": "~2.4.0",
|
||||||
"typescript": "~4.6.2",
|
"typescript": "~4.6.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
require('@rushstack/eslint-patch/modern-module-resolution.js');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'import/extensions': ['error', 'always'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -16,12 +16,6 @@ It runs the application for a duration and detects runtime errors in the package
|
|||||||
|
|
||||||
Upon error, the script captures a screenshot (if `--screenshot` is provided) and terminates.
|
Upon error, the script captures a screenshot (if `--screenshot` is provided) and terminates.
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```sh
|
|
||||||
node ./scripts/check-desktop-runtime-errors
|
|
||||||
```
|
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
- `--build`: Clears the electron distribution directory and forces a rebuild of the Electron app.
|
- `--build`: Clears the electron distribution directory and forces a rebuild of the Electron app.
|
||||||
@@ -32,4 +26,4 @@ It can be used to automate checking for runtime errors during development.
|
|||||||
|
|
||||||
## Configs
|
## Configs
|
||||||
|
|
||||||
Configurations are defined in [`config.js`](./config.js).
|
Configurations are defined in [`config.ts`](./config.ts).
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { unlink, readFile } from 'fs/promises';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { log, die, LOG_LEVELS } from '../utils/log.js';
|
|
||||||
import { exists } from '../utils/io.js';
|
|
||||||
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../utils/platform.js';
|
|
||||||
import { getAppName } from '../utils/npm.js';
|
|
||||||
|
|
||||||
export async function clearAppLogFile(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const logPath = await determineLogPath(projectDir);
|
|
||||||
if (!logPath || !await exists(logPath)) {
|
|
||||||
log(`Skipping clearing logs, log file does not exist: ${logPath}.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await unlink(logPath);
|
|
||||||
log(`Successfully cleared the log file at: ${logPath}.`);
|
|
||||||
} catch (error) {
|
|
||||||
die(`Failed to clear the log file at: ${logPath}. Reason: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readAppLogFile(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const logPath = await determineLogPath(projectDir);
|
|
||||||
if (!logPath || !await exists(logPath)) {
|
|
||||||
log(`No log file at: ${logPath}`, LOG_LEVELS.WARN);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const logContent = await readLogFile(logPath);
|
|
||||||
return logContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function determineLogPath(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const appName = await getAppName(projectDir);
|
|
||||||
if (!appName) {
|
|
||||||
die('App name not found.');
|
|
||||||
}
|
|
||||||
const logFilePaths = {
|
|
||||||
[SUPPORTED_PLATFORMS.MAC]: () => join(process.env.HOME, 'Library', 'Logs', appName, 'main.log'),
|
|
||||||
[SUPPORTED_PLATFORMS.LINUX]: () => join(process.env.HOME, '.config', appName, 'logs', 'main.log'),
|
|
||||||
[SUPPORTED_PLATFORMS.WINDOWS]: () => join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', 'main.log'),
|
|
||||||
};
|
|
||||||
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
|
||||||
if (!logFilePath) {
|
|
||||||
log(`Cannot determine log path, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
|
||||||
}
|
|
||||||
return logFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readLogFile(logFilePath) {
|
|
||||||
const content = await readFile(logFilePath, 'utf-8');
|
|
||||||
return content?.trim().length > 0 ? content : undefined;
|
|
||||||
}
|
|
||||||
82
scripts/check-desktop-runtime-errors/app/app-logs.ts
Normal file
82
scripts/check-desktop-runtime-errors/app/app-logs.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { unlink, readFile } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { log, die, LogLevel } from '../utils/log';
|
||||||
|
import { exists } from '../utils/io';
|
||||||
|
import { SupportedPlatform, CURRENT_PLATFORM } from '../utils/platform';
|
||||||
|
import { getAppName } from '../utils/npm';
|
||||||
|
|
||||||
|
const LOG_FILE_NAMES = ['main', 'renderer'];
|
||||||
|
|
||||||
|
export async function clearAppLogFiles(
|
||||||
|
projectDir: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
await Promise.all(LOG_FILE_NAMES.map(async (logFileName) => {
|
||||||
|
const logPath = await determineLogPath(projectDir, logFileName);
|
||||||
|
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: string,
|
||||||
|
logFileName: string,
|
||||||
|
): Promise<AppLogFileResult> {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const logPath = await determineLogPath(projectDir, logFileName);
|
||||||
|
if (!logPath || !await exists(logPath)) {
|
||||||
|
log(`No log file at: ${logPath}`, LogLevel.Warn);
|
||||||
|
return {
|
||||||
|
logFilePath: logPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const logContent = await readLogFile(logPath);
|
||||||
|
return {
|
||||||
|
logFileContent: logContent,
|
||||||
|
logFilePath: logPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppLogFileResult {
|
||||||
|
readonly logFilePath: string;
|
||||||
|
readonly logFileContent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function determineLogPath(
|
||||||
|
projectDir: string,
|
||||||
|
logFileName: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
if (!LOG_FILE_NAMES.includes(logFileName)) { throw new Error(`unknown log file name: ${logFileName}`); }
|
||||||
|
const appName = await getAppName(projectDir);
|
||||||
|
if (!appName) {
|
||||||
|
return die('App name not found.');
|
||||||
|
}
|
||||||
|
const logFilePaths: {
|
||||||
|
readonly [K in SupportedPlatform]: () => string;
|
||||||
|
} = {
|
||||||
|
[SupportedPlatform.macOS]: () => join(process.env.HOME, 'Library', 'Logs', appName, `${logFileName}.log`),
|
||||||
|
[SupportedPlatform.Linux]: () => join(process.env.HOME, '.config', appName, 'logs', `${logFileName}.log`),
|
||||||
|
[SupportedPlatform.Windows]: () => join(process.env.USERPROFILE, 'AppData', 'Roaming', appName, 'logs', `${logFileName}.log`),
|
||||||
|
};
|
||||||
|
const logFilePath = logFilePaths[CURRENT_PLATFORM]?.();
|
||||||
|
if (!logFilePath) {
|
||||||
|
log(`Cannot determine log path, unsupported OS: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn);
|
||||||
|
}
|
||||||
|
return logFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLogFile(
|
||||||
|
logFilePath: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const content = await readFile(logFilePath, 'utf-8');
|
||||||
|
return content?.trim().length > 0 ? content : undefined;
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { splitTextIntoLines, indentText } from '../utils/text.js';
|
|
||||||
import { die } from '../utils/log.js';
|
|
||||||
import { readAppLogFile } from './app-logs.js';
|
|
||||||
|
|
||||||
const ELECTRON_CRASH_TITLE = 'Error'; // Used by electron for early crashes
|
|
||||||
const LOG_ERROR_MARKER = '[error]'; // from electron-log
|
|
||||||
const EXPECTED_LOG_MARKERS = [
|
|
||||||
'[WINDOW_INIT]',
|
|
||||||
'[PRELOAD_INIT]',
|
|
||||||
'[APP_MOUNT_INIT]',
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function checkForErrors(stderr, windowTitles, projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const errors = await gatherErrors(stderr, windowTitles, projectDir);
|
|
||||||
if (errors.length) {
|
|
||||||
die(formatErrors(errors));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function gatherErrors(stderr, windowTitles, projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const logContent = await readAppLogFile(projectDir);
|
|
||||||
return [
|
|
||||||
verifyStdErr(stderr),
|
|
||||||
verifyApplicationLogsExist(logContent),
|
|
||||||
...EXPECTED_LOG_MARKERS.map((marker) => verifyLogMarkerExistsInLogs(logContent, marker)),
|
|
||||||
verifyWindowTitle(windowTitles),
|
|
||||||
verifyErrorsInLogs(logContent),
|
|
||||||
].filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatErrors(errors) {
|
|
||||||
if (!errors || !errors.length) { throw new Error('missing errors'); }
|
|
||||||
return [
|
|
||||||
'Errors detected during execution:',
|
|
||||||
...errors.map(
|
|
||||||
(error) => formatError(error),
|
|
||||||
),
|
|
||||||
].join('\n---\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatError(error) {
|
|
||||||
if (!error) { throw new Error('missing error'); }
|
|
||||||
if (!error.reason) { throw new Error(`missing reason, error (${typeof error}): ${JSON.stringify(error)}`); }
|
|
||||||
let message = `Reason: ${indentText(error.reason, 1)}`;
|
|
||||||
if (error.description) {
|
|
||||||
message += `\nDescription:\n${indentText(error.description, 2)}`;
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyApplicationLogsExist(logContent) {
|
|
||||||
if (!logContent || !logContent.length) {
|
|
||||||
return describeError(
|
|
||||||
'Missing application logs',
|
|
||||||
'Application logs are empty not were not found.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyLogMarkerExistsInLogs(logContent, marker) {
|
|
||||||
if (!marker) {
|
|
||||||
throw new Error('missing marker');
|
|
||||||
}
|
|
||||||
if (!logContent?.includes(marker)) {
|
|
||||||
return describeError(
|
|
||||||
'Incomplete application logs',
|
|
||||||
`Missing identifier "${marker}" in application logs.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyWindowTitle(windowTitles) {
|
|
||||||
const errorTitles = windowTitles.filter(
|
|
||||||
(title) => title.toLowerCase().includes(ELECTRON_CRASH_TITLE),
|
|
||||||
);
|
|
||||||
if (errorTitles.length) {
|
|
||||||
return describeError(
|
|
||||||
'Unexpected window title',
|
|
||||||
'One or more window titles suggest an error occurred in the application:'
|
|
||||||
+ `\nError Titles: ${errorTitles.join(', ')}`
|
|
||||||
+ `\nAll Titles: ${windowTitles.join(', ')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyStdErr(stderrOutput) {
|
|
||||||
if (stderrOutput && stderrOutput.length > 0) {
|
|
||||||
return describeError(
|
|
||||||
'Standard error stream (`stderr`) is not empty.',
|
|
||||||
stderrOutput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyErrorsInLogs(logContent) {
|
|
||||||
if (!logContent || !logContent.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const logLines = getNonEmptyLines(logContent)
|
|
||||||
.filter((line) => line.includes(LOG_ERROR_MARKER));
|
|
||||||
if (!logLines.length) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return describeError(
|
|
||||||
'Application log file',
|
|
||||||
logLines.join('\n'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function describeError(reason, description) {
|
|
||||||
return {
|
|
||||||
reason,
|
|
||||||
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNonEmptyLines(text) {
|
|
||||||
return splitTextIntoLines(text)
|
|
||||||
.filter((line) => line?.trim().length > 0);
|
|
||||||
}
|
|
||||||
181
scripts/check-desktop-runtime-errors/app/check-for-errors.ts
Normal file
181
scripts/check-desktop-runtime-errors/app/check-for-errors.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { splitTextIntoLines, indentText } from '../utils/text';
|
||||||
|
import { log, die } from '../utils/log';
|
||||||
|
import { readAppLogFile } from './app-logs';
|
||||||
|
import { STDERR_IGNORE_PATTERNS } from './error-ignore-patterns';
|
||||||
|
|
||||||
|
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_INIT]',
|
||||||
|
];
|
||||||
|
|
||||||
|
type ProcessType = 'main' | 'renderer';
|
||||||
|
|
||||||
|
export async function checkForErrors(
|
||||||
|
stderr: string,
|
||||||
|
windowTitles: readonly string[],
|
||||||
|
projectDir: string,
|
||||||
|
) {
|
||||||
|
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: string,
|
||||||
|
windowTitles: readonly string[],
|
||||||
|
projectDir: string,
|
||||||
|
): Promise<ExecutionError[]> {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const { logFileContent: mainLogs, logFilePath: mainLogFile } = await readAppLogFile(projectDir, 'main');
|
||||||
|
const { logFileContent: rendererLogs, logFilePath: rendererLogFile } = await readAppLogFile(projectDir, 'renderer');
|
||||||
|
const allLogs = [mainLogs, rendererLogs, stderr].filter(Boolean).join('\n');
|
||||||
|
return [
|
||||||
|
verifyStdErr(stderr),
|
||||||
|
verifyApplicationLogsExist('main', mainLogs, mainLogFile),
|
||||||
|
verifyApplicationLogsExist('renderer', rendererLogs, rendererLogFile),
|
||||||
|
...EXPECTED_LOG_MARKERS.map(
|
||||||
|
(marker) => verifyLogMarkerExistsInLogs(allLogs, marker),
|
||||||
|
),
|
||||||
|
verifyWindowTitle(windowTitles),
|
||||||
|
verifyErrorsInLogs(allLogs),
|
||||||
|
].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExecutionError {
|
||||||
|
readonly reason: string;
|
||||||
|
readonly description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrors(errors: readonly ExecutionError[]): string {
|
||||||
|
if (!errors?.length) { throw new Error('missing errors'); }
|
||||||
|
return [
|
||||||
|
'Errors detected during execution:',
|
||||||
|
...errors.map(
|
||||||
|
(error) => formatError(error),
|
||||||
|
),
|
||||||
|
].join('\n---\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(error: ExecutionError): string {
|
||||||
|
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(
|
||||||
|
processType: ProcessType,
|
||||||
|
logContent: string | undefined,
|
||||||
|
logFilePath: string,
|
||||||
|
): ExecutionError | undefined {
|
||||||
|
if (!logContent?.length) {
|
||||||
|
return describeError(
|
||||||
|
`Missing application (${processType}) logs`,
|
||||||
|
'Application logs are empty not were not found.'
|
||||||
|
+ `\nLog path: ${logFilePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyLogMarkerExistsInLogs(
|
||||||
|
logContent: string | undefined,
|
||||||
|
marker: string,
|
||||||
|
) : ExecutionError | undefined {
|
||||||
|
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: readonly string[],
|
||||||
|
) : ExecutionError | undefined {
|
||||||
|
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: string | undefined,
|
||||||
|
) : ExecutionError | undefined {
|
||||||
|
if (stderrOutput && stderrOutput.length > 0) {
|
||||||
|
const ignoredErrorLines = new Set();
|
||||||
|
const relevantErrors = getNonEmptyLines(stderrOutput)
|
||||||
|
.filter((line) => {
|
||||||
|
line = line.trim();
|
||||||
|
if (STDERR_IGNORE_PATTERNS.some((pattern) => pattern.test(line))) {
|
||||||
|
ignoredErrorLines.add(line);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (ignoredErrorLines.size > 0) {
|
||||||
|
log(`Ignoring \`stderr\` lines:\n${indentText([...ignoredErrorLines].join('\n'), 1)}`);
|
||||||
|
}
|
||||||
|
if (relevantErrors.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return describeError(
|
||||||
|
'Standard error stream (`stderr`) is not empty.',
|
||||||
|
`Relevant errors (${relevantErrors.length}):\n${indentText(relevantErrors.map((error) => `- ${error}`).join('\n'), 1)}`
|
||||||
|
+ `\nFull \`stderr\` output:\n${indentText(stderrOutput, 1)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyErrorsInLogs(
|
||||||
|
logContent: string | undefined,
|
||||||
|
) : ExecutionError | undefined {
|
||||||
|
if (!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: string,
|
||||||
|
description: string,
|
||||||
|
) : ExecutionError | undefined {
|
||||||
|
return {
|
||||||
|
reason,
|
||||||
|
description: `${description}\n\nThis might indicate an early crash or significant runtime issue.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNonEmptyLines(text: string) {
|
||||||
|
return splitTextIntoLines(text)
|
||||||
|
.filter((line) => line?.trim().length > 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/* eslint-disable vue/max-len */
|
||||||
|
|
||||||
|
/* Ignore errors specific to host environment, rather than application execution */
|
||||||
|
export const STDERR_IGNORE_PATTERNS: readonly RegExp[] = [
|
||||||
|
/*
|
||||||
|
OS: Linux
|
||||||
|
Background:
|
||||||
|
GLIBC and libgiolibproxy.so were seen on local Linux (Ubuntu-based) installation.
|
||||||
|
Original logs:
|
||||||
|
/snap/core20/current/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by /lib/x86_64-linux-gnu/libproxy.so.1)
|
||||||
|
Failed to load module: /home/bob/snap/code/common/.cache/gio-modules/libgiolibproxy.so
|
||||||
|
[334053:0829/122143.595703:ERROR:browser_main_loop.cc(274)] GLib: Failed to set scheduler settings: Operation not permitted
|
||||||
|
*/
|
||||||
|
/libstdc\+\+\.so.*?GLIBCXX_.*?not found/,
|
||||||
|
/Failed to load module: .*?libgiolibproxy\.so/,
|
||||||
|
/\[.*?:ERROR:browser_main_loop\.cc.*?\] GLib: Failed to set scheduler settings: Operation not permitted/,
|
||||||
|
|
||||||
|
/*
|
||||||
|
OS: macOS
|
||||||
|
Background:
|
||||||
|
Observed when running on GitHub runner, but not on local macOS environment.
|
||||||
|
Original logs:
|
||||||
|
[1571:0828/162611.460587:ERROR:trust_store_mac.cc(844)] Error parsing certificate:
|
||||||
|
ERROR: Failed parsing extensions
|
||||||
|
*/
|
||||||
|
/ERROR:trust_store_mac\.cc.*?Error parsing certificate:/,
|
||||||
|
/ERROR: Failed parsing extensions/,
|
||||||
|
|
||||||
|
/*
|
||||||
|
OS: Linux (GitHub Actions)
|
||||||
|
Background:
|
||||||
|
Occur during Electron's GPU process initialization. Common in headless CI/CD environments.
|
||||||
|
Not indicative of a problem in typical desktop environments.
|
||||||
|
Original logs:
|
||||||
|
[3548:0828/162502.835833:ERROR:viz_main_impl.cc(186)] Exiting GPU process due to errors during initialization
|
||||||
|
[3627:0828/162503.133178:ERROR:viz_main_impl.cc(186)] Exiting GPU process due to errors during initialization
|
||||||
|
[3621:0828/162503.420173:ERROR:command_buffer_proxy_impl.cc(128)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer.
|
||||||
|
*/
|
||||||
|
/ERROR:viz_main_impl\.cc.*?Exiting GPU process due to errors during initialization/,
|
||||||
|
/ERROR:command_buffer_proxy_impl\.cc.*?ContextResult::kTransientFailure: Failed to send GpuControl\.CreateCommandBuffer\./,
|
||||||
|
];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ExtractionResult {
|
||||||
|
readonly appExecutablePath: string;
|
||||||
|
readonly cleanup?: () => Promise<void>;
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { access, chmod } from 'fs/promises';
|
import { access, chmod } from 'fs/promises';
|
||||||
import { constants } from 'fs';
|
import { constants } from 'fs';
|
||||||
import { findSingleFileByExtension } from '../../utils/io.js';
|
import { findSingleFileByExtension } from '../../utils/io';
|
||||||
import { log } from '../../utils/log.js';
|
import { log } from '../../utils/log';
|
||||||
|
import { ExtractionResult } from './extraction-result';
|
||||||
|
|
||||||
export async function prepareLinuxApp(desktopDistPath) {
|
export async function prepareLinuxApp(
|
||||||
|
desktopDistPath: string,
|
||||||
|
): Promise<ExtractionResult> {
|
||||||
const { absolutePath: appFile } = await findSingleFileByExtension(
|
const { absolutePath: appFile } = await findSingleFileByExtension(
|
||||||
'AppImage',
|
'AppImage',
|
||||||
desktopDistPath,
|
desktopDistPath,
|
||||||
@@ -14,7 +17,7 @@ export async function prepareLinuxApp(desktopDistPath) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeExecutable(appFile) {
|
async function makeExecutable(appFile: string): Promise<void> {
|
||||||
if (!appFile) { throw new Error('missing file'); }
|
if (!appFile) { throw new Error('missing file'); }
|
||||||
if (await isExecutable(appFile)) {
|
if (await isExecutable(appFile)) {
|
||||||
log('AppImage is already executable.');
|
log('AppImage is already executable.');
|
||||||
@@ -24,7 +27,7 @@ async function makeExecutable(appFile) {
|
|||||||
await chmod(appFile, 0o755);
|
await chmod(appFile, 0o755);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isExecutable(file) {
|
async function isExecutable(file: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await access(file, constants.X_OK);
|
await access(file, constants.X_OK);
|
||||||
return true;
|
return true;
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { runCommand } from '../../utils/run-command.js';
|
import { runCommand } from '../../utils/run-command';
|
||||||
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
import { findSingleFileByExtension, exists } from '../../utils/io';
|
||||||
import { log, die, LOG_LEVELS } from '../../utils/log.js';
|
import { log, die, LogLevel } from '../../utils/log';
|
||||||
|
import { sleep } from '../../utils/sleep';
|
||||||
|
import { ExtractionResult } from './extraction-result';
|
||||||
|
|
||||||
export async function prepareMacOsApp(desktopDistPath) {
|
export async function prepareMacOsApp(
|
||||||
|
desktopDistPath: string,
|
||||||
|
): Promise<ExtractionResult> {
|
||||||
const { absolutePath: dmgPath } = await findSingleFileByExtension('dmg', desktopDistPath);
|
const { absolutePath: dmgPath } = await findSingleFileByExtension('dmg', desktopDistPath);
|
||||||
const { mountPath } = await mountDmg(dmgPath);
|
const { mountPath } = await mountDmg(dmgPath);
|
||||||
const appPath = await findMacAppExecutablePath(mountPath);
|
const appPath = await findMacAppExecutablePath(mountPath);
|
||||||
@@ -15,8 +19,12 @@ export async function prepareMacOsApp(desktopDistPath) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mountDmg(dmgFile) {
|
async function mountDmg(
|
||||||
const { stdout: hdiutilOutput, error } = await runCommand(`hdiutil attach '${dmgFile}'`);
|
dmgFile: string,
|
||||||
|
) {
|
||||||
|
const { stdout: hdiutilOutput, error } = await runCommand(
|
||||||
|
`hdiutil attach '${dmgFile}'`,
|
||||||
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
die(`Failed to mount DMG file at ${dmgFile}.\n${error}`);
|
die(`Failed to mount DMG file at ${dmgFile}.\n${error}`);
|
||||||
}
|
}
|
||||||
@@ -27,12 +35,14 @@ async function mountDmg(dmgFile) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findMacAppExecutablePath(mountPath) {
|
async function findMacAppExecutablePath(
|
||||||
|
mountPath: string,
|
||||||
|
): Promise<string> {
|
||||||
const { stdout: findOutput, error } = await runCommand(
|
const { stdout: findOutput, error } = await runCommand(
|
||||||
`find '${mountPath}' -maxdepth 1 -type d -name "*.app"`,
|
`find '${mountPath}' -maxdepth 1 -type d -name "*.app"`,
|
||||||
);
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
die(`Failed to find executable path at mount path ${mountPath}\n${error}`);
|
return die(`Failed to find executable path at mount path ${mountPath}\n${error}`);
|
||||||
}
|
}
|
||||||
const appFolder = findOutput.trim();
|
const appFolder = findOutput.trim();
|
||||||
const appName = appFolder.split('/').pop().replace('.app', '');
|
const appName = appFolder.split('/').pop().replace('.app', '');
|
||||||
@@ -40,16 +50,19 @@ async function findMacAppExecutablePath(mountPath) {
|
|||||||
if (await exists(appPath)) {
|
if (await exists(appPath)) {
|
||||||
log(`Application is located at ${appPath}`);
|
log(`Application is located at ${appPath}`);
|
||||||
} else {
|
} else {
|
||||||
die(`Application does not exist at ${appPath}`);
|
return die(`Application does not exist at ${appPath}`);
|
||||||
}
|
}
|
||||||
return appPath;
|
return appPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detachMount(mountPath, retries = 5) {
|
async function detachMount(
|
||||||
|
mountPath: string,
|
||||||
|
retries = 5,
|
||||||
|
) {
|
||||||
const { error } = await runCommand(`hdiutil detach '${mountPath}'`);
|
const { error } = await runCommand(`hdiutil detach '${mountPath}'`);
|
||||||
if (error) {
|
if (error) {
|
||||||
if (retries <= 0) {
|
if (retries <= 0) {
|
||||||
log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LOG_LEVELS.WARN);
|
log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LogLevel.Warn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
@@ -58,9 +71,3 @@ async function detachMount(mountPath, retries = 5) {
|
|||||||
}
|
}
|
||||||
log(`Successfully detached from ${mountPath}`);
|
log(`Successfully detached from ${mountPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(milliseconds) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, milliseconds);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,46 @@
|
|||||||
import { mkdtemp, rmdir } from 'fs/promises';
|
import { mkdtemp, rm } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { findSingleFileByExtension, exists } from '../../utils/io.js';
|
import { findSingleFileByExtension, exists } from '../../utils/io';
|
||||||
import { log, die } from '../../utils/log.js';
|
import { log, die, LogLevel } from '../../utils/log';
|
||||||
import { runCommand } from '../../utils/run-command.js';
|
import { runCommand } from '../../utils/run-command';
|
||||||
|
import { ExtractionResult } from './extraction-result';
|
||||||
|
|
||||||
export async function prepareWindowsApp(desktopDistPath) {
|
export async function prepareWindowsApp(
|
||||||
|
desktopDistPath: string,
|
||||||
|
): Promise<ExtractionResult> {
|
||||||
const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-'));
|
const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-'));
|
||||||
if (await exists(workdir)) {
|
if (await exists(workdir)) {
|
||||||
log(`Temporary directory ${workdir} already exists, cleaning up...`);
|
log(`Temporary directory ${workdir} already exists, cleaning up...`);
|
||||||
await rmdir(workdir, { recursive: true });
|
await rm(workdir, { recursive: true });
|
||||||
}
|
}
|
||||||
const { appExecutablePath } = await installNsis(workdir, desktopDistPath);
|
const appExecutablePath = await installNsis(workdir, desktopDistPath);
|
||||||
return {
|
return {
|
||||||
appExecutablePath,
|
appExecutablePath,
|
||||||
cleanup: async () => {
|
cleanup: async () => {
|
||||||
log(`Cleaning up working directory ${workdir}...`);
|
log(`Cleaning up working directory ${workdir}...`);
|
||||||
await rmdir(workdir, { recursive: true });
|
try {
|
||||||
|
await rm(workdir, { recursive: true, force: true });
|
||||||
|
} catch (error) {
|
||||||
|
log(`Could not cleanup the working directory: ${error.message}`, LogLevel.Error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installNsis(installationPath, desktopDistPath) {
|
async function installNsis(
|
||||||
|
installationPath: string,
|
||||||
|
desktopDistPath: string,
|
||||||
|
): Promise<string> {
|
||||||
const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath);
|
const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath);
|
||||||
|
|
||||||
log(`Silently installing contents of ${installerPath} to ${installationPath}...`);
|
log(`Silently installing contents of ${installerPath} to ${installationPath}...`);
|
||||||
const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`);
|
const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`);
|
||||||
if (error) {
|
if (error) {
|
||||||
die(`Failed to install.\n${error}`);
|
return die(`Failed to install.\n${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath);
|
const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath);
|
||||||
|
|
||||||
return {
|
return appExecutablePath;
|
||||||
appExecutablePath,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { log, LOG_LEVELS, die } from '../utils/log.js';
|
import { log, LogLevel, die } from '../utils/log';
|
||||||
import { captureScreen } from './system-capture/screen-capture.js';
|
import { captureScreen } from './system-capture/screen-capture';
|
||||||
import { captureWindowTitles } from './system-capture/window-title-capture.js';
|
import { captureWindowTitles } from './system-capture/window-title-capture';
|
||||||
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
const TERMINATION_GRACE_PERIOD_IN_SECONDS = 60;
|
const TERMINATION_GRACE_PERIOD_IN_SECONDS = 20;
|
||||||
const TERMINATION_CHECK_INTERVAL_IN_MS = 1000;
|
const TERMINATION_CHECK_INTERVAL_IN_MS = 1000;
|
||||||
const WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS = 100;
|
const WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS = 100;
|
||||||
|
|
||||||
export function runApplication(
|
export function runApplication(
|
||||||
appFile,
|
appFile: string,
|
||||||
executionDurationInSeconds,
|
executionDurationInSeconds: number,
|
||||||
enableScreenshot,
|
enableScreenshot: boolean,
|
||||||
screenshotPath,
|
screenshotPath: string,
|
||||||
) {
|
): Promise<ApplicationExecutionResult> {
|
||||||
if (!appFile) {
|
if (!appFile) {
|
||||||
throw new Error('Missing app file');
|
throw new Error('Missing app file');
|
||||||
}
|
}
|
||||||
|
|
||||||
logDetails(appFile, executionDurationInSeconds);
|
logDetails(appFile, executionDurationInSeconds);
|
||||||
|
|
||||||
const processDetails = {
|
const processDetails: ApplicationProcessDetails = {
|
||||||
stderrData: '',
|
stderrData: '',
|
||||||
stdoutData: '',
|
stdoutData: '',
|
||||||
explicitlyKilled: false,
|
explicitlyKilled: false,
|
||||||
@@ -35,7 +36,7 @@ export function runApplication(
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
processDetails.resolve = resolve;
|
processDetails.resolve = resolve;
|
||||||
handleTitleCapture(process.pid, processDetails);
|
beginCapturingTitles(process.pid, processDetails);
|
||||||
handleProcessEvents(
|
handleProcessEvents(
|
||||||
processDetails,
|
processDetails,
|
||||||
enableScreenshot,
|
enableScreenshot,
|
||||||
@@ -45,7 +46,28 @@ export function runApplication(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function logDetails(appFile, executionDurationInSeconds) {
|
interface ApplicationExecutionResult {
|
||||||
|
readonly stderr: string,
|
||||||
|
readonly stdout: string,
|
||||||
|
readonly windowTitles: readonly string[],
|
||||||
|
readonly isCrashed: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApplicationProcessDetails {
|
||||||
|
stderrData: string;
|
||||||
|
stdoutData: string;
|
||||||
|
explicitlyKilled: boolean;
|
||||||
|
windowTitles: Array<string>;
|
||||||
|
isCrashed: boolean;
|
||||||
|
isDone: boolean;
|
||||||
|
process: ChildProcess;
|
||||||
|
resolve: (value: ApplicationExecutionResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logDetails(
|
||||||
|
appFile: string,
|
||||||
|
executionDurationInSeconds: number,
|
||||||
|
): void {
|
||||||
log(
|
log(
|
||||||
[
|
[
|
||||||
'Executing the app to check for errors...',
|
'Executing the app to check for errors...',
|
||||||
@@ -55,12 +77,15 @@ function logDetails(appFile, executionDurationInSeconds) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTitleCapture(processId, processDetails) {
|
function beginCapturingTitles(
|
||||||
|
processId: number,
|
||||||
|
processDetails: ApplicationProcessDetails,
|
||||||
|
): void {
|
||||||
const capture = async () => {
|
const capture = async () => {
|
||||||
const titles = await captureWindowTitles(processId);
|
const titles = await captureWindowTitles(processId);
|
||||||
|
|
||||||
(titles || []).forEach((title) => {
|
(titles || []).forEach((title) => {
|
||||||
if (!title || !title.length) {
|
if (!title?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!processDetails.windowTitles.includes(title)) {
|
if (!processDetails.windowTitles.includes(title)) {
|
||||||
@@ -78,11 +103,11 @@ function handleTitleCapture(processId, processDetails) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleProcessEvents(
|
function handleProcessEvents(
|
||||||
processDetails,
|
processDetails: ApplicationProcessDetails,
|
||||||
enableScreenshot,
|
enableScreenshot: boolean,
|
||||||
screenshotPath,
|
screenshotPath: string,
|
||||||
executionDurationInSeconds,
|
executionDurationInSeconds: number,
|
||||||
) {
|
): void {
|
||||||
const { process } = processDetails;
|
const { process } = processDetails;
|
||||||
process.stderr.on('data', (data) => {
|
process.stderr.on('data', (data) => {
|
||||||
processDetails.stderrData += data.toString();
|
processDetails.stderrData += data.toString();
|
||||||
@@ -92,7 +117,7 @@ function handleProcessEvents(
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.on('error', (error) => {
|
process.on('error', (error) => {
|
||||||
die(`An issue spawning the child process: ${error}`, LOG_LEVELS.ERROR);
|
die(`An issue spawning the child process: ${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('exit', async (code) => {
|
process.on('exit', async (code) => {
|
||||||
@@ -100,11 +125,16 @@ function handleProcessEvents(
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath);
|
await onExecutionLimitReached(processDetails, enableScreenshot, screenshotPath);
|
||||||
}, executionDurationInSeconds * 1000);
|
}, executionDurationInSeconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onProcessExit(code, processDetails, enableScreenshot, screenshotPath) {
|
async function onProcessExit(
|
||||||
|
code: number,
|
||||||
|
processDetails: ApplicationProcessDetails,
|
||||||
|
enableScreenshot: boolean,
|
||||||
|
screenshotPath: string,
|
||||||
|
): Promise<void> {
|
||||||
log(`Application exited ${code === null || Number.isNaN(code) ? '.' : `with code ${code}`}`);
|
log(`Application exited ${code === null || Number.isNaN(code) ? '.' : `with code ${code}`}`);
|
||||||
|
|
||||||
if (processDetails.explicitlyKilled) return;
|
if (processDetails.explicitlyKilled) return;
|
||||||
@@ -118,17 +148,21 @@ async function onProcessExit(code, processDetails, enableScreenshot, screenshotP
|
|||||||
finishProcess(processDetails);
|
finishProcess(processDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath) {
|
async function onExecutionLimitReached(
|
||||||
|
processDetails: ApplicationProcessDetails,
|
||||||
|
enableScreenshot: boolean,
|
||||||
|
screenshotPath: string,
|
||||||
|
): Promise<void> {
|
||||||
if (enableScreenshot) {
|
if (enableScreenshot) {
|
||||||
await captureScreen(screenshotPath);
|
await captureScreen(screenshotPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
processDetails.explicitlyKilled = true;
|
processDetails.explicitlyKilled = true;
|
||||||
await terminateGracefully(process);
|
await terminateGracefully(processDetails.process);
|
||||||
finishProcess(processDetails);
|
finishProcess(processDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishProcess(processDetails) {
|
function finishProcess(processDetails: ApplicationProcessDetails): void {
|
||||||
processDetails.isDone = true;
|
processDetails.isDone = true;
|
||||||
processDetails.resolve({
|
processDetails.resolve({
|
||||||
stderr: processDetails.stderrData,
|
stderr: processDetails.stderrData,
|
||||||
@@ -138,7 +172,9 @@ function finishProcess(processDetails) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function terminateGracefully(process) {
|
async function terminateGracefully(
|
||||||
|
process: ChildProcess,
|
||||||
|
): Promise<void> {
|
||||||
let elapsedSeconds = 0;
|
let elapsedSeconds = 0;
|
||||||
log('Attempting to terminate the process gracefully...');
|
log('Attempting to terminate the process gracefully...');
|
||||||
process.kill('SIGTERM');
|
process.kill('SIGTERM');
|
||||||
@@ -147,18 +183,18 @@ async function terminateGracefully(process) {
|
|||||||
const checkInterval = setInterval(() => {
|
const checkInterval = setInterval(() => {
|
||||||
elapsedSeconds += TERMINATION_CHECK_INTERVAL_IN_MS / 1000;
|
elapsedSeconds += TERMINATION_CHECK_INTERVAL_IN_MS / 1000;
|
||||||
|
|
||||||
if (!process.killed) {
|
|
||||||
if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) {
|
if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) {
|
||||||
process.kill('SIGKILL');
|
process.kill('SIGKILL');
|
||||||
log('Process did not terminate gracefully within the grace period. Forcing termination.', LOG_LEVELS.WARN);
|
log('Process did not terminate gracefully within the grace period. Forcing termination.', LogLevel.Warn);
|
||||||
clearInterval(checkInterval);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log('Process terminated gracefully.');
|
|
||||||
clearInterval(checkInterval);
|
clearInterval(checkInterval);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}, TERMINATION_CHECK_INTERVAL_IN_MS);
|
}, TERMINATION_CHECK_INTERVAL_IN_MS);
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
log('Process terminated gracefully.');
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,33 @@
|
|||||||
import { unlink } from 'fs/promises';
|
import { unlink } from 'fs/promises';
|
||||||
import { runCommand } from '../../utils/run-command.js';
|
import { runCommand } from '../../utils/run-command';
|
||||||
import { log, LOG_LEVELS } from '../../utils/log.js';
|
import { log, LogLevel } from '../../utils/log';
|
||||||
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from '../../utils/platform.js';
|
import { CURRENT_PLATFORM, SupportedPlatform } from '../../utils/platform';
|
||||||
import { exists } from '../../utils/io.js';
|
import { exists } from '../../utils/io';
|
||||||
|
|
||||||
export async function captureScreen(imagePath) {
|
export async function captureScreen(
|
||||||
|
imagePath: string,
|
||||||
|
): Promise<void> {
|
||||||
if (!imagePath) {
|
if (!imagePath) {
|
||||||
throw new Error('Path for screenshot not provided');
|
throw new Error('Path for screenshot not provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await exists(imagePath)) {
|
if (await exists(imagePath)) {
|
||||||
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LOG_LEVELS.WARN);
|
log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LogLevel.Warn);
|
||||||
unlink(imagePath);
|
unlink(imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformCommands = {
|
const platformCommands: {
|
||||||
[SUPPORTED_PLATFORMS.MAC]: `screencapture -x ${imagePath}`,
|
readonly [K in SupportedPlatform]: string;
|
||||||
[SUPPORTED_PLATFORMS.LINUX]: `import -window root ${imagePath}`,
|
} = {
|
||||||
[SUPPORTED_PLATFORMS.WINDOWS]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
|
[SupportedPlatform.macOS]: `screencapture -x ${imagePath}`,
|
||||||
|
[SupportedPlatform.Linux]: `import -window root ${imagePath}`,
|
||||||
|
[SupportedPlatform.Windows]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const commandForPlatform = platformCommands[CURRENT_PLATFORM];
|
const commandForPlatform = platformCommands[CURRENT_PLATFORM];
|
||||||
|
|
||||||
if (!commandForPlatform) {
|
if (!commandForPlatform) {
|
||||||
log(`Screenshot capture not supported on: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
log(`Screenshot capture not supported on: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +35,13 @@ export async function captureScreen(imagePath) {
|
|||||||
|
|
||||||
const { error } = await runCommand(commandForPlatform);
|
const { error } = await runCommand(commandForPlatform);
|
||||||
if (error) {
|
if (error) {
|
||||||
log(`Failed to capture screenshot.\n${error}`, LOG_LEVELS.WARN);
|
log(`Failed to capture screenshot.\n${error}`, LogLevel.Warn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log(`Captured screenshot to ${imagePath}.`);
|
log(`Captured screenshot to ${imagePath}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScreenshotPowershellScript(imagePath) {
|
function getScreenshotPowershellScript(imagePath: string): string {
|
||||||
return `
|
return `
|
||||||
$ProgressPreference = 'SilentlyContinue' # Do not pollute stderr
|
$ProgressPreference = 'SilentlyContinue' # Do not pollute stderr
|
||||||
Add-Type -AssemblyName System.Windows.Forms
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
@@ -53,7 +57,7 @@ function getScreenshotPowershellScript(imagePath) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeForPowershell(script) {
|
function encodeForPowershell(script: string): string {
|
||||||
const buffer = Buffer.from(script, 'utf-16le');
|
const buffer = Buffer.from(script, 'utf16le');
|
||||||
return buffer.toString('base64');
|
return buffer.toString('base64');
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,40 @@
|
|||||||
import { runCommand } from '../../utils/run-command.js';
|
import { runCommand } from '../../utils/run-command';
|
||||||
import { log, LOG_LEVELS } from '../../utils/log.js';
|
import { log, LogLevel } from '../../utils/log';
|
||||||
import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../../utils/platform.js';
|
import { SupportedPlatform, CURRENT_PLATFORM } from '../../utils/platform';
|
||||||
|
|
||||||
export async function captureWindowTitles(processId) {
|
export async function captureWindowTitles(processId: number) {
|
||||||
if (!processId) { throw new Error('Missing process ID.'); }
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM];
|
const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM];
|
||||||
if (!captureFunction) {
|
if (!captureFunction) {
|
||||||
log(`Cannot capture window title, unsupported OS: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN);
|
log(`Cannot capture window title, unsupported OS: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return captureFunction(processId);
|
return captureFunction(processId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowTitleCaptureFunctions = {
|
const windowTitleCaptureFunctions: {
|
||||||
[SUPPORTED_PLATFORMS.MAC]: captureTitlesOnMac,
|
readonly [K in SupportedPlatform]: (processId: number) => Promise<string[]>;
|
||||||
[SUPPORTED_PLATFORMS.LINUX]: captureTitlesOnLinux,
|
} = {
|
||||||
[SUPPORTED_PLATFORMS.WINDOWS]: captureTitlesOnWindows,
|
[SupportedPlatform.macOS]: (processId) => captureTitlesOnMac(processId),
|
||||||
|
[SupportedPlatform.Linux]: (processId) => captureTitlesOnLinux(processId),
|
||||||
|
[SupportedPlatform.Windows]: (processId) => captureTitlesOnWindows(processId),
|
||||||
};
|
};
|
||||||
|
|
||||||
async function captureTitlesOnWindows(processId) {
|
async function captureTitlesOnWindows(processId: number): Promise<string[]> {
|
||||||
if (!processId) { throw new Error('Missing process ID.'); }
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
const { stdout: tasklistOutput, error } = await runCommand(
|
const { stdout: tasklistOutput, error } = await runCommand(
|
||||||
`tasklist /FI "PID eq ${processId}" /fo list /v`,
|
`tasklist /FI "PID eq ${processId}" /fo list /v`,
|
||||||
);
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
log(`Failed to retrieve window title.\n${error}`, LOG_LEVELS.WARN);
|
log(`Failed to retrieve window title.\n${error}`, LogLevel.Warn);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const match = tasklistOutput.match(/Window Title:\s*(.*)/);
|
const regex = /Window Title:\s*(.*)/;
|
||||||
if (match && match[1]) {
|
const match = regex.exec(tasklistOutput);
|
||||||
|
if (match && match.length > 1 && match[1]) {
|
||||||
const title = match[1].trim();
|
const title = match[1].trim();
|
||||||
if (title === 'N/A') {
|
if (title === 'N/A') {
|
||||||
return [];
|
return [];
|
||||||
@@ -41,7 +44,7 @@ async function captureTitlesOnWindows(processId) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function captureTitlesOnLinux(processId) {
|
async function captureTitlesOnLinux(processId: number): Promise<string[]> {
|
||||||
if (!processId) { throw new Error('Missing process ID.'); }
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
|
|
||||||
const { stdout: windowIdsOutput, error: windowIdError } = await runCommand(
|
const { stdout: windowIdsOutput, error: windowIdError } = await runCommand(
|
||||||
@@ -49,7 +52,7 @@ async function captureTitlesOnLinux(processId) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (windowIdError || !windowIdsOutput) {
|
if (windowIdError || !windowIdsOutput) {
|
||||||
return undefined;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowIds = windowIdsOutput.trim().split('\n');
|
const windowIds = windowIdsOutput.trim().split('\n');
|
||||||
@@ -69,7 +72,7 @@ async function captureTitlesOnLinux(processId) {
|
|||||||
|
|
||||||
let hasAssistiveAccessOnMac = true;
|
let hasAssistiveAccessOnMac = true;
|
||||||
|
|
||||||
async function captureTitlesOnMac(processId) {
|
async function captureTitlesOnMac(processId: number): Promise<string[]> {
|
||||||
if (!processId) { throw new Error('Missing process ID.'); }
|
if (!processId) { throw new Error('Missing process ID.'); }
|
||||||
if (!hasAssistiveAccessOnMac) {
|
if (!hasAssistiveAccessOnMac) {
|
||||||
return [];
|
return [];
|
||||||
@@ -82,10 +85,11 @@ async function captureTitlesOnMac(processId) {
|
|||||||
return
|
return
|
||||||
end try
|
end try
|
||||||
tell targetProcess
|
tell targetProcess
|
||||||
if (count of windows) > 0 then
|
set allWindowNames to {}
|
||||||
set window_name to name of front window
|
repeat with aWindow in windows
|
||||||
return window_name
|
set end of allWindowNames to name of aWindow
|
||||||
end if
|
end repeat
|
||||||
|
return allWindowNames
|
||||||
end tell
|
end tell
|
||||||
end tell
|
end tell
|
||||||
`;
|
`;
|
||||||
@@ -102,7 +106,7 @@ async function captureTitlesOnMac(processId) {
|
|||||||
hasAssistiveAccessOnMac = false;
|
hasAssistiveAccessOnMac = false;
|
||||||
}
|
}
|
||||||
errorMessage += error;
|
errorMessage += error;
|
||||||
log(errorMessage, LOG_LEVELS.WARN);
|
log(errorMessage, LogLevel.Warn);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const title = titleOutput?.trim();
|
const title = titleOutput?.trim();
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { log } from './utils/log.js';
|
|
||||||
|
|
||||||
const PROCESS_ARGUMENTS = process.argv.slice(2);
|
|
||||||
|
|
||||||
export const COMMAND_LINE_FLAGS = Object.freeze({
|
|
||||||
FORCE_REBUILD: '--build',
|
|
||||||
TAKE_SCREENSHOT: '--screenshot',
|
|
||||||
});
|
|
||||||
|
|
||||||
export function logCurrentArgs() {
|
|
||||||
if (!PROCESS_ARGUMENTS.length) {
|
|
||||||
log('No additional arguments provided.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log(`Arguments: ${PROCESS_ARGUMENTS.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasCommandLineFlag(flag) {
|
|
||||||
return PROCESS_ARGUMENTS.includes(flag);
|
|
||||||
}
|
|
||||||
25
scripts/check-desktop-runtime-errors/cli-args.ts
Normal file
25
scripts/check-desktop-runtime-errors/cli-args.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { log } from './utils/log';
|
||||||
|
|
||||||
|
const PROCESS_ARGUMENTS: string[] = process.argv.slice(2);
|
||||||
|
|
||||||
|
export enum CommandLineFlag {
|
||||||
|
ForceRebuild,
|
||||||
|
TakeScreenshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const COMMAND_LINE_FLAGS = Object.freeze({
|
||||||
|
[CommandLineFlag.ForceRebuild]: '--build',
|
||||||
|
[CommandLineFlag.TakeScreenshot]: '--screenshot',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function logCurrentArgs(): void {
|
||||||
|
if (!PROCESS_ARGUMENTS.length) {
|
||||||
|
log('No additional arguments provided.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log(`Arguments: ${PROCESS_ARGUMENTS.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCommandLineFlag(flag: CommandLineFlag): boolean {
|
||||||
|
return PROCESS_ARGUMENTS.includes(COMMAND_LINE_FLAGS[flag]);
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { main } from './main.js';
|
|
||||||
|
|
||||||
await main();
|
|
||||||
3
scripts/check-desktop-runtime-errors/index.ts
Normal file
3
scripts/check-desktop-runtime-errors/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { main } from './main';
|
||||||
|
|
||||||
|
await main();
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
import { logCurrentArgs, COMMAND_LINE_FLAGS, hasCommandLineFlag } from './cli-args.js';
|
import { logCurrentArgs, CommandLineFlag, hasCommandLineFlag } from './cli-args';
|
||||||
import { log, die } from './utils/log.js';
|
import { log, die } from './utils/log';
|
||||||
import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm.js';
|
import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm';
|
||||||
import { clearAppLogFile } from './app/app-logs.js';
|
import { clearAppLogFiles } from './app/app-logs';
|
||||||
import { checkForErrors } from './app/check-for-errors.js';
|
import { checkForErrors } from './app/check-for-errors';
|
||||||
import { runApplication } from './app/runner.js';
|
import { runApplication } from './app/runner.js';
|
||||||
import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from './utils/platform.js';
|
import { CURRENT_PLATFORM, SupportedPlatform } from './utils/platform';
|
||||||
import { prepareLinuxApp } from './app/extractors/linux.js';
|
import { prepareLinuxApp } from './app/extractors/linux';
|
||||||
import { prepareWindowsApp } from './app/extractors/windows.js';
|
import { prepareWindowsApp } from './app/extractors/windows.js';
|
||||||
import { prepareMacOsApp } from './app/extractors/macos.js';
|
import { prepareMacOsApp } from './app/extractors/macos';
|
||||||
import {
|
import {
|
||||||
DESKTOP_BUILD_COMMAND,
|
DESKTOP_BUILD_COMMAND,
|
||||||
PROJECT_DIR,
|
PROJECT_DIR,
|
||||||
DESKTOP_DIST_PATH,
|
DESKTOP_DIST_PATH,
|
||||||
APP_EXECUTION_DURATION_IN_SECONDS,
|
APP_EXECUTION_DURATION_IN_SECONDS,
|
||||||
SCREENSHOT_PATH,
|
SCREENSHOT_PATH,
|
||||||
} from './config.js';
|
} from './config';
|
||||||
|
import { indentText } from './utils/text';
|
||||||
|
import { ExtractionResult } from './app/extractors/extraction-result';
|
||||||
|
|
||||||
export async function main() {
|
export async function main(): Promise<void> {
|
||||||
logCurrentArgs();
|
logCurrentArgs();
|
||||||
await ensureNpmProjectDir(PROJECT_DIR);
|
await ensureNpmProjectDir(PROJECT_DIR);
|
||||||
await npmInstall(PROJECT_DIR);
|
await npmInstall(PROJECT_DIR);
|
||||||
@@ -24,14 +26,14 @@ export async function main() {
|
|||||||
PROJECT_DIR,
|
PROJECT_DIR,
|
||||||
DESKTOP_BUILD_COMMAND,
|
DESKTOP_BUILD_COMMAND,
|
||||||
DESKTOP_DIST_PATH,
|
DESKTOP_DIST_PATH,
|
||||||
hasCommandLineFlag(COMMAND_LINE_FLAGS.FORCE_REBUILD),
|
hasCommandLineFlag(CommandLineFlag.ForceRebuild),
|
||||||
);
|
);
|
||||||
await clearAppLogFile(PROJECT_DIR);
|
await clearAppLogFiles(PROJECT_DIR);
|
||||||
const {
|
const {
|
||||||
stderr, stdout, isCrashed, windowTitles,
|
stderr, stdout, isCrashed, windowTitles,
|
||||||
} = await extractAndRun();
|
} = await extractAndRun();
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
log(`Output (stdout) from application execution:\n${stdout}`);
|
log(`Output (stdout) from application execution:\n${indentText(stdout, 1)}`);
|
||||||
}
|
}
|
||||||
if (isCrashed) {
|
if (isCrashed) {
|
||||||
die('The application encountered an error during its execution.');
|
die('The application encountered an error during its execution.');
|
||||||
@@ -42,21 +44,23 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function extractAndRun() {
|
async function extractAndRun() {
|
||||||
const extractors = {
|
const extractors: {
|
||||||
[SUPPORTED_PLATFORMS.MAC]: () => prepareMacOsApp(DESKTOP_DIST_PATH),
|
readonly [K in SupportedPlatform]: () => Promise<ExtractionResult>;
|
||||||
[SUPPORTED_PLATFORMS.LINUX]: () => prepareLinuxApp(DESKTOP_DIST_PATH),
|
} = {
|
||||||
[SUPPORTED_PLATFORMS.WINDOWS]: () => prepareWindowsApp(DESKTOP_DIST_PATH),
|
[SupportedPlatform.macOS]: () => prepareMacOsApp(DESKTOP_DIST_PATH),
|
||||||
|
[SupportedPlatform.Linux]: () => prepareLinuxApp(DESKTOP_DIST_PATH),
|
||||||
|
[SupportedPlatform.Windows]: () => prepareWindowsApp(DESKTOP_DIST_PATH),
|
||||||
};
|
};
|
||||||
const extractor = extractors[CURRENT_PLATFORM];
|
const extractor = extractors[CURRENT_PLATFORM];
|
||||||
if (!extractor) {
|
if (!extractor) {
|
||||||
throw new Error(`Platform not supported: ${CURRENT_PLATFORM}`);
|
throw new Error(`Platform not supported: ${SupportedPlatform[CURRENT_PLATFORM]}`);
|
||||||
}
|
}
|
||||||
const { appExecutablePath, cleanup } = await extractor();
|
const { appExecutablePath, cleanup } = await extractor();
|
||||||
try {
|
try {
|
||||||
return await runApplication(
|
return await runApplication(
|
||||||
appExecutablePath,
|
appExecutablePath,
|
||||||
APP_EXECUTION_DURATION_IN_SECONDS,
|
APP_EXECUTION_DURATION_IN_SECONDS,
|
||||||
hasCommandLineFlag(COMMAND_LINE_FLAGS.TAKE_SCREENSHOT),
|
hasCommandLineFlag(CommandLineFlag.TakeScreenshot),
|
||||||
SCREENSHOT_PATH,
|
SCREENSHOT_PATH,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { extname, join } from 'path';
|
import { extname, join } from 'path';
|
||||||
import { readdir, access } from 'fs/promises';
|
import { readdir, access } from 'fs/promises';
|
||||||
import { constants } from 'fs';
|
import { constants } from 'fs';
|
||||||
import { log, die, LOG_LEVELS } from './log.js';
|
import { log, die, LogLevel } from './log';
|
||||||
|
|
||||||
export async function findSingleFileByExtension(extension, directory) {
|
export async function findSingleFileByExtension(
|
||||||
|
extension: string,
|
||||||
|
directory: string,
|
||||||
|
): Promise<FileSearchResult> {
|
||||||
if (!directory) { throw new Error('Missing directory'); }
|
if (!directory) { throw new Error('Missing directory'); }
|
||||||
if (!extension) { throw new Error('Missing file extension'); }
|
if (!extension) { throw new Error('Missing file extension'); }
|
||||||
|
|
||||||
if (!await exists(directory)) {
|
if (!await exists(directory)) {
|
||||||
die(`Directory does not exist: ${directory}`);
|
return die(`Directory does not exist: ${directory}`);
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const directoryContents = await readdir(directory);
|
const directoryContents = await readdir(directory);
|
||||||
@@ -18,17 +20,21 @@ export async function findSingleFileByExtension(extension, directory) {
|
|||||||
(fileName) => !fileName.toLowerCase().includes('uninstall'), // NSIS build has `Uninstall {app-name}.exe`
|
(fileName) => !fileName.toLowerCase().includes('uninstall'), // NSIS build has `Uninstall {app-name}.exe`
|
||||||
);
|
);
|
||||||
if (!withoutUninstaller.length) {
|
if (!withoutUninstaller.length) {
|
||||||
die(`No ${extension} found in ${directory} directory.`);
|
return die(`No ${extension} found in ${directory} directory.`);
|
||||||
}
|
}
|
||||||
if (withoutUninstaller.length > 1) {
|
if (withoutUninstaller.length > 1) {
|
||||||
log(`Found multiple ${extension} files: ${withoutUninstaller.join(', ')}. Using first occurrence`, LOG_LEVELS.WARN);
|
log(`Found multiple ${extension} files: ${withoutUninstaller.join(', ')}. Using first occurrence`, LogLevel.Warn);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
absolutePath: join(directory, withoutUninstaller[0]),
|
absolutePath: join(directory, withoutUninstaller[0]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exists(path) {
|
interface FileSearchResult {
|
||||||
|
readonly absolutePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exists(path: string): Promise<boolean> {
|
||||||
if (!path) { throw new Error('Missing path'); }
|
if (!path) { throw new Error('Missing path'); }
|
||||||
try {
|
try {
|
||||||
await access(path, constants.F_OK);
|
await access(path, constants.F_OK);
|
||||||
@@ -38,7 +44,7 @@ export async function exists(path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isDirMissingOrEmpty(dir) {
|
export async function isDirMissingOrEmpty(dir: string): Promise<boolean> {
|
||||||
if (!dir) { throw new Error('Missing directory'); }
|
if (!dir) { throw new Error('Missing directory'); }
|
||||||
if (!await exists(dir)) {
|
if (!await exists(dir)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
export const LOG_LEVELS = Object.freeze({
|
|
||||||
INFO: 'INFO',
|
|
||||||
WARN: 'WARN',
|
|
||||||
ERROR: 'ERROR',
|
|
||||||
});
|
|
||||||
|
|
||||||
export function log(message, level = LOG_LEVELS.INFO) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const config = LOG_LEVEL_CONFIG[level] || LOG_LEVEL_CONFIG[LOG_LEVELS.INFO];
|
|
||||||
const formattedMessage = `[${timestamp}][${config.color}${level}${COLOR_CODES.RESET}] ${message}`;
|
|
||||||
config.method(formattedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function die(message) {
|
|
||||||
log(message, LOG_LEVELS.ERROR);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLOR_CODES = {
|
|
||||||
RESET: '\x1b[0m',
|
|
||||||
LIGHT_RED: '\x1b[91m',
|
|
||||||
YELLOW: '\x1b[33m',
|
|
||||||
LIGHT_BLUE: '\x1b[94m',
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOG_LEVEL_CONFIG = {
|
|
||||||
[LOG_LEVELS.INFO]: {
|
|
||||||
color: COLOR_CODES.LIGHT_BLUE,
|
|
||||||
method: console.log,
|
|
||||||
},
|
|
||||||
[LOG_LEVELS.WARN]: {
|
|
||||||
color: COLOR_CODES.YELLOW,
|
|
||||||
method: console.warn,
|
|
||||||
},
|
|
||||||
[LOG_LEVELS.ERROR]: {
|
|
||||||
color: COLOR_CODES.LIGHT_RED,
|
|
||||||
method: console.error,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
68
scripts/check-desktop-runtime-errors/utils/log.ts
Normal file
68
scripts/check-desktop-runtime-errors/utils/log.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
export enum LogLevel {
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log(message: string, level = LogLevel.Info): void {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const config = LOG_LEVEL_CONFIG[level] || LOG_LEVEL_CONFIG[LogLevel.Info];
|
||||||
|
const logLevelText = `${getColorCode(config.color)}${LOG_LEVEL_LABELS[level]}${getColorCode(TextColor.Reset)}`;
|
||||||
|
const formattedMessage = `[${timestamp}][${logLevelText}] ${message}`;
|
||||||
|
config.method(formattedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function die(message: string): never {
|
||||||
|
log(message, LogLevel.Error);
|
||||||
|
return process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TextColor {
|
||||||
|
Reset,
|
||||||
|
LightRed,
|
||||||
|
Yellow,
|
||||||
|
LightBlue,
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorCode(color: TextColor): string {
|
||||||
|
return COLOR_CODE_MAPPING[color];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_LEVEL_LABELS: {
|
||||||
|
readonly [K in LogLevel]: string;
|
||||||
|
} = {
|
||||||
|
[LogLevel.Info]: 'INFO',
|
||||||
|
[LogLevel.Error]: 'ERROR',
|
||||||
|
[LogLevel.Warn]: 'WARN',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLOR_CODE_MAPPING: {
|
||||||
|
readonly [K in TextColor]: string;
|
||||||
|
} = {
|
||||||
|
[TextColor.Reset]: '\x1b[0m',
|
||||||
|
[TextColor.LightRed]: '\x1b[91m',
|
||||||
|
[TextColor.Yellow]: '\x1b[33m',
|
||||||
|
[TextColor.LightBlue]: '\x1b[94m',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ColorLevelConfig {
|
||||||
|
readonly color: TextColor;
|
||||||
|
readonly method: (...data: unknown[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_LEVEL_CONFIG: {
|
||||||
|
readonly [K in LogLevel]: ColorLevelConfig;
|
||||||
|
} = {
|
||||||
|
[LogLevel.Info]: {
|
||||||
|
color: TextColor.LightBlue,
|
||||||
|
method: console.log,
|
||||||
|
},
|
||||||
|
[LogLevel.Warn]: {
|
||||||
|
color: TextColor.Yellow,
|
||||||
|
method: console.warn,
|
||||||
|
},
|
||||||
|
[LogLevel.Error]: {
|
||||||
|
color: TextColor.LightRed,
|
||||||
|
method: console.error,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
import { rmdir, readFile } from 'fs/promises';
|
|
||||||
import { exists, isDirMissingOrEmpty } from './io.js';
|
|
||||||
import { runCommand } from './run-command.js';
|
|
||||||
import { LOG_LEVELS, die, log } from './log.js';
|
|
||||||
|
|
||||||
export async function ensureNpmProjectDir(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
if (!await exists(join(projectDir, 'package.json'))) {
|
|
||||||
die(`'package.json' not found in project directory: ${projectDir}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function npmInstall(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const npmModulesPath = join(projectDir, 'node_modules');
|
|
||||||
if (!await isDirMissingOrEmpty(npmModulesPath)) {
|
|
||||||
log(`Directory "${npmModulesPath}" exists and has content. Skipping \`npm install\`.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log('Starting dependency installation...');
|
|
||||||
const { error } = await runCommand('npm install --loglevel=error', {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: projectDir,
|
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
die(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function npmBuild(projectDir, buildCommand, distDir, forceRebuild) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
if (!buildCommand) { throw new Error('missing build command'); }
|
|
||||||
if (!distDir) { throw new Error('missing distribution directory'); }
|
|
||||||
|
|
||||||
const isMissingBuild = await isDirMissingOrEmpty(distDir);
|
|
||||||
|
|
||||||
if (!isMissingBuild && !forceRebuild) {
|
|
||||||
log(`Directory "${distDir}" exists and has content. Skipping build: '${buildCommand}'.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceRebuild) {
|
|
||||||
log(`Removing directory "${distDir}" for a clean build (triggered by --build flag).`);
|
|
||||||
await rmdir(distDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Starting project build...');
|
|
||||||
const { error } = await runCommand(buildCommand, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: projectDir,
|
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
log(error, LOG_LEVELS.WARN); // Cannot disable Vue CLI errors, stderr contains false-positives.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAppName(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const packageData = await readPackageJsonContents(projectDir);
|
|
||||||
try {
|
|
||||||
const packageJson = JSON.parse(packageData);
|
|
||||||
if (!packageJson.name) {
|
|
||||||
die(`The 'package.json' file doesn't specify a name: ${packageData}`);
|
|
||||||
}
|
|
||||||
return packageJson.name;
|
|
||||||
} catch (error) {
|
|
||||||
die(`Unable to parse 'package.json'. Error: ${error}\nContent: ${packageData}`, LOG_LEVELS.ERROR);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readPackageJsonContents(projectDir) {
|
|
||||||
if (!projectDir) { throw new Error('missing project directory'); }
|
|
||||||
const packagePath = join(projectDir, 'package.json');
|
|
||||||
if (!await exists(packagePath)) {
|
|
||||||
die(`'package.json' file not found at ${packagePath}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const packageData = await readFile(packagePath, 'utf8');
|
|
||||||
return packageData;
|
|
||||||
} catch (error) {
|
|
||||||
log(`Error reading 'package.json' from ${packagePath}.`, LOG_LEVELS.ERROR);
|
|
||||||
die(`Error detail: ${error}`, LOG_LEVELS.ERROR);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
120
scripts/check-desktop-runtime-errors/utils/npm.ts
Normal file
120
scripts/check-desktop-runtime-errors/utils/npm.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { rm, readFile } from 'fs/promises';
|
||||||
|
import { exists, isDirMissingOrEmpty } from './io';
|
||||||
|
import { CommandResult, runCommand } from './run-command';
|
||||||
|
import { LogLevel, die, log } from './log';
|
||||||
|
import { sleep } from './sleep';
|
||||||
|
import type { ExecOptions } from 'child_process';
|
||||||
|
|
||||||
|
const NPM_INSTALL_MAX_RETRIES = 3;
|
||||||
|
const NPM_INSTALL_RETRY_DELAY_MS = 5 /* seconds */ * 1000;
|
||||||
|
|
||||||
|
export async function ensureNpmProjectDir(projectDir: string): Promise<void> {
|
||||||
|
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: string): Promise<void> {
|
||||||
|
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 executeWithRetry('npm install --loglevel=error', {
|
||||||
|
cwd: projectDir,
|
||||||
|
}, NPM_INSTALL_MAX_RETRIES, NPM_INSTALL_RETRY_DELAY_MS);
|
||||||
|
if (error) {
|
||||||
|
die(error);
|
||||||
|
}
|
||||||
|
log('Installed dependencies...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function npmBuild(
|
||||||
|
projectDir: string,
|
||||||
|
buildCommand: string,
|
||||||
|
distDir: string,
|
||||||
|
forceRebuild: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
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 rm(distDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Building project...');
|
||||||
|
const { error } = await runCommand(buildCommand, {
|
||||||
|
cwd: projectDir,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
log(error, LogLevel.Warn); // Cannot disable Vue CLI errors, stderr contains false-positives.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAppName(projectDir: string): Promise<string> {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const packageData = await readPackageJsonContents(projectDir);
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(packageData);
|
||||||
|
if (!packageJson.name) {
|
||||||
|
return die(`The \`package.json\` file doesn't specify a name: ${packageData}`);
|
||||||
|
}
|
||||||
|
return packageJson.name;
|
||||||
|
} catch (error) {
|
||||||
|
return die(`Unable to parse \`package.json\`. Error: ${error}\nContent: ${packageData}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPackageJsonContents(projectDir: string): Promise<string> {
|
||||||
|
if (!projectDir) { throw new Error('missing project directory'); }
|
||||||
|
const packagePath = join(projectDir, 'package.json');
|
||||||
|
if (!await exists(packagePath)) {
|
||||||
|
return 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}.`, LogLevel.Error);
|
||||||
|
return die(`Error detail: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeWithRetry(
|
||||||
|
command: string,
|
||||||
|
options: ExecOptions,
|
||||||
|
maxRetries: number,
|
||||||
|
retryDelayInMs: number,
|
||||||
|
currentAttempt = 1,
|
||||||
|
): Promise<CommandResult> {
|
||||||
|
const result = await runCommand(command, options);
|
||||||
|
|
||||||
|
if (!result.error || currentAttempt >= maxRetries) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Attempt ${currentAttempt} failed. Retrying in ${retryDelayInMs / 1000} seconds...`);
|
||||||
|
await sleep(retryDelayInMs);
|
||||||
|
|
||||||
|
const retryResult = await executeWithRetry(
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
maxRetries,
|
||||||
|
retryDelayInMs,
|
||||||
|
currentAttempt + 1,
|
||||||
|
);
|
||||||
|
return retryResult;
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { platform } from 'os';
|
|
||||||
|
|
||||||
export const SUPPORTED_PLATFORMS = {
|
|
||||||
MAC: 'darwin',
|
|
||||||
LINUX: 'linux',
|
|
||||||
WINDOWS: 'win32',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CURRENT_PLATFORM = platform();
|
|
||||||
31
scripts/check-desktop-runtime-errors/utils/platform.ts
Normal file
31
scripts/check-desktop-runtime-errors/utils/platform.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { platform } from 'os';
|
||||||
|
import { die } from './log';
|
||||||
|
|
||||||
|
export enum SupportedPlatform {
|
||||||
|
macOS,
|
||||||
|
Windows,
|
||||||
|
Linux,
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_PLATFORM_MAPPINGS: {
|
||||||
|
readonly [K in SupportedPlatform]: NodeJS.Platform;
|
||||||
|
} = {
|
||||||
|
[SupportedPlatform.macOS]: 'darwin',
|
||||||
|
[SupportedPlatform.Linux]: 'linux',
|
||||||
|
[SupportedPlatform.Windows]: 'win32',
|
||||||
|
};
|
||||||
|
|
||||||
|
function findCurrentPlatform(): SupportedPlatform | undefined {
|
||||||
|
const nodePlatform = platform();
|
||||||
|
|
||||||
|
for (const key of Object.keys(NODE_PLATFORM_MAPPINGS)) {
|
||||||
|
const keyAsSupportedPlatform = parseInt(key, 10) as SupportedPlatform;
|
||||||
|
if (NODE_PLATFORM_MAPPINGS[keyAsSupportedPlatform] === nodePlatform) {
|
||||||
|
return keyAsSupportedPlatform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return die(`Unsupported platform: ${nodePlatform}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CURRENT_PLATFORM: SupportedPlatform = findCurrentPlatform();
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { indentText } from './text.js';
|
import { indentText } from './text';
|
||||||
|
import type { ExecOptions, ExecException } from 'child_process';
|
||||||
|
|
||||||
const TIMEOUT_IN_SECONDS = 180;
|
const TIMEOUT_IN_SECONDS = 180;
|
||||||
const MAX_OUTPUT_BUFFER_SIZE = 1024 * 1024; // 1 MB
|
const MAX_OUTPUT_BUFFER_SIZE = 1024 * 1024; // 1 MB
|
||||||
|
|
||||||
export function runCommand(commandString, options) {
|
export function runCommand(
|
||||||
|
command: string,
|
||||||
|
options?: ExecOptions,
|
||||||
|
): Promise<CommandResult> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
options = {
|
options = {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
timeout: TIMEOUT_IN_SECONDS * 1000,
|
timeout: TIMEOUT_IN_SECONDS * 1000,
|
||||||
maxBuffer: MAX_OUTPUT_BUFFER_SIZE * 2,
|
maxBuffer: MAX_OUTPUT_BUFFER_SIZE * 2,
|
||||||
...options,
|
...(options ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
exec(commandString, options, (error, stdout, stderr) => {
|
exec(command, options, (error, stdout, stderr) => {
|
||||||
let errorText;
|
let errorText: string | undefined;
|
||||||
if (error || stderr?.length > 0) {
|
if (error || stderr?.length > 0) {
|
||||||
errorText = formatError(commandString, error, stdout, stderr);
|
errorText = formatError(command, error, stdout, stderr);
|
||||||
}
|
}
|
||||||
resolve({
|
resolve({
|
||||||
stdout,
|
stdout,
|
||||||
@@ -26,18 +30,28 @@ export function runCommand(commandString, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatError(commandString, error, stdout, stderr) {
|
export interface CommandResult {
|
||||||
|
readonly stdout: string;
|
||||||
|
readonly error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(
|
||||||
|
command: string,
|
||||||
|
error: ExecException | undefined,
|
||||||
|
stdout: string | undefined,
|
||||||
|
stderr: string | undefined,
|
||||||
|
) {
|
||||||
const errorParts = [
|
const errorParts = [
|
||||||
'Error while running command.',
|
'Error while running command.',
|
||||||
`Command:\n${indentText(commandString, 1)}`,
|
`Command:\n${indentText(command, 1)}`,
|
||||||
];
|
];
|
||||||
if (error?.toString().trim()) {
|
if (error?.toString().trim()) {
|
||||||
errorParts.push(`Error:\n${indentText(error.toString(), 1)}`);
|
errorParts.push(`Error:\n${indentText(error.toString(), 1)}`);
|
||||||
}
|
}
|
||||||
if (stderr?.toString().trim()) {
|
if (stderr?.trim()) {
|
||||||
errorParts.push(`stderr:\n${indentText(stderr, 1)}`);
|
errorParts.push(`stderr:\n${indentText(stderr, 1)}`);
|
||||||
}
|
}
|
||||||
if (stdout?.toString().trim()) {
|
if (stdout?.trim()) {
|
||||||
errorParts.push(`stdout:\n${indentText(stdout, 1)}`);
|
errorParts.push(`stdout:\n${indentText(stdout, 1)}`);
|
||||||
}
|
}
|
||||||
return errorParts.join('\n---\n');
|
return errorParts.join('\n---\n');
|
||||||
5
scripts/check-desktop-runtime-errors/utils/sleep.ts
Normal file
5
scripts/check-desktop-runtime-errors/utils/sleep.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export function sleep(milliseconds: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, milliseconds);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
export function indentText(text, indentLevel = 1) {
|
export function indentText(
|
||||||
|
text: string,
|
||||||
|
indentLevel = 1,
|
||||||
|
): string {
|
||||||
validateText(text);
|
validateText(text);
|
||||||
const indentation = '\t'.repeat(indentLevel);
|
const indentation = '\t'.repeat(indentLevel);
|
||||||
return splitTextIntoLines(text)
|
return splitTextIntoLines(text)
|
||||||
@@ -6,13 +9,13 @@ export function indentText(text, indentLevel = 1) {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitTextIntoLines(text) {
|
export function splitTextIntoLines(text: string): string[] {
|
||||||
validateText(text);
|
validateText(text);
|
||||||
return text
|
return text
|
||||||
.split(/[\r\n]+/);
|
.split(/[\r\n]+/);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateText(text) {
|
function validateText(text: string): void {
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== 'string') {
|
||||||
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
|
throw new Error(`text is not a string. It is: ${typeof text}\n${text}`);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export type Constructible<T, TArgs extends unknown[] = never> = {
|
export type Constructible<T, TArgs extends unknown[] = never> = {
|
||||||
prototype: T;
|
prototype: T;
|
||||||
apply: (this: unknown, args: TArgs) => void;
|
apply: (this: unknown, args: TArgs) => void;
|
||||||
|
readonly name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropertyKeys<T> = {
|
export type PropertyKeys<T> = {
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
|
||||||
import { IApplicationFactory } from '../IApplicationFactory';
|
import { IApplicationFactory } from '../IApplicationFactory';
|
||||||
import { ApplicationFactory } from '../ApplicationFactory';
|
import { ApplicationFactory } from '../ApplicationFactory';
|
||||||
import { ApplicationContext } from './ApplicationContext';
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
|
||||||
export async function buildContext(
|
export async function buildContext(
|
||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = Environment.CurrentEnvironment,
|
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
): Promise<IApplicationContext> {
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('missing factory'); }
|
if (!factory) { throw new Error('missing factory'); }
|
||||||
if (!environment) { throw new Error('missing environment'); }
|
if (!environment) { throw new Error('missing environment'); }
|
||||||
const app = await factory.getApp();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment);
|
const os = getInitialOs(app, environment.os);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
||||||
const currentOs = environment.os;
|
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import MacOsData from '@/application/collections/macos.yaml';
|
|||||||
import LinuxData from '@/application/collections/linux.yaml';
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
categoryParser = parseCategoryCollection,
|
categoryParser = parseCategoryCollection,
|
||||||
informationParser = parseProjectInformation,
|
informationParser = parseProjectInformation,
|
||||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||||
collectionsData = PreParsedCollections,
|
collectionsData = PreParsedCollections,
|
||||||
): IApplication {
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData);
|
validateCollectionsData(collectionsData);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
import { Version } from '@/domain/Version';
|
import { Version } from '@/domain/Version';
|
||||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { ConstructorArguments } from '@/TypeHelpers';
|
import { ConstructorArguments } from '@/TypeHelpers';
|
||||||
|
|
||||||
export function
|
export function
|
||||||
parseProjectInformation(
|
parseProjectInformation(
|
||||||
metadata: IAppMetadata = AppMetadataFactory.Current.instance,
|
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||||
createProjectInformation: ProjectInformationFactory = (
|
createProjectInformation: ProjectInformationFactory = (
|
||||||
...args
|
...args
|
||||||
) => new ProjectInformation(...args),
|
) => new ProjectInformation(...args),
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations';
|
||||||
|
|
||||||
export class CodeRunner {
|
export class CodeRunner {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly environment = Environment.CurrentEnvironment,
|
private readonly system = getWindowInjectedSystemOperations(),
|
||||||
|
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
) {
|
) {
|
||||||
if (!environment.system) {
|
if (!system) {
|
||||||
throw new Error('missing system operations');
|
throw new Error('missing system operations');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||||
const { system } = this.environment;
|
const { os } = this.environment;
|
||||||
const dir = system.location.combinePaths(
|
const dir = this.system.location.combinePaths(
|
||||||
system.operatingSystem.getTempDirectory(),
|
this.system.operatingSystem.getTempDirectory(),
|
||||||
folderName,
|
folderName,
|
||||||
);
|
);
|
||||||
await system.fileSystem.createDirectory(dir, true);
|
await this.system.fileSystem.createDirectory(dir, true);
|
||||||
const filePath = system.location.combinePaths(dir, `run.${fileExtension}`);
|
const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`);
|
||||||
await system.fileSystem.writeToFile(filePath, code);
|
await this.system.fileSystem.writeToFile(filePath, code);
|
||||||
await system.fileSystem.setFilePermissions(filePath, '755');
|
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||||
const command = getExecuteCommand(filePath, this.environment);
|
const command = getExecuteCommand(filePath, os);
|
||||||
system.command.execute(command);
|
this.system.command.execute(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
function getExecuteCommand(
|
||||||
switch (environment.os) {
|
scriptPath: string,
|
||||||
|
currentOperatingSystem: OperatingSystem,
|
||||||
|
): string {
|
||||||
|
switch (currentOperatingSystem) {
|
||||||
case OperatingSystem.Linux:
|
case OperatingSystem.Linux:
|
||||||
return `x-terminal-emulator -e '${scriptPath}'`;
|
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||||
case OperatingSystem.macOS:
|
case OperatingSystem.macOS:
|
||||||
@@ -37,6 +42,6 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
|
|||||||
case OperatingSystem.Windows:
|
case OperatingSystem.Windows:
|
||||||
return scriptPath;
|
return scriptPath;
|
||||||
default:
|
default:
|
||||||
throw Error(`unsupported os: ${OperatingSystem[environment.os]}`);
|
throw Error(`unsupported os: ${OperatingSystem[currentOperatingSystem]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
|
||||||
|
|
||||||
export interface IEnvironment {
|
|
||||||
readonly isDesktop: boolean;
|
|
||||||
readonly os: OperatingSystem | undefined;
|
|
||||||
readonly system: ISystemOperations | undefined;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { ISystemOperations } from './SystemOperations/ISystemOperations';
|
|
||||||
|
|
||||||
export type WindowVariables = {
|
|
||||||
system: ISystemOperations;
|
|
||||||
isDesktop: boolean;
|
|
||||||
os: OperatingSystem;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
interface Window extends WindowVariables { }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { IEnvironmentVariablesFactory } from './IEnvironmentVariablesFactory';
|
||||||
|
import { validateEnvironmentVariables } from './EnvironmentVariablesValidator';
|
||||||
|
import { ViteEnvironmentVariables } from './Vite/ViteEnvironmentVariables';
|
||||||
|
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||||
|
|
||||||
|
export class EnvironmentVariablesFactory implements IEnvironmentVariablesFactory {
|
||||||
|
public static readonly Current = new EnvironmentVariablesFactory();
|
||||||
|
|
||||||
|
public readonly instance: IEnvironmentVariables;
|
||||||
|
|
||||||
|
protected constructor(validator: EnvironmentVariablesValidator = validateEnvironmentVariables) {
|
||||||
|
const environment = new ViteEnvironmentVariables();
|
||||||
|
validator(environment);
|
||||||
|
this.instance = environment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnvironmentVariablesValidator = typeof validateEnvironmentVariables;
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||||
|
|
||||||
/* Validation is externalized to keep the environment objects simple */
|
/* Validation is externalized to keep the environment objects simple */
|
||||||
export function validateMetadata(metadata: IAppMetadata): void {
|
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
||||||
if (!metadata) {
|
if (!environment) {
|
||||||
throw new Error('missing metadata');
|
throw new Error('missing environment');
|
||||||
}
|
}
|
||||||
const keyValues = capturePropertyValues(metadata);
|
const keyValues = capturePropertyValues(environment);
|
||||||
if (!Object.keys(keyValues).length) {
|
if (!Object.keys(keyValues).length) {
|
||||||
throw new Error('Unable to capture metadata key/value pairs');
|
throw new Error('Unable to capture key/value pairs');
|
||||||
}
|
}
|
||||||
const keysMissingValue = getMissingMetadataKeys(keyValues);
|
const keysMissingValue = getKeysMissingValues(keyValues);
|
||||||
if (keysMissingValue.length > 0) {
|
if (keysMissingValue.length > 0) {
|
||||||
throw new Error(`Metadata keys missing: ${keysMissingValue.join(', ')}`);
|
throw new Error(`Environment keys missing: ${keysMissingValue.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMissingMetadataKeys(keyValuePairs: Record<string, unknown>): string[] {
|
function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[] {
|
||||||
return Object.entries(keyValuePairs)
|
return Object.entries(keyValuePairs)
|
||||||
.reduce((acc, [key, value]) => {
|
.reduce((acc, [key, value]) => {
|
||||||
if (!value) {
|
if (!value && typeof value !== 'boolean') {
|
||||||
acc.push(key);
|
acc.push(key);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Represents essential metadata about the application.
|
* Represents essential metadata about the application.
|
||||||
*
|
|
||||||
* Designed to decouple the process of retrieving metadata
|
|
||||||
* (e.g., from the build environment) from the rest of the application.
|
|
||||||
*/
|
*/
|
||||||
export interface IAppMetadata {
|
export interface IAppMetadata {
|
||||||
readonly version: string;
|
readonly version: string;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IAppMetadata } from './IAppMetadata';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Designed to decouple the process of retrieving environment variables
|
||||||
|
* (e.g., from the build environment) from the rest of the application.
|
||||||
|
*/
|
||||||
|
export interface IEnvironmentVariables extends IAppMetadata {
|
||||||
|
readonly isNonProduction: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { IEnvironmentVariables } from './IEnvironmentVariables';
|
||||||
|
|
||||||
|
export interface IEnvironmentVariablesFactory {
|
||||||
|
readonly instance: IEnvironmentVariables;
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
// Only variables prefixed with VITE_ are exposed to Vite-processed code
|
// Only variables prefixed with VITE_ are exposed to Vite-processed code
|
||||||
export const VITE_ENVIRONMENT_KEYS = {
|
export const VITE_USER_DEFINED_ENVIRONMENT_KEYS = {
|
||||||
VERSION: 'VITE_APP_VERSION',
|
VERSION: 'VITE_APP_VERSION',
|
||||||
NAME: 'VITE_APP_NAME',
|
NAME: 'VITE_APP_NAME',
|
||||||
SLOGAN: 'VITE_APP_SLOGAN',
|
SLOGAN: 'VITE_APP_SLOGAN',
|
||||||
REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL',
|
REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL',
|
||||||
HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL',
|
HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const VITE_ENVIRONMENT_KEYS = {
|
||||||
|
...VITE_USER_DEFINED_ENVIRONMENT_KEYS,
|
||||||
|
DEV: 'DEV',
|
||||||
|
} as const;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IAppMetadata } from '../IAppMetadata';
|
import { IEnvironmentVariables } from '../IEnvironmentVariables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the application's metadata using Vite's environment variables.
|
* Provides the application's environment variables.
|
||||||
*/
|
*/
|
||||||
export class ViteAppMetadata implements IAppMetadata {
|
export class ViteEnvironmentVariables implements IEnvironmentVariables {
|
||||||
// Ensure the use of import.meta.env prefix for the following properties.
|
// Ensure the use of import.meta.env prefix for the following properties.
|
||||||
// Vue will replace these statically during production builds.
|
// Vue will replace these statically during production builds.
|
||||||
|
|
||||||
@@ -26,4 +26,8 @@ export class ViteAppMetadata implements IAppMetadata {
|
|||||||
public get homepageUrl(): string {
|
public get homepageUrl(): string {
|
||||||
return import.meta.env.VITE_APP_HOMEPAGE_URL;
|
return import.meta.env.VITE_APP_HOMEPAGE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isNonProduction(): boolean {
|
||||||
|
return import.meta.env.DEV;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
13
src/infrastructure/Log/ConsoleLogger.ts
Normal file
13
src/infrastructure/Log/ConsoleLogger.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export class ConsoleLogger implements ILogger {
|
||||||
|
constructor(private readonly globalConsole: Partial<Console> = console) {
|
||||||
|
if (!globalConsole) {
|
||||||
|
throw new Error('missing console');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(...params: unknown[]): void {
|
||||||
|
this.globalConsole.info(...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/infrastructure/Log/ElectronLogger.ts
Normal file
12
src/infrastructure/Log/ElectronLogger.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ElectronLog } from 'electron-log';
|
||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
// Using plain-function rather than class so it can be used in Electron's context-bridging.
|
||||||
|
export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
|
||||||
|
if (!logger) {
|
||||||
|
throw new Error('missing logger');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
info: (...params) => logger.info(...params),
|
||||||
|
};
|
||||||
|
}
|
||||||
3
src/infrastructure/Log/ILogger.ts
Normal file
3
src/infrastructure/Log/ILogger.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ILogger {
|
||||||
|
info (...params: unknown[]): void;
|
||||||
|
}
|
||||||
5
src/infrastructure/Log/ILoggerFactory.ts
Normal file
5
src/infrastructure/Log/ILoggerFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export interface ILoggerFactory {
|
||||||
|
readonly logger: ILogger;
|
||||||
|
}
|
||||||
5
src/infrastructure/Log/NoopLogger.ts
Normal file
5
src/infrastructure/Log/NoopLogger.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export class NoopLogger implements ILogger {
|
||||||
|
public info(): void { /* NOOP */ }
|
||||||
|
}
|
||||||
20
src/infrastructure/Log/WindowInjectedLogger.ts
Normal file
20
src/infrastructure/Log/WindowInjectedLogger.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||||
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
|
export class WindowInjectedLogger implements ILogger {
|
||||||
|
private readonly logger: ILogger;
|
||||||
|
|
||||||
|
constructor(windowVariables: WindowVariables = window) {
|
||||||
|
if (!windowVariables) {
|
||||||
|
throw new Error('missing window');
|
||||||
|
}
|
||||||
|
if (!windowVariables.log) {
|
||||||
|
throw new Error('missing log');
|
||||||
|
}
|
||||||
|
this.logger = windowVariables.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(...params: unknown[]): void {
|
||||||
|
this.logger.info(...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { IAppMetadata } from './IAppMetadata';
|
|
||||||
import { IAppMetadataFactory } from './IAppMetadataFactory';
|
|
||||||
import { validateMetadata } from './MetadataValidator';
|
|
||||||
import { ViteAppMetadata } from './Vite/ViteAppMetadata';
|
|
||||||
|
|
||||||
export class AppMetadataFactory implements IAppMetadataFactory {
|
|
||||||
public static readonly Current = new AppMetadataFactory();
|
|
||||||
|
|
||||||
public readonly instance: IAppMetadata;
|
|
||||||
|
|
||||||
protected constructor(validator: MetadataValidator = validateMetadata) {
|
|
||||||
const metadata = new ViteAppMetadata();
|
|
||||||
validator(metadata);
|
|
||||||
this.instance = metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MetadataValidator = typeof validateMetadata;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { IAppMetadata } from './IAppMetadata';
|
|
||||||
|
|
||||||
export interface IAppMetadataFactory {
|
|
||||||
readonly instance: IAppMetadata;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export interface IRuntimeEnvironment {
|
||||||
|
readonly isDesktop: boolean;
|
||||||
|
readonly os: OperatingSystem | undefined;
|
||||||
|
readonly isNonProduction: boolean;
|
||||||
|
}
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
|
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||||
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
import { IEnvironment } from './IEnvironment';
|
import { IRuntimeEnvironment } from './IRuntimeEnvironment';
|
||||||
import { WindowVariables } from './WindowVariables';
|
|
||||||
import { validateWindowVariables } from './WindowVariablesValidator';
|
|
||||||
|
|
||||||
export class Environment implements IEnvironment {
|
export class RuntimeEnvironment implements IRuntimeEnvironment {
|
||||||
public static readonly CurrentEnvironment: IEnvironment = new Environment(window);
|
public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window);
|
||||||
|
|
||||||
public readonly isDesktop: boolean;
|
public readonly isDesktop: boolean;
|
||||||
|
|
||||||
public readonly os: OperatingSystem | undefined;
|
public readonly os: OperatingSystem | undefined;
|
||||||
|
|
||||||
public readonly system: ISystemOperations | undefined;
|
public readonly isNonProduction: boolean;
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
window: Partial<Window>,
|
window: Partial<Window>,
|
||||||
|
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance,
|
||||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||||
windowValidator: WindowValidator = validateWindowVariables,
|
|
||||||
) {
|
) {
|
||||||
if (!window) {
|
if (!window) {
|
||||||
throw new Error('missing window');
|
throw new Error('missing window');
|
||||||
}
|
}
|
||||||
windowValidator(window);
|
this.isNonProduction = environmentVariables.isNonProduction;
|
||||||
this.isDesktop = isDesktop(window);
|
this.isDesktop = isDesktop(window);
|
||||||
if (this.isDesktop) {
|
if (this.isDesktop) {
|
||||||
this.os = window?.os;
|
this.os = window?.os;
|
||||||
@@ -34,7 +34,6 @@ export class Environment implements IEnvironment {
|
|||||||
this.os = browserOsDetector.detect(userAgent);
|
this.os = browserOsDetector.detect(userAgent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.system = window?.system;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,5 +44,3 @@ function getUserAgent(window: Partial<Window>): string {
|
|||||||
function isDesktop(window: Partial<WindowVariables>): boolean {
|
function isDesktop(window: Partial<WindowVariables>): boolean {
|
||||||
return window?.isDesktop === true;
|
return window?.isDesktop === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WindowValidator = typeof validateWindowVariables;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface ISanityCheckOptions {
|
export interface ISanityCheckOptions {
|
||||||
readonly validateMetadata: boolean;
|
readonly validateEnvironmentVariables: boolean;
|
||||||
readonly validateEnvironment: boolean;
|
readonly validateWindowVariables: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
import { ISanityCheckOptions } from './Common/ISanityCheckOptions';
|
||||||
import { ISanityValidator } from './Common/ISanityValidator';
|
import { ISanityValidator } from './Common/ISanityValidator';
|
||||||
import { MetadataValidator } from './Validators/MetadataValidator';
|
import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator';
|
||||||
|
|
||||||
const DefaultSanityValidators: ISanityValidator[] = [
|
const DefaultSanityValidators: ISanityValidator[] = [
|
||||||
new MetadataValidator(),
|
new EnvironmentVariablesValidator(),
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Helps to fail-fast on errors */
|
/* Helps to fail-fast on errors */
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
|
||||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
|
||||||
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
|
||||||
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
|
||||||
|
|
||||||
export class EnvironmentValidator extends FactoryValidator<IEnvironment> {
|
|
||||||
constructor(factory: FactoryFunction<IEnvironment> = () => Environment.CurrentEnvironment) {
|
|
||||||
super(factory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override name = 'environment';
|
|
||||||
|
|
||||||
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
|
||||||
return options.validateEnvironment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
|
||||||
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
|
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||||
|
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
||||||
|
|
||||||
|
export class EnvironmentVariablesValidator extends FactoryValidator<IEnvironmentVariables> {
|
||||||
|
constructor(
|
||||||
|
factory: FactoryFunction<IEnvironmentVariables> = () => {
|
||||||
|
return EnvironmentVariablesFactory.Current.instance;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
super(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override name = 'environment variables';
|
||||||
|
|
||||||
|
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||||
|
return options.validateEnvironmentVariables;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
|
||||||
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
|
|
||||||
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
|
||||||
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
|
||||||
|
|
||||||
export class MetadataValidator extends FactoryValidator<IAppMetadata> {
|
|
||||||
constructor(factory: FactoryFunction<IAppMetadata> = () => AppMetadataFactory.Current.instance) {
|
|
||||||
super(factory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override name = 'metadata';
|
|
||||||
|
|
||||||
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
|
||||||
return options.validateMetadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
|
import { ISanityCheckOptions } from '../Common/ISanityCheckOptions';
|
||||||
|
import { FactoryValidator, FactoryFunction } from '../Common/FactoryValidator';
|
||||||
|
|
||||||
|
export class WindowVariablesValidator extends FactoryValidator<WindowVariables> {
|
||||||
|
constructor(factory: FactoryFunction<WindowVariables> = () => window) {
|
||||||
|
super(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override name = 'window variables';
|
||||||
|
|
||||||
|
public override shouldValidate(options: ISanityCheckOptions): boolean {
|
||||||
|
return options.validateWindowVariables;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { WindowVariables } from '../WindowVariables/WindowVariables';
|
||||||
|
import { ISystemOperations } from './ISystemOperations';
|
||||||
|
|
||||||
|
export function getWindowInjectedSystemOperations(
|
||||||
|
windowVariables: Partial<WindowVariables> = window,
|
||||||
|
): ISystemOperations {
|
||||||
|
if (!windowVariables) {
|
||||||
|
throw new Error('missing window');
|
||||||
|
}
|
||||||
|
if (!windowVariables.system) {
|
||||||
|
throw new Error('missing system');
|
||||||
|
}
|
||||||
|
return windowVariables.system;
|
||||||
|
}
|
||||||
11
src/infrastructure/WindowVariables/WindowVariables.ts
Normal file
11
src/infrastructure/WindowVariables/WindowVariables.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||||
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
|
|
||||||
|
/* Primary entry point for platform-specific injections */
|
||||||
|
export interface WindowVariables {
|
||||||
|
readonly system: ISystemOperations;
|
||||||
|
readonly isDesktop: boolean;
|
||||||
|
readonly os: OperatingSystem;
|
||||||
|
readonly log: ILogger;
|
||||||
|
}
|
||||||
@@ -6,11 +6,8 @@ import { WindowVariables } from './WindowVariables';
|
|||||||
* Checks for consistency in runtime environment properties injected by Electron preloader.
|
* Checks for consistency in runtime environment properties injected by Electron preloader.
|
||||||
*/
|
*/
|
||||||
export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||||
if (!variables) {
|
|
||||||
throw new Error('missing variables');
|
|
||||||
}
|
|
||||||
if (!isObject(variables)) {
|
if (!isObject(variables)) {
|
||||||
throw new Error(`window is not an object but ${typeof variables}`);
|
throw new Error('window is not an object');
|
||||||
}
|
}
|
||||||
const errors = [...testEveryProperty(variables)];
|
const errors = [...testEveryProperty(variables)];
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
@@ -25,6 +22,7 @@ function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<strin
|
|||||||
os: testOperatingSystem(variables.os),
|
os: testOperatingSystem(variables.os),
|
||||||
isDesktop: testIsDesktop(variables.isDesktop),
|
isDesktop: testIsDesktop(variables.isDesktop),
|
||||||
system: testSystem(variables),
|
system: testSystem(variables),
|
||||||
|
log: testLogger(variables),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [propertyName, testResult] of Object.entries(tests)) {
|
for (const [propertyName, testResult] of Object.entries(tests)) {
|
||||||
@@ -47,11 +45,18 @@ function testOperatingSystem(os: unknown): boolean {
|
|||||||
.includes(os);
|
.includes(os);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testLogger(variables: Partial<WindowVariables>): boolean {
|
||||||
|
if (!variables.isDesktop) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isObject(variables.log);
|
||||||
|
}
|
||||||
|
|
||||||
function testSystem(variables: Partial<WindowVariables>): boolean {
|
function testSystem(variables: Partial<WindowVariables>): boolean {
|
||||||
if (!variables.isDesktop) {
|
if (!variables.isDesktop) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return variables.system !== undefined && isObject(variables.system);
|
return isObject(variables.system);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testIsDesktop(isDesktop: unknown): boolean {
|
function testIsDesktop(isDesktop: unknown): boolean {
|
||||||
@@ -70,7 +75,7 @@ function isBoolean(variable: unknown): variable is boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isObject(variable: unknown): variable is object {
|
function isObject(variable: unknown): variable is object {
|
||||||
return typeof variable === 'object'
|
return Boolean(variable) // the data type of null is an object
|
||||||
&& variable !== null // the data type of null is an object
|
&& typeof variable === 'object'
|
||||||
&& !Array.isArray(variable);
|
&& !Array.isArray(variable);
|
||||||
}
|
}
|
||||||
6
src/infrastructure/WindowVariables/window-variables.d.ts
vendored
Normal file
6
src/infrastructure/WindowVariables/window-variables.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { WindowVariables } from './WindowVariables';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface Window extends WindowVariables { }
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import { IconBootstrapper } from './Modules/IconBootstrapper';
|
|||||||
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper';
|
||||||
import { VueBootstrapper } from './Modules/VueBootstrapper';
|
import { VueBootstrapper } from './Modules/VueBootstrapper';
|
||||||
import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
|
import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
|
||||||
|
import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator';
|
||||||
|
import { AppInitializationLogger } from './Modules/AppInitializationLogger';
|
||||||
|
|
||||||
export class ApplicationBootstrapper implements IVueBootstrapper {
|
export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||||
public bootstrap(vue: VueConstructor): void {
|
public bootstrap(vue: VueConstructor): void {
|
||||||
@@ -18,6 +20,8 @@ export class ApplicationBootstrapper implements IVueBootstrapper {
|
|||||||
new TreeBootstrapper(),
|
new TreeBootstrapper(),
|
||||||
new VueBootstrapper(),
|
new VueBootstrapper(),
|
||||||
new TooltipBootstrapper(),
|
new TooltipBootstrapper(),
|
||||||
|
new RuntimeSanityValidator(),
|
||||||
|
new AppInitializationLogger(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/presentation/bootstrapping/ClientLoggerFactory.ts
Normal file
25
src/presentation/bootstrapping/ClientLoggerFactory.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
|
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
||||||
|
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
||||||
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
|
import { ILoggerFactory } from '@/infrastructure/Log/ILoggerFactory';
|
||||||
|
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||||
|
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
||||||
|
|
||||||
|
export class ClientLoggerFactory implements ILoggerFactory {
|
||||||
|
public static readonly Current: ILoggerFactory = new ClientLoggerFactory();
|
||||||
|
|
||||||
|
public readonly logger: ILogger;
|
||||||
|
|
||||||
|
protected constructor(environment: IRuntimeEnvironment = RuntimeEnvironment.CurrentEnvironment) {
|
||||||
|
if (environment.isDesktop) {
|
||||||
|
this.logger = new WindowInjectedLogger();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (environment.isNonProduction) {
|
||||||
|
this.logger = new ConsoleLogger();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger = new NoopLogger();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,15 @@ import { InjectionKey, provide } from 'vue';
|
|||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
import {
|
import {
|
||||||
useCollectionStateKey, useApplicationKey, useEnvironmentKey,
|
useCollectionStateKey, useApplicationKey, useRuntimeEnvironmentKey,
|
||||||
} from '@/presentation/injectionSymbols';
|
} from '@/presentation/injectionSymbols';
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
|
|
||||||
export function provideDependencies(context: IApplicationContext) {
|
export function provideDependencies(context: IApplicationContext) {
|
||||||
registerSingleton(useApplicationKey, useApplication(context.app));
|
registerSingleton(useApplicationKey, useApplication(context.app));
|
||||||
registerTransient(useCollectionStateKey, () => useCollectionState(context));
|
registerTransient(useCollectionStateKey, () => useCollectionState(context));
|
||||||
registerSingleton(useEnvironmentKey, Environment.CurrentEnvironment);
|
registerSingleton(useRuntimeEnvironmentKey, RuntimeEnvironment.CurrentEnvironment);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerSingleton<T>(
|
function registerSingleton<T>(
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
|
import { IVueBootstrapper } from '../IVueBootstrapper';
|
||||||
|
import { ClientLoggerFactory } from '../ClientLoggerFactory';
|
||||||
|
|
||||||
|
export class AppInitializationLogger implements IVueBootstrapper {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: ILogger = ClientLoggerFactory.Current.logger,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public bootstrap(): void {
|
||||||
|
// Do not remove [APP_INIT]; it's a marker used in tests.
|
||||||
|
this.logger.info('[APP_INIT] Application is initialized.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
||||||
|
import { IVueBootstrapper } from '../IVueBootstrapper';
|
||||||
|
|
||||||
|
export class RuntimeSanityValidator implements IVueBootstrapper {
|
||||||
|
constructor(private readonly validator = validateRuntimeSanity) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public bootstrap(): void {
|
||||||
|
this.validator({
|
||||||
|
validateEnvironmentVariables: true,
|
||||||
|
validateWindowVariables: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeBu
|
|||||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
||||||
import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks';
|
|
||||||
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
import { provideDependencies } from '../bootstrapping/DependencyProvider';
|
||||||
|
|
||||||
const singletonAppContext = await buildContext();
|
const singletonAppContext = await buildContext();
|
||||||
@@ -33,10 +32,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
||||||
validateRuntimeSanity({
|
|
||||||
validateMetadata: true,
|
|
||||||
validateEnvironment: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,11 +29,10 @@
|
|||||||
import {
|
import {
|
||||||
defineComponent, ref, computed, inject,
|
defineComponent, ref, computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionStateKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
import { useCollectionStateKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
import { Environment } from '@/infrastructure/Environment/Environment';
|
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
@@ -56,10 +55,10 @@ export default defineComponent({
|
|||||||
const {
|
const {
|
||||||
currentState, currentContext, onStateChange, events,
|
currentState, currentContext, onStateChange, events,
|
||||||
} = inject(useCollectionStateKey)();
|
} = inject(useCollectionStateKey)();
|
||||||
const { isDesktop } = inject(useEnvironmentKey);
|
const { os, isDesktop } = inject(useRuntimeEnvironmentKey);
|
||||||
|
|
||||||
const areInstructionsVisible = ref(false);
|
const areInstructionsVisible = ref(false);
|
||||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop));
|
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
||||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||||
const hasCode = ref(false);
|
const hasCode = ref(false);
|
||||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||||
@@ -122,8 +121,12 @@ function getDownloadInstructions(
|
|||||||
return getInstructions(os, fileName);
|
return getInstructions(os, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanRunState(selectedOs: OperatingSystem, isDesktopVersion: boolean): boolean {
|
function getCanRunState(
|
||||||
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
|
selectedOs: OperatingSystem,
|
||||||
|
isDesktopVersion: boolean,
|
||||||
|
hostOs: OperatingSystem,
|
||||||
|
): boolean {
|
||||||
|
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||||
return isDesktopVersion && isRunningOnSelectedOs;
|
return isDesktopVersion && isRunningOnSelectedOs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
|
||||||
|
|
||||||
export function useEnvironment(environment: IEnvironment) {
|
|
||||||
if (!environment) {
|
|
||||||
throw new Error('missing environment');
|
|
||||||
}
|
|
||||||
return environment;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
|
||||||
|
|
||||||
|
export function useRuntimeEnvironment(environment: IRuntimeEnvironment) {
|
||||||
|
if (!environment) {
|
||||||
|
throw new Error('missing environment');
|
||||||
|
}
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, inject } from 'vue';
|
import { defineComponent, inject } from 'vue';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { useEnvironmentKey } from '@/presentation/injectionSymbols';
|
import { useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
||||||
|
|
||||||
const supportedOperativeSystems: readonly OperatingSystem[] = [
|
const supportedOperativeSystems: readonly OperatingSystem[] = [
|
||||||
@@ -34,7 +34,7 @@ export default defineComponent({
|
|||||||
DownloadUrlListItem,
|
DownloadUrlListItem,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { os: currentOs } = inject(useEnvironmentKey);
|
const { os: currentOs } = inject(useRuntimeEnvironmentKey);
|
||||||
const supportedDesktops = [
|
const supportedDesktops = [
|
||||||
...supportedOperativeSystems,
|
...supportedOperativeSystems,
|
||||||
].sort((os) => (os === currentOs ? 0 : 1));
|
].sort((os) => (os === currentOs ? 0 : 1));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
defineComponent, PropType, computed,
|
defineComponent, PropType, computed,
|
||||||
inject,
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useApplicationKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
import { useApplicationKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -26,7 +26,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { info } = inject(useApplicationKey);
|
const { info } = inject(useApplicationKey);
|
||||||
const { os: currentOs } = inject(useEnvironmentKey);
|
const { os: currentOs } = inject(useRuntimeEnvironmentKey);
|
||||||
|
|
||||||
const isCurrentOs = computed<boolean>(() => {
|
const isCurrentOs = computed<boolean>(() => {
|
||||||
return currentOs === props.operatingSystem;
|
return currentOs === props.operatingSystem;
|
||||||
|
|||||||
@@ -42,12 +42,12 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, inject } from 'vue';
|
import { defineComponent, computed, inject } from 'vue';
|
||||||
import { useApplicationKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
import { useApplicationKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const { info } = inject(useApplicationKey);
|
const { info } = inject(useApplicationKey);
|
||||||
const { isDesktop } = inject(useEnvironmentKey);
|
const { isDesktop } = inject(useRuntimeEnvironmentKey);
|
||||||
|
|
||||||
const repositoryUrl = computed<string>(() => info.repositoryUrl);
|
const repositoryUrl = computed<string>(() => info.repositoryUrl);
|
||||||
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
defineComponent, ref, computed, inject,
|
defineComponent, ref, computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
import { useApplicationKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
import { useApplicationKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import DownloadUrlList from './DownloadUrlList.vue';
|
import DownloadUrlList from './DownloadUrlList.vue';
|
||||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { info } = inject(useApplicationKey);
|
const { info } = inject(useApplicationKey);
|
||||||
const { isDesktop } = inject(useEnvironmentKey);
|
const { isDesktop } = inject(useRuntimeEnvironmentKey);
|
||||||
|
|
||||||
const isPrivacyDialogVisible = ref(false);
|
const isPrivacyDialogVisible = ref(false);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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';
|
||||||
@@ -29,7 +28,7 @@ export async function handleManualUpdate(info: UpdateInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTargetProject(targetVersion: string) {
|
function getTargetProject(targetVersion: string) {
|
||||||
const existingProject = parseProjectInformation(new ViteAppMetadata());
|
const existingProject = parseProjectInformation();
|
||||||
const targetProject = new ProjectInformation(
|
const targetProject = new ProjectInformation(
|
||||||
existingProject.name,
|
existingProject.name,
|
||||||
new Version(targetVersion),
|
new Version(targetVersion),
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
setupLogger();
|
setupLogger();
|
||||||
validateRuntimeSanity({
|
validateRuntimeSanity({
|
||||||
// Metadata is used by manual updates.
|
// Metadata is used by manual updates.
|
||||||
validateMetadata: true,
|
validateEnvironmentVariables: true,
|
||||||
|
|
||||||
// Environment is populated by the preload script and is in the renderer's context;
|
// Environment is populated by the preload script and is in the renderer's context;
|
||||||
// it's not directly accessible from the main process.
|
// it's not directly accessible from the main process.
|
||||||
validateEnvironment: false,
|
validateWindowVariables: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
@@ -151,7 +151,4 @@ function getWindowSize(idealWidth: number, idealHeight: number) {
|
|||||||
|
|
||||||
function setupLogger(): void {
|
function setupLogger(): void {
|
||||||
log.transports.file.level = 'silly';
|
log.transports.file.level = 'silly';
|
||||||
if (!isDevelopment) {
|
|
||||||
Object.assign(console, log.functions); // override console.log, console.warn etc.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { createNodeSystemOperations } from '@/infrastructure/Environment/SystemOperations/NodeSystemOperations';
|
import log from 'electron-log';
|
||||||
import { WindowVariables } from '@/infrastructure/Environment/WindowVariables';
|
import { createNodeSystemOperations } from '@/infrastructure/SystemOperations/NodeSystemOperations';
|
||||||
|
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
|
import { ILogger } from '@/infrastructure/Log/ILogger';
|
||||||
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
import { convertPlatformToOs } from './NodeOsMapper';
|
import { convertPlatformToOs } from './NodeOsMapper';
|
||||||
|
|
||||||
export function provideWindowVariables(
|
export function provideWindowVariables(
|
||||||
createSystem = createNodeSystemOperations,
|
createSystem = createNodeSystemOperations,
|
||||||
|
createLogger: () => ILogger = () => createElectronLogger(log),
|
||||||
convertToOs = convertPlatformToOs,
|
convertToOs = convertPlatformToOs,
|
||||||
): WindowVariables {
|
): WindowVariables {
|
||||||
return {
|
return {
|
||||||
system: createSystem(),
|
system: createSystem(),
|
||||||
isDesktop: true,
|
isDesktop: true,
|
||||||
|
log: createLogger(),
|
||||||
os: convertToOs(process.platform),
|
os: convertToOs(process.platform),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { provideWindowVariables } from './WindowVariablesProvider';
|
|||||||
validateRuntimeSanity({
|
validateRuntimeSanity({
|
||||||
// Validate metadata as a preventive measure for fail-fast,
|
// Validate metadata as a preventive measure for fail-fast,
|
||||||
// even if it's not currently used in the preload script.
|
// even if it's not currently used in the preload script.
|
||||||
validateMetadata: true,
|
validateEnvironmentVariables: true,
|
||||||
|
|
||||||
// The preload script cannot access variables on the window object.
|
// The preload script cannot access variables on the window object.
|
||||||
validateEnvironment: false,
|
validateWindowVariables: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const windowVariables = provideWindowVariables();
|
const windowVariables = provideWindowVariables();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
import { useEnvironment } from '@/presentation/components/Shared/Hooks/UseEnvironment';
|
import { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment';
|
||||||
import type { InjectionKey } from 'vue';
|
import type { InjectionKey } from 'vue';
|
||||||
|
|
||||||
export const useCollectionStateKey = defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState');
|
export const useCollectionStateKey = defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState');
|
||||||
export const useApplicationKey = defineSingletonKey<ReturnType<typeof useApplication>>('useApplication');
|
export const useApplicationKey = defineSingletonKey<ReturnType<typeof useApplication>>('useApplication');
|
||||||
export const useEnvironmentKey = defineSingletonKey<ReturnType<typeof useEnvironment>>('useEnvironment');
|
export const useRuntimeEnvironmentKey = defineSingletonKey<ReturnType<typeof useRuntimeEnvironment>>('useRuntimeEnvironment');
|
||||||
|
|
||||||
function defineSingletonKey<T>(key: string) {
|
function defineSingletonKey<T>(key: string) {
|
||||||
return Symbol(key) as InjectionKey<T>;
|
return Symbol(key) as InjectionKey<T>;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, it } from 'vitest';
|
||||||
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
|
|
||||||
|
describe('EnvironmentVariablesFactory', () => {
|
||||||
|
it('can read current environment', () => {
|
||||||
|
const environmentVariables = EnvironmentVariablesFactory.Current.instance;
|
||||||
|
Object.entries(environmentVariables).forEach(([key, value]) => {
|
||||||
|
it(`${key} value is defined`, () => {
|
||||||
|
expect(value).to.not.equal(undefined);
|
||||||
|
expect(value).to.not.equal(null);
|
||||||
|
expect(value).to.not.equal(Number.NaN);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { ViteAppMetadata } from '@/infrastructure/Metadata/Vite/ViteAppMetadata';
|
|
||||||
import packageJson from '@/../package.json' assert { type: 'json' };
|
import packageJson from '@/../package.json' assert { type: 'json' };
|
||||||
import { PropertyKeys } from '@/TypeHelpers';
|
import { PropertyKeys } from '@/TypeHelpers';
|
||||||
|
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
|
import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables';
|
||||||
|
|
||||||
describe('ViteAppMetadata', () => {
|
describe('ViteEnvironmentVariables', () => {
|
||||||
describe('populates from package.json', () => {
|
describe('populates metadata from package.json', () => {
|
||||||
interface ITestCase {
|
interface ITestCase {
|
||||||
readonly getActualValue: (sut: ViteAppMetadata) => string;
|
readonly getActualValue: (sut: IAppMetadata) => string;
|
||||||
readonly expected: string;
|
readonly expected: string;
|
||||||
}
|
}
|
||||||
const testCases: { readonly [K in PropertyKeys<ViteAppMetadata>]: ITestCase } = {
|
const testCases: { readonly [K in PropertyKeys<IAppMetadata>]: ITestCase } = {
|
||||||
name: {
|
name: {
|
||||||
expected: packageJson.name,
|
expected: packageJson.name,
|
||||||
getActualValue: (sut) => sut.name,
|
getActualValue: (sut) => sut.name,
|
||||||
@@ -34,7 +35,7 @@ describe('ViteAppMetadata', () => {
|
|||||||
Object.entries(testCases).forEach(([propertyName, { expected, getActualValue }]) => {
|
Object.entries(testCases).forEach(([propertyName, { expected, getActualValue }]) => {
|
||||||
it(`should correctly get the value of ${propertyName}`, () => {
|
it(`should correctly get the value of ${propertyName}`, () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new ViteAppMetadata();
|
const sut = new ViteEnvironmentVariables();
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const actualValue = getActualValue(sut);
|
const actualValue = getActualValue(sut);
|
||||||
@@ -22,8 +22,8 @@ describe('SanityChecks', () => {
|
|||||||
|
|
||||||
function generateTestOptions(): ISanityCheckOptions[] {
|
function generateTestOptions(): ISanityCheckOptions[] {
|
||||||
const defaultOptions: ISanityCheckOptions = {
|
const defaultOptions: ISanityCheckOptions = {
|
||||||
validateMetadata: true,
|
validateEnvironmentVariables: true,
|
||||||
validateEnvironment: true,
|
validateWindowVariables: true,
|
||||||
};
|
};
|
||||||
return generateBooleanPermutations(defaultOptions);
|
return generateBooleanPermutations(defaultOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { describe } from 'vitest';
|
|
||||||
import { EnvironmentValidator } from '@/infrastructure/RuntimeSanity/Validators/EnvironmentValidator';
|
|
||||||
import { FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
|
|
||||||
import { EnvironmentStub } from '@tests/unit/shared/Stubs/EnvironmentStub';
|
|
||||||
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
|
|
||||||
import { runFactoryValidatorTests } from './FactoryValidatorConcreteTestRunner';
|
|
||||||
|
|
||||||
describe('EnvironmentValidator', () => {
|
|
||||||
runFactoryValidatorTests({
|
|
||||||
createValidator: (factory?: FactoryFunction<IEnvironment>) => new EnvironmentValidator(factory),
|
|
||||||
enablingOptionProperty: 'validateEnvironment',
|
|
||||||
factoryFunctionStub: () => new EnvironmentStub(),
|
|
||||||
expectedValidatorName: 'environment',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe } from 'vitest';
|
||||||
|
import { EnvironmentVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator';
|
||||||
|
import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner';
|
||||||
|
|
||||||
|
describe('EnvironmentVariablesValidator', () => {
|
||||||
|
itNoErrorsOnCurrentEnvironment(() => new EnvironmentVariablesValidator());
|
||||||
|
});
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { describe } from 'vitest';
|
|
||||||
import { MetadataValidator } from '@/infrastructure/RuntimeSanity/Validators/MetadataValidator';
|
|
||||||
import { FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
|
|
||||||
import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub';
|
|
||||||
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
|
|
||||||
import { runFactoryValidatorTests } from './FactoryValidatorConcreteTestRunner';
|
|
||||||
|
|
||||||
describe('MetadataValidator', () => {
|
|
||||||
runFactoryValidatorTests({
|
|
||||||
createValidator: (factory?: FactoryFunction<IAppMetadata>) => new MetadataValidator(factory),
|
|
||||||
enablingOptionProperty: 'validateMetadata',
|
|
||||||
factoryFunctionStub: () => new AppMetadataStub(),
|
|
||||||
expectedValidatorName: 'metadata',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe } from 'vitest';
|
||||||
|
import { WindowVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator';
|
||||||
|
import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner';
|
||||||
|
|
||||||
|
describe('WindowVariablesValidator', () => {
|
||||||
|
itNoErrorsOnCurrentEnvironment(() => new WindowVariablesValidator());
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user