From ad0576a752f8fd6ea2f917a59173fe61f9951246 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Tue, 29 Aug 2023 16:30:00 +0200 Subject: [PATCH] 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. --- .../checks.desktop-runtime-errors.yaml | 5 +- docs/development.md | 1 + package-lock.json | 238 +++++++++++++++++- package.json | 2 + .../.eslintrc.cjs | 10 - .../check-desktop-runtime-errors/README.md | 8 +- .../app/app-logs.js | 55 ---- .../app/app-logs.ts | 82 ++++++ .../app/check-for-errors.js | 126 ---------- .../app/check-for-errors.ts | 181 +++++++++++++ .../app/error-ignore-patterns.ts | 41 +++ .../app/extractors/extraction-result.ts | 4 + .../app/extractors/{linux.js => linux.ts} | 13 +- .../app/extractors/{macos.js => macos.ts} | 41 +-- .../app/extractors/{windows.js => windows.ts} | 34 ++- .../app/{runner.js => runner.ts} | 106 +++++--- .../{screen-capture.js => screen-capture.ts} | 34 +-- ...tle-capture.js => window-title-capture.ts} | 48 ++-- .../check-desktop-runtime-errors/cli-args.js | 20 -- .../check-desktop-runtime-errors/cli-args.ts | 25 ++ .../{config.js => config.ts} | 0 scripts/check-desktop-runtime-errors/index.js | 3 - scripts/check-desktop-runtime-errors/index.ts | 3 + .../{main.js => main.ts} | 42 ++-- .../utils/{io.js => io.ts} | 22 +- .../check-desktop-runtime-errors/utils/log.js | 39 --- .../check-desktop-runtime-errors/utils/log.ts | 68 +++++ .../check-desktop-runtime-errors/utils/npm.js | 87 ------- .../check-desktop-runtime-errors/utils/npm.ts | 120 +++++++++ .../utils/platform.js | 9 - .../utils/platform.ts | 31 +++ .../utils/{run-command.js => run-command.ts} | 34 ++- .../utils/sleep.ts | 5 + .../utils/{text.js => text.ts} | 9 +- src/TypeHelpers.ts | 1 + .../Context/ApplicationContextFactory.ts | 10 +- src/application/Parser/ApplicationParser.ts | 6 +- .../Parser/ProjectInformationParser.ts | 6 +- src/infrastructure/CodeRunner.ts | 35 +-- .../Environment/IEnvironment.ts | 8 - .../Environment/WindowVariables.ts | 13 - .../EnvironmentVariablesFactory.ts | 18 ++ .../EnvironmentVariablesValidator.ts} | 20 +- .../IAppMetadata.ts | 3 - .../IEnvironmentVariables.ts | 9 + .../IEnvironmentVariablesFactory.ts | 5 + .../Vite/ViteEnvironmentKeys.ts | 7 +- .../Vite/ViteEnvironmentVariables.ts} | 10 +- .../Vite/vite-env.d.ts | 0 src/infrastructure/Log/ConsoleLogger.ts | 13 + src/infrastructure/Log/ElectronLogger.ts | 12 + src/infrastructure/Log/ILogger.ts | 3 + src/infrastructure/Log/ILoggerFactory.ts | 5 + src/infrastructure/Log/NoopLogger.ts | 5 + .../Log/WindowInjectedLogger.ts | 20 ++ .../Metadata/AppMetadataFactory.ts | 18 -- .../Metadata/IAppMetadataFactory.ts | 5 - .../BrowserOs/BrowserOsDetector.ts | 0 .../BrowserOs/DetectorBuilder.ts | 0 .../BrowserOs/IBrowserOsDetector.ts | 0 .../RuntimeEnvironment/IRuntimeEnvironment.ts | 7 + .../RuntimeEnvironment.ts} | 21 +- .../Common/ISanityCheckOptions.ts | 4 +- .../RuntimeSanity/SanityChecks.ts | 4 +- .../Validators/EnvironmentValidator.ts | 16 -- .../EnvironmentVariablesValidator.ts | 20 ++ .../Validators/MetadataValidator.ts | 16 -- .../Validators/WindowVariablesValidator.ts | 15 ++ .../SystemOperations/ISystemOperations.ts | 0 .../SystemOperations/NodeSystemOperations.ts | 0 .../WindowInjectedSystemOperations.ts | 14 ++ .../WindowVariables/WindowVariables.ts | 11 + .../WindowVariablesValidator.ts | 19 +- .../WindowVariables/window-variables.d.ts | 6 + .../bootstrapping/ApplicationBootstrapper.ts | 4 + .../bootstrapping/ClientLoggerFactory.ts | 25 ++ .../bootstrapping/DependencyProvider.ts | 6 +- .../Modules/AppInitializationLogger.ts | 14 ++ .../Modules/RuntimeSanityValidator.ts | 15 ++ src/presentation/components/App.vue | 5 - .../Code/CodeButtons/TheCodeButtons.vue | 15 +- .../components/Shared/Hooks/UseEnvironment.ts | 8 - .../Shared/Hooks/UseRuntimeEnvironment.ts | 8 + .../components/TheFooter/DownloadUrlList.vue | 4 +- .../TheFooter/DownloadUrlListItem.vue | 4 +- .../components/TheFooter/PrivacyPolicy.vue | 4 +- .../components/TheFooter/TheFooter.vue | 4 +- .../electron/main/Update/ManualUpdater.ts | 3 +- src/presentation/electron/main/index.ts | 7 +- .../preload/WindowVariablesProvider.ts | 9 +- src/presentation/electron/preload/index.ts | 4 +- src/presentation/injectionSymbols.ts | 4 +- .../EnvironmentVariablesFactory.spec.ts | 15 ++ .../Vite/ViteEnvironmentVariables.spec.ts} | 13 +- .../RuntimeSanity/SanityChecks.spec.ts | 4 +- .../Validators/EnvironmentValidator.spec.ts | 15 -- .../EnvironmentVariablesValidator.spec.ts | 7 + .../Validators/MetadataValidator.spec.ts | 15 -- .../Validators/ValidatorTestRunner.ts | 0 .../WindowVariablesValidator.spec.ts | 7 + .../preload/WindowVariablesProvider.spec.ts | 35 +++ .../Context/ApplicationContextFactory.spec.ts | 8 +- .../Parser/ApplicationParser.spec.ts | 10 +- tests/unit/infrastructure/CodeRunner.spec.ts | 24 +- .../WindowVariablesValidator.spec.ts | 160 ------------ .../EnvironmentVariablesFactory.ts | 53 ++++ .../EnvironmentVariablesValidator.spec.ts | 88 +++++++ .../Vite}/ViteEnvironmentKeys.spec.ts | 2 +- .../Vite/ViteEnvironmentVariables.spec.ts} | 24 +- .../infrastructure/Log/ConsoleLogger.spec.ts | 46 ++++ .../infrastructure/Log/ElectronLogger.spec.ts | 45 ++++ .../infrastructure/Log/LoggerTestRunner.ts | 21 ++ .../infrastructure/Log/NoopLogger.spec.ts | 20 ++ .../Log/WindowInjectedLogger.spec.ts | 50 ++++ .../Metadata/AppMetadataFactory.spec.ts | 53 ---- .../Metadata/MetadataValidator.spec.ts | 74 ------ .../BrowserOs/BrowserOsDetector.spec.ts | 2 +- .../BrowserOs/BrowserOsTestCases.ts | 0 .../RuntimeEnvironment.spec.ts} | 92 ++----- .../WindowVariablesValidator.spec.ts | 226 +++++++++++++++++ .../Validators/EnvironmentValidator.spec.ts | 7 - .../EnvironmentVariablesValidator.spec.ts | 17 ++ .../FactoryValidatorConcreteTestRunner.ts | 0 .../Validators/MetadataValidator.spec.ts | 7 - .../WindowVariablesValidator.spec.ts | 17 ++ .../WindowInjectedSystemOperations.spec.ts | 46 ++++ .../bootstrapping/ClientLoggerFactory.spec.ts | 80 ++++++ .../Modules/AppInitializationLogger.spec.ts | 18 ++ .../Modules/RuntimeSanityValidator.spec.ts | 43 ++++ ....spec.ts => UseRuntimeEnvironment.spec.ts} | 12 +- .../preload/WindowVariablesProvider.spec.ts | 26 +- tests/unit/shared/Stubs/AppMetadataStub.ts | 2 +- .../shared/Stubs/BrowserOsDetectorStub.ts | 2 +- tests/unit/shared/Stubs/CommandOpsStub.ts | 2 +- tests/unit/shared/Stubs/EnvironmentStub.ts | 22 -- .../shared/Stubs/EnvironmentVariablesStub.ts | 11 + tests/unit/shared/Stubs/FileSystemOpsStub.ts | 2 +- tests/unit/shared/Stubs/LocationOpsStub.ts | 2 +- tests/unit/shared/Stubs/LoggerStub.ts | 12 + .../shared/Stubs/OperatingSystemOpsStub.ts | 2 +- .../Stubs/ProjectInformationParserStub.ts | 2 +- .../shared/Stubs/RuntimeEnvironmentStub.ts | 25 ++ .../shared/Stubs/SanityCheckOptionsStub.ts | 10 +- .../unit/shared/Stubs/SystemOperationsStub.ts | 2 +- .../unit/shared/Stubs/WindowVariablesStub.ts | 36 +++ vite-config-helper.ts | 16 +- 146 files changed, 2418 insertions(+), 1186 deletions(-) delete mode 100644 scripts/check-desktop-runtime-errors/.eslintrc.cjs delete mode 100644 scripts/check-desktop-runtime-errors/app/app-logs.js create mode 100644 scripts/check-desktop-runtime-errors/app/app-logs.ts delete mode 100644 scripts/check-desktop-runtime-errors/app/check-for-errors.js create mode 100644 scripts/check-desktop-runtime-errors/app/check-for-errors.ts create mode 100644 scripts/check-desktop-runtime-errors/app/error-ignore-patterns.ts create mode 100644 scripts/check-desktop-runtime-errors/app/extractors/extraction-result.ts rename scripts/check-desktop-runtime-errors/app/extractors/{linux.js => linux.ts} (61%) rename scripts/check-desktop-runtime-errors/app/extractors/{macos.js => macos.ts} (64%) rename scripts/check-desktop-runtime-errors/app/extractors/{windows.js => windows.ts} (51%) rename scripts/check-desktop-runtime-errors/app/{runner.js => runner.ts} (56%) rename scripts/check-desktop-runtime-errors/app/system-capture/{screen-capture.js => screen-capture.ts} (53%) rename scripts/check-desktop-runtime-errors/app/system-capture/{window-title-capture.js => window-title-capture.ts} (61%) delete mode 100644 scripts/check-desktop-runtime-errors/cli-args.js create mode 100644 scripts/check-desktop-runtime-errors/cli-args.ts rename scripts/check-desktop-runtime-errors/{config.js => config.ts} (100%) delete mode 100644 scripts/check-desktop-runtime-errors/index.js create mode 100644 scripts/check-desktop-runtime-errors/index.ts rename scripts/check-desktop-runtime-errors/{main.js => main.ts} (50%) rename scripts/check-desktop-runtime-errors/utils/{io.js => io.ts} (67%) delete mode 100644 scripts/check-desktop-runtime-errors/utils/log.js create mode 100644 scripts/check-desktop-runtime-errors/utils/log.ts delete mode 100644 scripts/check-desktop-runtime-errors/utils/npm.js create mode 100644 scripts/check-desktop-runtime-errors/utils/npm.ts delete mode 100644 scripts/check-desktop-runtime-errors/utils/platform.js create mode 100644 scripts/check-desktop-runtime-errors/utils/platform.ts rename scripts/check-desktop-runtime-errors/utils/{run-command.js => run-command.ts} (51%) create mode 100644 scripts/check-desktop-runtime-errors/utils/sleep.ts rename scripts/check-desktop-runtime-errors/utils/{text.js => text.ts} (66%) delete mode 100644 src/infrastructure/Environment/IEnvironment.ts delete mode 100644 src/infrastructure/Environment/WindowVariables.ts create mode 100644 src/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.ts rename src/infrastructure/{Metadata/MetadataValidator.ts => EnvironmentVariables/EnvironmentVariablesValidator.ts} (66%) rename src/infrastructure/{Metadata => EnvironmentVariables}/IAppMetadata.ts (64%) create mode 100644 src/infrastructure/EnvironmentVariables/IEnvironmentVariables.ts create mode 100644 src/infrastructure/EnvironmentVariables/IEnvironmentVariablesFactory.ts rename src/infrastructure/{Metadata => EnvironmentVariables}/Vite/ViteEnvironmentKeys.ts (71%) rename src/infrastructure/{Metadata/Vite/ViteAppMetadata.ts => EnvironmentVariables/Vite/ViteEnvironmentVariables.ts} (68%) rename src/infrastructure/{Metadata => EnvironmentVariables}/Vite/vite-env.d.ts (100%) create mode 100644 src/infrastructure/Log/ConsoleLogger.ts create mode 100644 src/infrastructure/Log/ElectronLogger.ts create mode 100644 src/infrastructure/Log/ILogger.ts create mode 100644 src/infrastructure/Log/ILoggerFactory.ts create mode 100644 src/infrastructure/Log/NoopLogger.ts create mode 100644 src/infrastructure/Log/WindowInjectedLogger.ts delete mode 100644 src/infrastructure/Metadata/AppMetadataFactory.ts delete mode 100644 src/infrastructure/Metadata/IAppMetadataFactory.ts rename src/infrastructure/{Environment => RuntimeEnvironment}/BrowserOs/BrowserOsDetector.ts (100%) rename src/infrastructure/{Environment => RuntimeEnvironment}/BrowserOs/DetectorBuilder.ts (100%) rename src/infrastructure/{Environment => RuntimeEnvironment}/BrowserOs/IBrowserOsDetector.ts (100%) create mode 100644 src/infrastructure/RuntimeEnvironment/IRuntimeEnvironment.ts rename src/infrastructure/{Environment/Environment.ts => RuntimeEnvironment/RuntimeEnvironment.ts} (57%) delete mode 100644 src/infrastructure/RuntimeSanity/Validators/EnvironmentValidator.ts create mode 100644 src/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.ts delete mode 100644 src/infrastructure/RuntimeSanity/Validators/MetadataValidator.ts create mode 100644 src/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.ts rename src/infrastructure/{Environment => }/SystemOperations/ISystemOperations.ts (100%) rename src/infrastructure/{Environment => }/SystemOperations/NodeSystemOperations.ts (100%) create mode 100644 src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts create mode 100644 src/infrastructure/WindowVariables/WindowVariables.ts rename src/infrastructure/{Environment => WindowVariables}/WindowVariablesValidator.ts (83%) create mode 100644 src/infrastructure/WindowVariables/window-variables.d.ts create mode 100644 src/presentation/bootstrapping/ClientLoggerFactory.ts create mode 100644 src/presentation/bootstrapping/Modules/AppInitializationLogger.ts create mode 100644 src/presentation/bootstrapping/Modules/RuntimeSanityValidator.ts delete mode 100644 src/presentation/components/Shared/Hooks/UseEnvironment.ts create mode 100644 src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts create mode 100644 tests/integration/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.spec.ts rename tests/integration/infrastructure/{Metadata/Vite/ViteAppMetadata.spec.ts => EnvironmentVariables/Vite/ViteEnvironmentVariables.spec.ts} (69%) delete mode 100644 tests/integration/infrastructure/RuntimeSanity/Validators/EnvironmentValidator.spec.ts create mode 100644 tests/integration/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.spec.ts delete mode 100644 tests/integration/infrastructure/RuntimeSanity/Validators/MetadataValidator.spec.ts rename tests/{unit => integration}/infrastructure/RuntimeSanity/Validators/ValidatorTestRunner.ts (100%) create mode 100644 tests/integration/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.spec.ts create mode 100644 tests/integration/presentation/electron/preload/WindowVariablesProvider.spec.ts delete mode 100644 tests/unit/infrastructure/Environment/WindowVariablesValidator.spec.ts create mode 100644 tests/unit/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.ts create mode 100644 tests/unit/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.spec.ts rename tests/unit/infrastructure/{Metadata => EnvironmentVariables/Vite}/ViteEnvironmentKeys.spec.ts (80%) rename tests/unit/infrastructure/{Metadata/ViteAppMetadata.spec.ts => EnvironmentVariables/Vite/ViteEnvironmentVariables.spec.ts} (68%) create mode 100644 tests/unit/infrastructure/Log/ConsoleLogger.spec.ts create mode 100644 tests/unit/infrastructure/Log/ElectronLogger.spec.ts create mode 100644 tests/unit/infrastructure/Log/LoggerTestRunner.ts create mode 100644 tests/unit/infrastructure/Log/NoopLogger.spec.ts create mode 100644 tests/unit/infrastructure/Log/WindowInjectedLogger.spec.ts delete mode 100644 tests/unit/infrastructure/Metadata/AppMetadataFactory.spec.ts delete mode 100644 tests/unit/infrastructure/Metadata/MetadataValidator.spec.ts rename tests/unit/infrastructure/{Environment => RuntimeEnvironment}/BrowserOs/BrowserOsDetector.spec.ts (92%) rename tests/unit/infrastructure/{Environment => RuntimeEnvironment}/BrowserOs/BrowserOsTestCases.ts (100%) rename tests/unit/infrastructure/{Environment/Environment.spec.ts => RuntimeEnvironment/RuntimeEnvironment.spec.ts} (63%) create mode 100644 tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts delete mode 100644 tests/unit/infrastructure/RuntimeSanity/Validators/EnvironmentValidator.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.spec.ts rename tests/{integration => unit}/infrastructure/RuntimeSanity/Validators/FactoryValidatorConcreteTestRunner.ts (100%) delete mode 100644 tests/unit/infrastructure/RuntimeSanity/Validators/MetadataValidator.spec.ts create mode 100644 tests/unit/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.spec.ts create mode 100644 tests/unit/infrastructure/SystemOperations/WindowInjectedSystemOperations.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/ClientLoggerFactory.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/Modules/AppInitializationLogger.spec.ts create mode 100644 tests/unit/presentation/bootstrapping/Modules/RuntimeSanityValidator.spec.ts rename tests/unit/presentation/components/Shared/Hooks/{UseEnvironment.spec.ts => UseRuntimeEnvironment.spec.ts} (57%) delete mode 100644 tests/unit/shared/Stubs/EnvironmentStub.ts create mode 100644 tests/unit/shared/Stubs/EnvironmentVariablesStub.ts create mode 100644 tests/unit/shared/Stubs/LoggerStub.ts create mode 100644 tests/unit/shared/Stubs/RuntimeEnvironmentStub.ts create mode 100644 tests/unit/shared/Stubs/WindowVariablesStub.ts diff --git a/.github/workflows/checks.desktop-runtime-errors.yaml b/.github/workflows/checks.desktop-runtime-errors.yaml index f5917f43..d2f9d430 100644 --- a/.github/workflows/checks.desktop-runtime-errors.yaml +++ b/.github/workflows/checks.desktop-runtime-errors.yaml @@ -19,6 +19,9 @@ jobs: - name: Setup node uses: ./.github/actions/setup-node + - + name: Install dependencies + run: npm ci - name: Configure Ubuntu if: matrix.os == 'ubuntu' @@ -57,7 +60,7 @@ jobs: - name: Test shell: bash - run: node ./scripts/check-desktop-runtime-errors --screenshot + run: npm run check:desktop -- --screenshot - name: Upload screenshot if: always() # Run even if previous step fails diff --git a/docs/development.md b/docs/development.md index 6dde6df0..beae74e2 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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: - `npm run test:cy:open`: Run tests interactively using the development server with hot-reloading. - `npm run test:cy:run`: Run tests on the production build in a headless mode. +- Run 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). diff --git a/package-lock.json b/package-lock.json index e2d7a2c5..49c5f3dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "privacy.sexy", - "version": "0.12.1", + "version": "0.12.2", "hasInstallScript": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", @@ -63,6 +63,7 @@ "start-server-and-test": "^2.0.0", "svgexport": "^0.4.2", "terser": "^5.19.2", + "ts-node": "^10.9.1", "tslib": "~2.4.0", "typescript": "~4.6.2", "vite": "^4.4.9", @@ -1789,6 +1790,28 @@ "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": { "version": "2.88.12", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", @@ -3611,6 +3634,30 @@ "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": { "version": "0.0.48", "resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz", @@ -6070,6 +6117,12 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -11185,6 +11238,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": { "version": "0.1.3", "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" } }, + "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": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -20365,6 +20482,12 @@ "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": { "version": "3.0.4", "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" } }, + "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": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -22509,6 +22641,27 @@ "dev": 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": { "version": "2.88.12", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", @@ -23786,6 +23939,30 @@ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "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": { "version": "0.0.48", "resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.48.tgz", @@ -25660,6 +25837,12 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -29569,6 +29752,12 @@ "@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": { "version": "0.1.3", "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" } }, + "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": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -36115,6 +36339,12 @@ "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": { "version": "3.0.4", "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" } }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index cbbbaaf6..bf334999 100644 --- a/package.json +++ b/package.json @@ -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\"", "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", + "check:desktop": "ts-node --experimentalSpecifierResolution node --esm scripts/check-desktop-runtime-errors/index.ts", "electron:dev": "electron-vite dev", "electron:preview": "electron-vite preview", "electron:prebuild": "electron-vite build", @@ -85,6 +86,7 @@ "start-server-and-test": "^2.0.0", "svgexport": "^0.4.2", "terser": "^5.19.2", + "ts-node": "^10.9.1", "tslib": "~2.4.0", "typescript": "~4.6.2", "vite": "^4.4.9", diff --git a/scripts/check-desktop-runtime-errors/.eslintrc.cjs b/scripts/check-desktop-runtime-errors/.eslintrc.cjs deleted file mode 100644 index 3c2e3ed7..00000000 --- a/scripts/check-desktop-runtime-errors/.eslintrc.cjs +++ /dev/null @@ -1,10 +0,0 @@ -require('@rushstack/eslint-patch/modern-module-resolution.js'); - -module.exports = { - env: { - node: true, - }, - rules: { - 'import/extensions': ['error', 'always'], - }, -}; diff --git a/scripts/check-desktop-runtime-errors/README.md b/scripts/check-desktop-runtime-errors/README.md index f167b448..9fc9837d 100644 --- a/scripts/check-desktop-runtime-errors/README.md +++ b/scripts/check-desktop-runtime-errors/README.md @@ -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. -## Usage - -```sh -node ./scripts/check-desktop-runtime-errors -``` - ## Options - `--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 -Configurations are defined in [`config.js`](./config.js). +Configurations are defined in [`config.ts`](./config.ts). diff --git a/scripts/check-desktop-runtime-errors/app/app-logs.js b/scripts/check-desktop-runtime-errors/app/app-logs.js deleted file mode 100644 index c620a4fa..00000000 --- a/scripts/check-desktop-runtime-errors/app/app-logs.js +++ /dev/null @@ -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; -} diff --git a/scripts/check-desktop-runtime-errors/app/app-logs.ts b/scripts/check-desktop-runtime-errors/app/app-logs.ts new file mode 100644 index 00000000..1766580e --- /dev/null +++ b/scripts/check-desktop-runtime-errors/app/app-logs.ts @@ -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 { + 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 { + 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 { + 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 { + const content = await readFile(logFilePath, 'utf-8'); + return content?.trim().length > 0 ? content : undefined; +} diff --git a/scripts/check-desktop-runtime-errors/app/check-for-errors.js b/scripts/check-desktop-runtime-errors/app/check-for-errors.js deleted file mode 100644 index 7db6f66c..00000000 --- a/scripts/check-desktop-runtime-errors/app/check-for-errors.js +++ /dev/null @@ -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); -} diff --git a/scripts/check-desktop-runtime-errors/app/check-for-errors.ts b/scripts/check-desktop-runtime-errors/app/check-for-errors.ts new file mode 100644 index 00000000..25a39a88 --- /dev/null +++ b/scripts/check-desktop-runtime-errors/app/check-for-errors.ts @@ -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 { + 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); +} diff --git a/scripts/check-desktop-runtime-errors/app/error-ignore-patterns.ts b/scripts/check-desktop-runtime-errors/app/error-ignore-patterns.ts new file mode 100644 index 00000000..c90a0ad3 --- /dev/null +++ b/scripts/check-desktop-runtime-errors/app/error-ignore-patterns.ts @@ -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\./, +]; diff --git a/scripts/check-desktop-runtime-errors/app/extractors/extraction-result.ts b/scripts/check-desktop-runtime-errors/app/extractors/extraction-result.ts new file mode 100644 index 00000000..531b0db1 --- /dev/null +++ b/scripts/check-desktop-runtime-errors/app/extractors/extraction-result.ts @@ -0,0 +1,4 @@ +export interface ExtractionResult { + readonly appExecutablePath: string; + readonly cleanup?: () => Promise; +} diff --git a/scripts/check-desktop-runtime-errors/app/extractors/linux.js b/scripts/check-desktop-runtime-errors/app/extractors/linux.ts similarity index 61% rename from scripts/check-desktop-runtime-errors/app/extractors/linux.js rename to scripts/check-desktop-runtime-errors/app/extractors/linux.ts index 0d4f8e32..dd0b60e9 100644 --- a/scripts/check-desktop-runtime-errors/app/extractors/linux.js +++ b/scripts/check-desktop-runtime-errors/app/extractors/linux.ts @@ -1,9 +1,12 @@ import { access, chmod } from 'fs/promises'; import { constants } from 'fs'; -import { findSingleFileByExtension } from '../../utils/io.js'; -import { log } from '../../utils/log.js'; +import { findSingleFileByExtension } from '../../utils/io'; +import { log } from '../../utils/log'; +import { ExtractionResult } from './extraction-result'; -export async function prepareLinuxApp(desktopDistPath) { +export async function prepareLinuxApp( + desktopDistPath: string, +): Promise { const { absolutePath: appFile } = await findSingleFileByExtension( 'AppImage', desktopDistPath, @@ -14,7 +17,7 @@ export async function prepareLinuxApp(desktopDistPath) { }; } -async function makeExecutable(appFile) { +async function makeExecutable(appFile: string): Promise { if (!appFile) { throw new Error('missing file'); } if (await isExecutable(appFile)) { log('AppImage is already executable.'); @@ -24,7 +27,7 @@ async function makeExecutable(appFile) { await chmod(appFile, 0o755); } -async function isExecutable(file) { +async function isExecutable(file: string): Promise { try { await access(file, constants.X_OK); return true; diff --git a/scripts/check-desktop-runtime-errors/app/extractors/macos.js b/scripts/check-desktop-runtime-errors/app/extractors/macos.ts similarity index 64% rename from scripts/check-desktop-runtime-errors/app/extractors/macos.js rename to scripts/check-desktop-runtime-errors/app/extractors/macos.ts index 4f0730fb..a1f538bf 100644 --- a/scripts/check-desktop-runtime-errors/app/extractors/macos.js +++ b/scripts/check-desktop-runtime-errors/app/extractors/macos.ts @@ -1,8 +1,12 @@ -import { runCommand } from '../../utils/run-command.js'; -import { findSingleFileByExtension, exists } from '../../utils/io.js'; -import { log, die, LOG_LEVELS } from '../../utils/log.js'; +import { runCommand } from '../../utils/run-command'; +import { findSingleFileByExtension, exists } from '../../utils/io'; +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 { const { absolutePath: dmgPath } = await findSingleFileByExtension('dmg', desktopDistPath); const { mountPath } = await mountDmg(dmgPath); const appPath = await findMacAppExecutablePath(mountPath); @@ -15,8 +19,12 @@ export async function prepareMacOsApp(desktopDistPath) { }; } -async function mountDmg(dmgFile) { - const { stdout: hdiutilOutput, error } = await runCommand(`hdiutil attach '${dmgFile}'`); +async function mountDmg( + dmgFile: string, +) { + const { stdout: hdiutilOutput, error } = await runCommand( + `hdiutil attach '${dmgFile}'`, + ); if (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 { const { stdout: findOutput, error } = await runCommand( `find '${mountPath}' -maxdepth 1 -type d -name "*.app"`, ); if (error) { - die(`Failed to find executable path at mount path ${mountPath}\n${error}`); + return die(`Failed to find executable path at mount path ${mountPath}\n${error}`); } const appFolder = findOutput.trim(); const appName = appFolder.split('/').pop().replace('.app', ''); @@ -40,16 +50,19 @@ async function findMacAppExecutablePath(mountPath) { if (await exists(appPath)) { log(`Application is located at ${appPath}`); } else { - die(`Application does not exist at ${appPath}`); + return die(`Application does not exist at ${appPath}`); } return appPath; } -async function detachMount(mountPath, retries = 5) { +async function detachMount( + mountPath: string, + retries = 5, +) { const { error } = await runCommand(`hdiutil detach '${mountPath}'`); if (error) { if (retries <= 0) { - log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LOG_LEVELS.WARN); + log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LogLevel.Warn); return; } await sleep(500); @@ -58,9 +71,3 @@ async function detachMount(mountPath, retries = 5) { } log(`Successfully detached from ${mountPath}`); } - -function sleep(milliseconds) { - return new Promise((resolve) => { - setTimeout(resolve, milliseconds); - }); -} diff --git a/scripts/check-desktop-runtime-errors/app/extractors/windows.js b/scripts/check-desktop-runtime-errors/app/extractors/windows.ts similarity index 51% rename from scripts/check-desktop-runtime-errors/app/extractors/windows.js rename to scripts/check-desktop-runtime-errors/app/extractors/windows.ts index a03cb947..7da059b5 100644 --- a/scripts/check-desktop-runtime-errors/app/extractors/windows.js +++ b/scripts/check-desktop-runtime-errors/app/extractors/windows.ts @@ -1,38 +1,46 @@ -import { mkdtemp, rmdir } from 'fs/promises'; +import { mkdtemp, rm } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; -import { findSingleFileByExtension, exists } from '../../utils/io.js'; -import { log, die } from '../../utils/log.js'; -import { runCommand } from '../../utils/run-command.js'; +import { findSingleFileByExtension, exists } from '../../utils/io'; +import { log, die, LogLevel } from '../../utils/log'; +import { runCommand } from '../../utils/run-command'; +import { ExtractionResult } from './extraction-result'; -export async function prepareWindowsApp(desktopDistPath) { +export async function prepareWindowsApp( + desktopDistPath: string, +): Promise { const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-')); if (await exists(workdir)) { log(`Temporary directory ${workdir} already exists, cleaning up...`); - await rmdir(workdir, { recursive: true }); + await rm(workdir, { recursive: true }); } - const { appExecutablePath } = await installNsis(workdir, desktopDistPath); + const appExecutablePath = await installNsis(workdir, desktopDistPath); return { appExecutablePath, cleanup: async () => { 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 { const { absolutePath: installerPath } = await findSingleFileByExtension('exe', desktopDistPath); log(`Silently installing contents of ${installerPath} to ${installationPath}...`); const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`); if (error) { - die(`Failed to install.\n${error}`); + return die(`Failed to install.\n${error}`); } const { absolutePath: appExecutablePath } = await findSingleFileByExtension('exe', installationPath); - return { - appExecutablePath, - }; + return appExecutablePath; } diff --git a/scripts/check-desktop-runtime-errors/app/runner.js b/scripts/check-desktop-runtime-errors/app/runner.ts similarity index 56% rename from scripts/check-desktop-runtime-errors/app/runner.js rename to scripts/check-desktop-runtime-errors/app/runner.ts index 4b76382a..a4e12971 100644 --- a/scripts/check-desktop-runtime-errors/app/runner.js +++ b/scripts/check-desktop-runtime-errors/app/runner.ts @@ -1,25 +1,26 @@ import { spawn } from 'child_process'; -import { log, LOG_LEVELS, die } from '../utils/log.js'; -import { captureScreen } from './system-capture/screen-capture.js'; -import { captureWindowTitles } from './system-capture/window-title-capture.js'; +import { log, LogLevel, die } from '../utils/log'; +import { captureScreen } from './system-capture/screen-capture'; +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 WINDOW_TITLE_CAPTURE_INTERVAL_IN_MS = 100; export function runApplication( - appFile, - executionDurationInSeconds, - enableScreenshot, - screenshotPath, -) { + appFile: string, + executionDurationInSeconds: number, + enableScreenshot: boolean, + screenshotPath: string, +): Promise { if (!appFile) { throw new Error('Missing app file'); } logDetails(appFile, executionDurationInSeconds); - const processDetails = { + const processDetails: ApplicationProcessDetails = { stderrData: '', stdoutData: '', explicitlyKilled: false, @@ -35,7 +36,7 @@ export function runApplication( return new Promise((resolve) => { processDetails.resolve = resolve; - handleTitleCapture(process.pid, processDetails); + beginCapturingTitles(process.pid, processDetails); handleProcessEvents( processDetails, 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; + isCrashed: boolean; + isDone: boolean; + process: ChildProcess; + resolve: (value: ApplicationExecutionResult) => void; +} + +function logDetails( + appFile: string, + executionDurationInSeconds: number, +): void { log( [ '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 titles = await captureWindowTitles(processId); (titles || []).forEach((title) => { - if (!title || !title.length) { + if (!title?.length) { return; } if (!processDetails.windowTitles.includes(title)) { @@ -78,11 +103,11 @@ function handleTitleCapture(processId, processDetails) { } function handleProcessEvents( - processDetails, - enableScreenshot, - screenshotPath, - executionDurationInSeconds, -) { + processDetails: ApplicationProcessDetails, + enableScreenshot: boolean, + screenshotPath: string, + executionDurationInSeconds: number, +): void { const { process } = processDetails; process.stderr.on('data', (data) => { processDetails.stderrData += data.toString(); @@ -92,7 +117,7 @@ function handleProcessEvents( }); 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) => { @@ -100,11 +125,16 @@ function handleProcessEvents( }); setTimeout(async () => { - await onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath); + await onExecutionLimitReached(processDetails, enableScreenshot, screenshotPath); }, executionDurationInSeconds * 1000); } -async function onProcessExit(code, processDetails, enableScreenshot, screenshotPath) { +async function onProcessExit( + code: number, + processDetails: ApplicationProcessDetails, + enableScreenshot: boolean, + screenshotPath: string, +): Promise { log(`Application exited ${code === null || Number.isNaN(code) ? '.' : `with code ${code}`}`); if (processDetails.explicitlyKilled) return; @@ -118,17 +148,21 @@ async function onProcessExit(code, processDetails, enableScreenshot, screenshotP finishProcess(processDetails); } -async function onExecutionLimitReached(process, processDetails, enableScreenshot, screenshotPath) { +async function onExecutionLimitReached( + processDetails: ApplicationProcessDetails, + enableScreenshot: boolean, + screenshotPath: string, +): Promise { if (enableScreenshot) { await captureScreen(screenshotPath); } processDetails.explicitlyKilled = true; - await terminateGracefully(process); + await terminateGracefully(processDetails.process); finishProcess(processDetails); } -function finishProcess(processDetails) { +function finishProcess(processDetails: ApplicationProcessDetails): void { processDetails.isDone = true; processDetails.resolve({ stderr: processDetails.stderrData, @@ -138,7 +172,9 @@ function finishProcess(processDetails) { }); } -async function terminateGracefully(process) { +async function terminateGracefully( + process: ChildProcess, +): Promise { let elapsedSeconds = 0; log('Attempting to terminate the process gracefully...'); process.kill('SIGTERM'); @@ -147,18 +183,18 @@ async function terminateGracefully(process) { const checkInterval = setInterval(() => { elapsedSeconds += TERMINATION_CHECK_INTERVAL_IN_MS / 1000; - if (!process.killed) { - if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) { - process.kill('SIGKILL'); - log('Process did not terminate gracefully within the grace period. Forcing termination.', LOG_LEVELS.WARN); - clearInterval(checkInterval); - resolve(); - } - } else { - log('Process terminated gracefully.'); + if (elapsedSeconds >= TERMINATION_GRACE_PERIOD_IN_SECONDS) { + process.kill('SIGKILL'); + log('Process did not terminate gracefully within the grace period. Forcing termination.', LogLevel.Warn); clearInterval(checkInterval); resolve(); } }, TERMINATION_CHECK_INTERVAL_IN_MS); + + process.on('exit', () => { + log('Process terminated gracefully.'); + clearInterval(checkInterval); + resolve(); + }); }); } diff --git a/scripts/check-desktop-runtime-errors/app/system-capture/screen-capture.js b/scripts/check-desktop-runtime-errors/app/system-capture/screen-capture.ts similarity index 53% rename from scripts/check-desktop-runtime-errors/app/system-capture/screen-capture.js rename to scripts/check-desktop-runtime-errors/app/system-capture/screen-capture.ts index fcae87ef..f09b5334 100644 --- a/scripts/check-desktop-runtime-errors/app/system-capture/screen-capture.js +++ b/scripts/check-desktop-runtime-errors/app/system-capture/screen-capture.ts @@ -1,29 +1,33 @@ import { unlink } from 'fs/promises'; -import { runCommand } from '../../utils/run-command.js'; -import { log, LOG_LEVELS } from '../../utils/log.js'; -import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from '../../utils/platform.js'; -import { exists } from '../../utils/io.js'; +import { runCommand } from '../../utils/run-command'; +import { log, LogLevel } from '../../utils/log'; +import { CURRENT_PLATFORM, SupportedPlatform } from '../../utils/platform'; +import { exists } from '../../utils/io'; -export async function captureScreen(imagePath) { +export async function captureScreen( + imagePath: string, +): Promise { if (!imagePath) { throw new Error('Path for screenshot not provided'); } if (await exists(imagePath)) { - log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LOG_LEVELS.WARN); + log(`Screenshot file already exists at ${imagePath}. It will be overwritten.`, LogLevel.Warn); unlink(imagePath); } - const platformCommands = { - [SUPPORTED_PLATFORMS.MAC]: `screencapture -x ${imagePath}`, - [SUPPORTED_PLATFORMS.LINUX]: `import -window root ${imagePath}`, - [SUPPORTED_PLATFORMS.WINDOWS]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`, + const platformCommands: { + readonly [K in SupportedPlatform]: string; + } = { + [SupportedPlatform.macOS]: `screencapture -x ${imagePath}`, + [SupportedPlatform.Linux]: `import -window root ${imagePath}`, + [SupportedPlatform.Windows]: `powershell -NoProfile -EncodedCommand ${encodeForPowershell(getScreenshotPowershellScript(imagePath))}`, }; const commandForPlatform = platformCommands[CURRENT_PLATFORM]; if (!commandForPlatform) { - log(`Screenshot capture not supported on: ${CURRENT_PLATFORM}`, LOG_LEVELS.WARN); + log(`Screenshot capture not supported on: ${SupportedPlatform[CURRENT_PLATFORM]}`, LogLevel.Warn); return; } @@ -31,13 +35,13 @@ export async function captureScreen(imagePath) { const { error } = await runCommand(commandForPlatform); if (error) { - log(`Failed to capture screenshot.\n${error}`, LOG_LEVELS.WARN); + log(`Failed to capture screenshot.\n${error}`, LogLevel.Warn); return; } log(`Captured screenshot to ${imagePath}.`); } -function getScreenshotPowershellScript(imagePath) { +function getScreenshotPowershellScript(imagePath: string): string { return ` $ProgressPreference = 'SilentlyContinue' # Do not pollute stderr Add-Type -AssemblyName System.Windows.Forms @@ -53,7 +57,7 @@ function getScreenshotPowershellScript(imagePath) { `; } -function encodeForPowershell(script) { - const buffer = Buffer.from(script, 'utf-16le'); +function encodeForPowershell(script: string): string { + const buffer = Buffer.from(script, 'utf16le'); return buffer.toString('base64'); } diff --git a/scripts/check-desktop-runtime-errors/app/system-capture/window-title-capture.js b/scripts/check-desktop-runtime-errors/app/system-capture/window-title-capture.ts similarity index 61% rename from scripts/check-desktop-runtime-errors/app/system-capture/window-title-capture.js rename to scripts/check-desktop-runtime-errors/app/system-capture/window-title-capture.ts index 305219c6..8a81f679 100644 --- a/scripts/check-desktop-runtime-errors/app/system-capture/window-title-capture.js +++ b/scripts/check-desktop-runtime-errors/app/system-capture/window-title-capture.ts @@ -1,37 +1,40 @@ -import { runCommand } from '../../utils/run-command.js'; -import { log, LOG_LEVELS } from '../../utils/log.js'; -import { SUPPORTED_PLATFORMS, CURRENT_PLATFORM } from '../../utils/platform.js'; +import { runCommand } from '../../utils/run-command'; +import { log, LogLevel } from '../../utils/log'; +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.'); } const captureFunction = windowTitleCaptureFunctions[CURRENT_PLATFORM]; 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 captureFunction(processId); } -const windowTitleCaptureFunctions = { - [SUPPORTED_PLATFORMS.MAC]: captureTitlesOnMac, - [SUPPORTED_PLATFORMS.LINUX]: captureTitlesOnLinux, - [SUPPORTED_PLATFORMS.WINDOWS]: captureTitlesOnWindows, +const windowTitleCaptureFunctions: { + readonly [K in SupportedPlatform]: (processId: number) => Promise; +} = { + [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 { if (!processId) { throw new Error('Missing process ID.'); } const { stdout: tasklistOutput, error } = await runCommand( `tasklist /FI "PID eq ${processId}" /fo list /v`, ); if (error) { - log(`Failed to retrieve window title.\n${error}`, LOG_LEVELS.WARN); + log(`Failed to retrieve window title.\n${error}`, LogLevel.Warn); return []; } - const match = tasklistOutput.match(/Window Title:\s*(.*)/); - if (match && match[1]) { + const regex = /Window Title:\s*(.*)/; + const match = regex.exec(tasklistOutput); + if (match && match.length > 1 && match[1]) { const title = match[1].trim(); if (title === 'N/A') { return []; @@ -41,7 +44,7 @@ async function captureTitlesOnWindows(processId) { return []; } -async function captureTitlesOnLinux(processId) { +async function captureTitlesOnLinux(processId: number): Promise { if (!processId) { throw new Error('Missing process ID.'); } const { stdout: windowIdsOutput, error: windowIdError } = await runCommand( @@ -49,7 +52,7 @@ async function captureTitlesOnLinux(processId) { ); if (windowIdError || !windowIdsOutput) { - return undefined; + return []; } const windowIds = windowIdsOutput.trim().split('\n'); @@ -69,23 +72,24 @@ async function captureTitlesOnLinux(processId) { let hasAssistiveAccessOnMac = true; -async function captureTitlesOnMac(processId) { +async function captureTitlesOnMac(processId: number): Promise { if (!processId) { throw new Error('Missing process ID.'); } if (!hasAssistiveAccessOnMac) { return []; } const script = ` - tell application "System Events" + tell application "System Events" try set targetProcess to first process whose unix id is ${processId} on error return end try tell targetProcess - if (count of windows) > 0 then - set window_name to name of front window - return window_name - end if + set allWindowNames to {} + repeat with aWindow in windows + set end of allWindowNames to name of aWindow + end repeat + return allWindowNames end tell end tell `; @@ -102,7 +106,7 @@ async function captureTitlesOnMac(processId) { hasAssistiveAccessOnMac = false; } errorMessage += error; - log(errorMessage, LOG_LEVELS.WARN); + log(errorMessage, LogLevel.Warn); return []; } const title = titleOutput?.trim(); diff --git a/scripts/check-desktop-runtime-errors/cli-args.js b/scripts/check-desktop-runtime-errors/cli-args.js deleted file mode 100644 index f9557efc..00000000 --- a/scripts/check-desktop-runtime-errors/cli-args.js +++ /dev/null @@ -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); -} diff --git a/scripts/check-desktop-runtime-errors/cli-args.ts b/scripts/check-desktop-runtime-errors/cli-args.ts new file mode 100644 index 00000000..91aa247b --- /dev/null +++ b/scripts/check-desktop-runtime-errors/cli-args.ts @@ -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]); +} diff --git a/scripts/check-desktop-runtime-errors/config.js b/scripts/check-desktop-runtime-errors/config.ts similarity index 100% rename from scripts/check-desktop-runtime-errors/config.js rename to scripts/check-desktop-runtime-errors/config.ts diff --git a/scripts/check-desktop-runtime-errors/index.js b/scripts/check-desktop-runtime-errors/index.js deleted file mode 100644 index 904a873a..00000000 --- a/scripts/check-desktop-runtime-errors/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { main } from './main.js'; - -await main(); diff --git a/scripts/check-desktop-runtime-errors/index.ts b/scripts/check-desktop-runtime-errors/index.ts new file mode 100644 index 00000000..3d44da4c --- /dev/null +++ b/scripts/check-desktop-runtime-errors/index.ts @@ -0,0 +1,3 @@ +import { main } from './main'; + +await main(); diff --git a/scripts/check-desktop-runtime-errors/main.js b/scripts/check-desktop-runtime-errors/main.ts similarity index 50% rename from scripts/check-desktop-runtime-errors/main.js rename to scripts/check-desktop-runtime-errors/main.ts index 79912963..8e425cb5 100644 --- a/scripts/check-desktop-runtime-errors/main.js +++ b/scripts/check-desktop-runtime-errors/main.ts @@ -1,22 +1,24 @@ -import { logCurrentArgs, COMMAND_LINE_FLAGS, hasCommandLineFlag } from './cli-args.js'; -import { log, die } from './utils/log.js'; -import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm.js'; -import { clearAppLogFile } from './app/app-logs.js'; -import { checkForErrors } from './app/check-for-errors.js'; +import { logCurrentArgs, CommandLineFlag, hasCommandLineFlag } from './cli-args'; +import { log, die } from './utils/log'; +import { ensureNpmProjectDir, npmInstall, npmBuild } from './utils/npm'; +import { clearAppLogFiles } from './app/app-logs'; +import { checkForErrors } from './app/check-for-errors'; import { runApplication } from './app/runner.js'; -import { CURRENT_PLATFORM, SUPPORTED_PLATFORMS } from './utils/platform.js'; -import { prepareLinuxApp } from './app/extractors/linux.js'; +import { CURRENT_PLATFORM, SupportedPlatform } from './utils/platform'; +import { prepareLinuxApp } from './app/extractors/linux'; import { prepareWindowsApp } from './app/extractors/windows.js'; -import { prepareMacOsApp } from './app/extractors/macos.js'; +import { prepareMacOsApp } from './app/extractors/macos'; import { DESKTOP_BUILD_COMMAND, PROJECT_DIR, DESKTOP_DIST_PATH, APP_EXECUTION_DURATION_IN_SECONDS, 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 { logCurrentArgs(); await ensureNpmProjectDir(PROJECT_DIR); await npmInstall(PROJECT_DIR); @@ -24,14 +26,14 @@ export async function main() { PROJECT_DIR, DESKTOP_BUILD_COMMAND, DESKTOP_DIST_PATH, - hasCommandLineFlag(COMMAND_LINE_FLAGS.FORCE_REBUILD), + hasCommandLineFlag(CommandLineFlag.ForceRebuild), ); - await clearAppLogFile(PROJECT_DIR); + await clearAppLogFiles(PROJECT_DIR); const { stderr, stdout, isCrashed, windowTitles, } = await extractAndRun(); if (stdout) { - log(`Output (stdout) from application execution:\n${stdout}`); + log(`Output (stdout) from application execution:\n${indentText(stdout, 1)}`); } if (isCrashed) { die('The application encountered an error during its execution.'); @@ -42,21 +44,23 @@ export async function main() { } async function extractAndRun() { - const extractors = { - [SUPPORTED_PLATFORMS.MAC]: () => prepareMacOsApp(DESKTOP_DIST_PATH), - [SUPPORTED_PLATFORMS.LINUX]: () => prepareLinuxApp(DESKTOP_DIST_PATH), - [SUPPORTED_PLATFORMS.WINDOWS]: () => prepareWindowsApp(DESKTOP_DIST_PATH), + const extractors: { + readonly [K in SupportedPlatform]: () => Promise; + } = { + [SupportedPlatform.macOS]: () => prepareMacOsApp(DESKTOP_DIST_PATH), + [SupportedPlatform.Linux]: () => prepareLinuxApp(DESKTOP_DIST_PATH), + [SupportedPlatform.Windows]: () => prepareWindowsApp(DESKTOP_DIST_PATH), }; const extractor = extractors[CURRENT_PLATFORM]; if (!extractor) { - throw new Error(`Platform not supported: ${CURRENT_PLATFORM}`); + throw new Error(`Platform not supported: ${SupportedPlatform[CURRENT_PLATFORM]}`); } const { appExecutablePath, cleanup } = await extractor(); try { return await runApplication( appExecutablePath, APP_EXECUTION_DURATION_IN_SECONDS, - hasCommandLineFlag(COMMAND_LINE_FLAGS.TAKE_SCREENSHOT), + hasCommandLineFlag(CommandLineFlag.TakeScreenshot), SCREENSHOT_PATH, ); } finally { diff --git a/scripts/check-desktop-runtime-errors/utils/io.js b/scripts/check-desktop-runtime-errors/utils/io.ts similarity index 67% rename from scripts/check-desktop-runtime-errors/utils/io.js rename to scripts/check-desktop-runtime-errors/utils/io.ts index fdebceb9..195d787e 100644 --- a/scripts/check-desktop-runtime-errors/utils/io.js +++ b/scripts/check-desktop-runtime-errors/utils/io.ts @@ -1,15 +1,17 @@ import { extname, join } from 'path'; import { readdir, access } from 'fs/promises'; 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 { if (!directory) { throw new Error('Missing directory'); } if (!extension) { throw new Error('Missing file extension'); } if (!await exists(directory)) { - die(`Directory does not exist: ${directory}`); - return []; + return die(`Directory does not exist: ${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` ); if (!withoutUninstaller.length) { - die(`No ${extension} found in ${directory} directory.`); + return die(`No ${extension} found in ${directory} directory.`); } 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 { absolutePath: join(directory, withoutUninstaller[0]), }; } -export async function exists(path) { +interface FileSearchResult { + readonly absolutePath?: string; +} + +export async function exists(path: string): Promise { if (!path) { throw new Error('Missing path'); } try { 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 { if (!dir) { throw new Error('Missing directory'); } if (!await exists(dir)) { return true; diff --git a/scripts/check-desktop-runtime-errors/utils/log.js b/scripts/check-desktop-runtime-errors/utils/log.js deleted file mode 100644 index 66b09c1d..00000000 --- a/scripts/check-desktop-runtime-errors/utils/log.js +++ /dev/null @@ -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, - }, -}; diff --git a/scripts/check-desktop-runtime-errors/utils/log.ts b/scripts/check-desktop-runtime-errors/utils/log.ts new file mode 100644 index 00000000..9edc911a --- /dev/null +++ b/scripts/check-desktop-runtime-errors/utils/log.ts @@ -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, + }, +}; diff --git a/scripts/check-desktop-runtime-errors/utils/npm.js b/scripts/check-desktop-runtime-errors/utils/npm.js deleted file mode 100644 index b74e600b..00000000 --- a/scripts/check-desktop-runtime-errors/utils/npm.js +++ /dev/null @@ -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; - } -} diff --git a/scripts/check-desktop-runtime-errors/utils/npm.ts b/scripts/check-desktop-runtime-errors/utils/npm.ts new file mode 100644 index 00000000..fec3a547 --- /dev/null +++ b/scripts/check-desktop-runtime-errors/utils/npm.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; +} diff --git a/scripts/check-desktop-runtime-errors/utils/platform.js b/scripts/check-desktop-runtime-errors/utils/platform.js deleted file mode 100644 index 24acb111..00000000 --- a/scripts/check-desktop-runtime-errors/utils/platform.js +++ /dev/null @@ -1,9 +0,0 @@ -import { platform } from 'os'; - -export const SUPPORTED_PLATFORMS = { - MAC: 'darwin', - LINUX: 'linux', - WINDOWS: 'win32', -}; - -export const CURRENT_PLATFORM = platform(); diff --git a/scripts/check-desktop-runtime-errors/utils/platform.ts b/scripts/check-desktop-runtime-errors/utils/platform.ts new file mode 100644 index 00000000..9f60f19a --- /dev/null +++ b/scripts/check-desktop-runtime-errors/utils/platform.ts @@ -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(); diff --git a/scripts/check-desktop-runtime-errors/utils/run-command.js b/scripts/check-desktop-runtime-errors/utils/run-command.ts similarity index 51% rename from scripts/check-desktop-runtime-errors/utils/run-command.js rename to scripts/check-desktop-runtime-errors/utils/run-command.ts index ddcac9f9..6fee517d 100644 --- a/scripts/check-desktop-runtime-errors/utils/run-command.js +++ b/scripts/check-desktop-runtime-errors/utils/run-command.ts @@ -1,22 +1,26 @@ 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 MAX_OUTPUT_BUFFER_SIZE = 1024 * 1024; // 1 MB -export function runCommand(commandString, options) { +export function runCommand( + command: string, + options?: ExecOptions, +): Promise { return new Promise((resolve) => { options = { cwd: process.cwd(), timeout: TIMEOUT_IN_SECONDS * 1000, maxBuffer: MAX_OUTPUT_BUFFER_SIZE * 2, - ...options, + ...(options ?? {}), }; - exec(commandString, options, (error, stdout, stderr) => { - let errorText; + exec(command, options, (error, stdout, stderr) => { + let errorText: string | undefined; if (error || stderr?.length > 0) { - errorText = formatError(commandString, error, stdout, stderr); + errorText = formatError(command, error, stdout, stderr); } resolve({ 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 = [ 'Error while running command.', - `Command:\n${indentText(commandString, 1)}`, + `Command:\n${indentText(command, 1)}`, ]; if (error?.toString().trim()) { errorParts.push(`Error:\n${indentText(error.toString(), 1)}`); } - if (stderr?.toString().trim()) { + if (stderr?.trim()) { errorParts.push(`stderr:\n${indentText(stderr, 1)}`); } - if (stdout?.toString().trim()) { + if (stdout?.trim()) { errorParts.push(`stdout:\n${indentText(stdout, 1)}`); } return errorParts.join('\n---\n'); diff --git a/scripts/check-desktop-runtime-errors/utils/sleep.ts b/scripts/check-desktop-runtime-errors/utils/sleep.ts new file mode 100644 index 00000000..0b80a7b2 --- /dev/null +++ b/scripts/check-desktop-runtime-errors/utils/sleep.ts @@ -0,0 +1,5 @@ +export function sleep(milliseconds: number) { + return new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +} diff --git a/scripts/check-desktop-runtime-errors/utils/text.js b/scripts/check-desktop-runtime-errors/utils/text.ts similarity index 66% rename from scripts/check-desktop-runtime-errors/utils/text.js rename to scripts/check-desktop-runtime-errors/utils/text.ts index 1a21d3ef..d9af9a97 100644 --- a/scripts/check-desktop-runtime-errors/utils/text.js +++ b/scripts/check-desktop-runtime-errors/utils/text.ts @@ -1,4 +1,7 @@ -export function indentText(text, indentLevel = 1) { +export function indentText( + text: string, + indentLevel = 1, +): string { validateText(text); const indentation = '\t'.repeat(indentLevel); return splitTextIntoLines(text) @@ -6,13 +9,13 @@ export function indentText(text, indentLevel = 1) { .join('\n'); } -export function splitTextIntoLines(text) { +export function splitTextIntoLines(text: string): string[] { validateText(text); return text .split(/[\r\n]+/); } -function validateText(text) { +function validateText(text: string): void { if (typeof text !== 'string') { throw new Error(`text is not a string. It is: ${typeof text}\n${text}`); } diff --git a/src/TypeHelpers.ts b/src/TypeHelpers.ts index 5bed9b42..562eb95c 100644 --- a/src/TypeHelpers.ts +++ b/src/TypeHelpers.ts @@ -1,6 +1,7 @@ export type Constructible = { prototype: T; apply: (this: unknown, args: TArgs) => void; + readonly name: string; }; export type PropertyKeys = { diff --git a/src/application/Context/ApplicationContextFactory.ts b/src/application/Context/ApplicationContextFactory.ts index cb3e61a7..44350ef5 100644 --- a/src/application/Context/ApplicationContextFactory.ts +++ b/src/application/Context/ApplicationContextFactory.ts @@ -1,25 +1,23 @@ import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { IApplication } from '@/domain/IApplication'; -import { Environment } from '@/infrastructure/Environment/Environment'; -import { IEnvironment } from '@/infrastructure/Environment/IEnvironment'; +import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; import { IApplicationFactory } from '../IApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory'; import { ApplicationContext } from './ApplicationContext'; export async function buildContext( factory: IApplicationFactory = ApplicationFactory.Current, - environment = Environment.CurrentEnvironment, + environment = RuntimeEnvironment.CurrentEnvironment, ): Promise { if (!factory) { throw new Error('missing factory'); } if (!environment) { throw new Error('missing environment'); } const app = await factory.getApp(); - const os = getInitialOs(app, environment); + const os = getInitialOs(app, environment.os); return new ApplicationContext(app, os); } -function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem { - const currentOs = environment.os; +function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem { const supportedOsList = app.getSupportedOsList(); if (supportedOsList.includes(currentOs)) { return currentOs; diff --git a/src/application/Parser/ApplicationParser.ts b/src/application/Parser/ApplicationParser.ts index 30dc17ad..e5be8c57 100644 --- a/src/application/Parser/ApplicationParser.ts +++ b/src/application/Parser/ApplicationParser.ts @@ -7,14 +7,14 @@ import MacOsData from '@/application/collections/macos.yaml'; import LinuxData from '@/application/collections/linux.yaml'; import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; import { Application } from '@/domain/Application'; -import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata'; -import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory'; +import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata'; +import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; import { parseCategoryCollection } from './CategoryCollectionParser'; export function parseApplication( categoryParser = parseCategoryCollection, informationParser = parseProjectInformation, - metadata: IAppMetadata = AppMetadataFactory.Current.instance, + metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance, collectionsData = PreParsedCollections, ): IApplication { validateCollectionsData(collectionsData); diff --git a/src/application/Parser/ProjectInformationParser.ts b/src/application/Parser/ProjectInformationParser.ts index b6582175..7f573040 100644 --- a/src/application/Parser/ProjectInformationParser.ts +++ b/src/application/Parser/ProjectInformationParser.ts @@ -1,13 +1,13 @@ import { IProjectInformation } from '@/domain/IProjectInformation'; import { ProjectInformation } from '@/domain/ProjectInformation'; -import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata'; +import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata'; import { Version } from '@/domain/Version'; -import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory'; +import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; import { ConstructorArguments } from '@/TypeHelpers'; export function parseProjectInformation( - metadata: IAppMetadata = AppMetadataFactory.Current.instance, + metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance, createProjectInformation: ProjectInformationFactory = ( ...args ) => new ProjectInformation(...args), diff --git a/src/infrastructure/CodeRunner.ts b/src/infrastructure/CodeRunner.ts index 317758eb..30ae771c 100644 --- a/src/infrastructure/CodeRunner.ts +++ b/src/infrastructure/CodeRunner.ts @@ -1,32 +1,37 @@ -import { Environment } from '@/infrastructure/Environment/Environment'; +import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; import { OperatingSystem } from '@/domain/OperatingSystem'; +import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations'; export class CodeRunner { constructor( - private readonly environment = Environment.CurrentEnvironment, + private readonly system = getWindowInjectedSystemOperations(), + private readonly environment = RuntimeEnvironment.CurrentEnvironment, ) { - if (!environment.system) { + if (!system) { throw new Error('missing system operations'); } } public async runCode(code: string, folderName: string, fileExtension: string): Promise { - const { system } = this.environment; - const dir = system.location.combinePaths( - system.operatingSystem.getTempDirectory(), + const { os } = this.environment; + const dir = this.system.location.combinePaths( + this.system.operatingSystem.getTempDirectory(), folderName, ); - await system.fileSystem.createDirectory(dir, true); - const filePath = system.location.combinePaths(dir, `run.${fileExtension}`); - await system.fileSystem.writeToFile(filePath, code); - await system.fileSystem.setFilePermissions(filePath, '755'); - const command = getExecuteCommand(filePath, this.environment); - system.command.execute(command); + await this.system.fileSystem.createDirectory(dir, true); + const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`); + await this.system.fileSystem.writeToFile(filePath, code); + await this.system.fileSystem.setFilePermissions(filePath, '755'); + const command = getExecuteCommand(filePath, os); + this.system.command.execute(command); } } -function getExecuteCommand(scriptPath: string, environment: Environment): string { - switch (environment.os) { +function getExecuteCommand( + scriptPath: string, + currentOperatingSystem: OperatingSystem, +): string { + switch (currentOperatingSystem) { case OperatingSystem.Linux: return `x-terminal-emulator -e '${scriptPath}'`; case OperatingSystem.macOS: @@ -37,6 +42,6 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string case OperatingSystem.Windows: return scriptPath; default: - throw Error(`unsupported os: ${OperatingSystem[environment.os]}`); + throw Error(`unsupported os: ${OperatingSystem[currentOperatingSystem]}`); } } diff --git a/src/infrastructure/Environment/IEnvironment.ts b/src/infrastructure/Environment/IEnvironment.ts deleted file mode 100644 index 32f59841..00000000 --- a/src/infrastructure/Environment/IEnvironment.ts +++ /dev/null @@ -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; -} diff --git a/src/infrastructure/Environment/WindowVariables.ts b/src/infrastructure/Environment/WindowVariables.ts deleted file mode 100644 index 36ad4523..00000000 --- a/src/infrastructure/Environment/WindowVariables.ts +++ /dev/null @@ -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 { } -} diff --git a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.ts b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.ts new file mode 100644 index 00000000..9d333623 --- /dev/null +++ b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory.ts @@ -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; diff --git a/src/infrastructure/Metadata/MetadataValidator.ts b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts similarity index 66% rename from src/infrastructure/Metadata/MetadataValidator.ts rename to src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts index d050c4da..70676ce4 100644 --- a/src/infrastructure/Metadata/MetadataValidator.ts +++ b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts @@ -1,24 +1,24 @@ -import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata'; +import { IEnvironmentVariables } from './IEnvironmentVariables'; /* Validation is externalized to keep the environment objects simple */ -export function validateMetadata(metadata: IAppMetadata): void { - if (!metadata) { - throw new Error('missing metadata'); +export function validateEnvironmentVariables(environment: IEnvironmentVariables): void { + if (!environment) { + throw new Error('missing environment'); } - const keyValues = capturePropertyValues(metadata); + const keyValues = capturePropertyValues(environment); if (!Object.keys(keyValues).length) { - throw new Error('Unable to capture metadata key/value pairs'); + throw new Error('Unable to capture key/value pairs'); } - const keysMissingValue = getMissingMetadataKeys(keyValues); + const keysMissingValue = getKeysMissingValues(keyValues); if (keysMissingValue.length > 0) { - throw new Error(`Metadata keys missing: ${keysMissingValue.join(', ')}`); + throw new Error(`Environment keys missing: ${keysMissingValue.join(', ')}`); } } -function getMissingMetadataKeys(keyValuePairs: Record): string[] { +function getKeysMissingValues(keyValuePairs: Record): string[] { return Object.entries(keyValuePairs) .reduce((acc, [key, value]) => { - if (!value) { + if (!value && typeof value !== 'boolean') { acc.push(key); } return acc; diff --git a/src/infrastructure/Metadata/IAppMetadata.ts b/src/infrastructure/EnvironmentVariables/IAppMetadata.ts similarity index 64% rename from src/infrastructure/Metadata/IAppMetadata.ts rename to src/infrastructure/EnvironmentVariables/IAppMetadata.ts index c616c25f..7d3b3706 100644 --- a/src/infrastructure/Metadata/IAppMetadata.ts +++ b/src/infrastructure/EnvironmentVariables/IAppMetadata.ts @@ -1,8 +1,5 @@ /** * Represents essential metadata about the application. - * - * Designed to decouple the process of retrieving metadata - * (e.g., from the build environment) from the rest of the application. */ export interface IAppMetadata { readonly version: string; diff --git a/src/infrastructure/EnvironmentVariables/IEnvironmentVariables.ts b/src/infrastructure/EnvironmentVariables/IEnvironmentVariables.ts new file mode 100644 index 00000000..0255655f --- /dev/null +++ b/src/infrastructure/EnvironmentVariables/IEnvironmentVariables.ts @@ -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; +} diff --git a/src/infrastructure/EnvironmentVariables/IEnvironmentVariablesFactory.ts b/src/infrastructure/EnvironmentVariables/IEnvironmentVariablesFactory.ts new file mode 100644 index 00000000..26d1e569 --- /dev/null +++ b/src/infrastructure/EnvironmentVariables/IEnvironmentVariablesFactory.ts @@ -0,0 +1,5 @@ +import { IEnvironmentVariables } from './IEnvironmentVariables'; + +export interface IEnvironmentVariablesFactory { + readonly instance: IEnvironmentVariables; +} diff --git a/src/infrastructure/Metadata/Vite/ViteEnvironmentKeys.ts b/src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentKeys.ts similarity index 71% rename from src/infrastructure/Metadata/Vite/ViteEnvironmentKeys.ts rename to src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentKeys.ts index 62d0e9a9..8ab0f712 100644 --- a/src/infrastructure/Metadata/Vite/ViteEnvironmentKeys.ts +++ b/src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentKeys.ts @@ -1,8 +1,13 @@ // Only variables prefixed with VITE_ are exposed to Vite-processed code -export const VITE_ENVIRONMENT_KEYS = { +export const VITE_USER_DEFINED_ENVIRONMENT_KEYS = { VERSION: 'VITE_APP_VERSION', NAME: 'VITE_APP_NAME', SLOGAN: 'VITE_APP_SLOGAN', REPOSITORY_URL: 'VITE_APP_REPOSITORY_URL', HOMEPAGE_URL: 'VITE_APP_HOMEPAGE_URL', } as const; + +export const VITE_ENVIRONMENT_KEYS = { + ...VITE_USER_DEFINED_ENVIRONMENT_KEYS, + DEV: 'DEV', +} as const; diff --git a/src/infrastructure/Metadata/Vite/ViteAppMetadata.ts b/src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.ts similarity index 68% rename from src/infrastructure/Metadata/Vite/ViteAppMetadata.ts rename to src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.ts index 80ea3488..2d3f5b70 100644 --- a/src/infrastructure/Metadata/Vite/ViteAppMetadata.ts +++ b/src/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables.ts @@ -1,9 +1,9 @@ -import { IAppMetadata } from '../IAppMetadata'; +import { IEnvironmentVariables } from '../IEnvironmentVariables'; /** - * Provides the application's metadata using Vite's environment variables. + * Provides the application's environment variables. */ -export class ViteAppMetadata implements IAppMetadata { +export class ViteEnvironmentVariables implements IEnvironmentVariables { // Ensure the use of import.meta.env prefix for the following properties. // Vue will replace these statically during production builds. @@ -26,4 +26,8 @@ export class ViteAppMetadata implements IAppMetadata { public get homepageUrl(): string { return import.meta.env.VITE_APP_HOMEPAGE_URL; } + + public get isNonProduction(): boolean { + return import.meta.env.DEV; + } } diff --git a/src/infrastructure/Metadata/Vite/vite-env.d.ts b/src/infrastructure/EnvironmentVariables/Vite/vite-env.d.ts similarity index 100% rename from src/infrastructure/Metadata/Vite/vite-env.d.ts rename to src/infrastructure/EnvironmentVariables/Vite/vite-env.d.ts diff --git a/src/infrastructure/Log/ConsoleLogger.ts b/src/infrastructure/Log/ConsoleLogger.ts new file mode 100644 index 00000000..4a1bc6bd --- /dev/null +++ b/src/infrastructure/Log/ConsoleLogger.ts @@ -0,0 +1,13 @@ +import { ILogger } from './ILogger'; + +export class ConsoleLogger implements ILogger { + constructor(private readonly globalConsole: Partial = console) { + if (!globalConsole) { + throw new Error('missing console'); + } + } + + public info(...params: unknown[]): void { + this.globalConsole.info(...params); + } +} diff --git a/src/infrastructure/Log/ElectronLogger.ts b/src/infrastructure/Log/ElectronLogger.ts new file mode 100644 index 00000000..5a1dc1ae --- /dev/null +++ b/src/infrastructure/Log/ElectronLogger.ts @@ -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): ILogger { + if (!logger) { + throw new Error('missing logger'); + } + return { + info: (...params) => logger.info(...params), + }; +} diff --git a/src/infrastructure/Log/ILogger.ts b/src/infrastructure/Log/ILogger.ts new file mode 100644 index 00000000..954d1272 --- /dev/null +++ b/src/infrastructure/Log/ILogger.ts @@ -0,0 +1,3 @@ +export interface ILogger { + info (...params: unknown[]): void; +} diff --git a/src/infrastructure/Log/ILoggerFactory.ts b/src/infrastructure/Log/ILoggerFactory.ts new file mode 100644 index 00000000..0e6ba3db --- /dev/null +++ b/src/infrastructure/Log/ILoggerFactory.ts @@ -0,0 +1,5 @@ +import { ILogger } from './ILogger'; + +export interface ILoggerFactory { + readonly logger: ILogger; +} diff --git a/src/infrastructure/Log/NoopLogger.ts b/src/infrastructure/Log/NoopLogger.ts new file mode 100644 index 00000000..54cc6f08 --- /dev/null +++ b/src/infrastructure/Log/NoopLogger.ts @@ -0,0 +1,5 @@ +import { ILogger } from './ILogger'; + +export class NoopLogger implements ILogger { + public info(): void { /* NOOP */ } +} diff --git a/src/infrastructure/Log/WindowInjectedLogger.ts b/src/infrastructure/Log/WindowInjectedLogger.ts new file mode 100644 index 00000000..c3642dbe --- /dev/null +++ b/src/infrastructure/Log/WindowInjectedLogger.ts @@ -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); + } +} diff --git a/src/infrastructure/Metadata/AppMetadataFactory.ts b/src/infrastructure/Metadata/AppMetadataFactory.ts deleted file mode 100644 index e303c788..00000000 --- a/src/infrastructure/Metadata/AppMetadataFactory.ts +++ /dev/null @@ -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; diff --git a/src/infrastructure/Metadata/IAppMetadataFactory.ts b/src/infrastructure/Metadata/IAppMetadataFactory.ts deleted file mode 100644 index c8caab3a..00000000 --- a/src/infrastructure/Metadata/IAppMetadataFactory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IAppMetadata } from './IAppMetadata'; - -export interface IAppMetadataFactory { - readonly instance: IAppMetadata; -} diff --git a/src/infrastructure/Environment/BrowserOs/BrowserOsDetector.ts b/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector.ts similarity index 100% rename from src/infrastructure/Environment/BrowserOs/BrowserOsDetector.ts rename to src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector.ts diff --git a/src/infrastructure/Environment/BrowserOs/DetectorBuilder.ts b/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts similarity index 100% rename from src/infrastructure/Environment/BrowserOs/DetectorBuilder.ts rename to src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts diff --git a/src/infrastructure/Environment/BrowserOs/IBrowserOsDetector.ts b/src/infrastructure/RuntimeEnvironment/BrowserOs/IBrowserOsDetector.ts similarity index 100% rename from src/infrastructure/Environment/BrowserOs/IBrowserOsDetector.ts rename to src/infrastructure/RuntimeEnvironment/BrowserOs/IBrowserOsDetector.ts diff --git a/src/infrastructure/RuntimeEnvironment/IRuntimeEnvironment.ts b/src/infrastructure/RuntimeEnvironment/IRuntimeEnvironment.ts new file mode 100644 index 00000000..52b5ea85 --- /dev/null +++ b/src/infrastructure/RuntimeEnvironment/IRuntimeEnvironment.ts @@ -0,0 +1,7 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; + +export interface IRuntimeEnvironment { + readonly isDesktop: boolean; + readonly os: OperatingSystem | undefined; + readonly isNonProduction: boolean; +} diff --git a/src/infrastructure/Environment/Environment.ts b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts similarity index 57% rename from src/infrastructure/Environment/Environment.ts rename to src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts index cb059854..fa088a15 100644 --- a/src/infrastructure/Environment/Environment.ts +++ b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts @@ -1,29 +1,29 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; -import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations'; +import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; +import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables'; +import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector'; -import { IEnvironment } from './IEnvironment'; -import { WindowVariables } from './WindowVariables'; -import { validateWindowVariables } from './WindowVariablesValidator'; +import { IRuntimeEnvironment } from './IRuntimeEnvironment'; -export class Environment implements IEnvironment { - public static readonly CurrentEnvironment: IEnvironment = new Environment(window); +export class RuntimeEnvironment implements IRuntimeEnvironment { + public static readonly CurrentEnvironment: IRuntimeEnvironment = new RuntimeEnvironment(window); public readonly isDesktop: boolean; public readonly os: OperatingSystem | undefined; - public readonly system: ISystemOperations | undefined; + public readonly isNonProduction: boolean; protected constructor( window: Partial, + environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance, browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(), - windowValidator: WindowValidator = validateWindowVariables, ) { if (!window) { throw new Error('missing window'); } - windowValidator(window); + this.isNonProduction = environmentVariables.isNonProduction; this.isDesktop = isDesktop(window); if (this.isDesktop) { this.os = window?.os; @@ -34,7 +34,6 @@ export class Environment implements IEnvironment { this.os = browserOsDetector.detect(userAgent); } } - this.system = window?.system; } } @@ -45,5 +44,3 @@ function getUserAgent(window: Partial): string { function isDesktop(window: Partial): boolean { return window?.isDesktop === true; } - -export type WindowValidator = typeof validateWindowVariables; diff --git a/src/infrastructure/RuntimeSanity/Common/ISanityCheckOptions.ts b/src/infrastructure/RuntimeSanity/Common/ISanityCheckOptions.ts index ccbfa7de..574bc3a8 100644 --- a/src/infrastructure/RuntimeSanity/Common/ISanityCheckOptions.ts +++ b/src/infrastructure/RuntimeSanity/Common/ISanityCheckOptions.ts @@ -1,4 +1,4 @@ export interface ISanityCheckOptions { - readonly validateMetadata: boolean; - readonly validateEnvironment: boolean; + readonly validateEnvironmentVariables: boolean; + readonly validateWindowVariables: boolean; } diff --git a/src/infrastructure/RuntimeSanity/SanityChecks.ts b/src/infrastructure/RuntimeSanity/SanityChecks.ts index 38173661..36f44bf9 100644 --- a/src/infrastructure/RuntimeSanity/SanityChecks.ts +++ b/src/infrastructure/RuntimeSanity/SanityChecks.ts @@ -1,9 +1,9 @@ import { ISanityCheckOptions } from './Common/ISanityCheckOptions'; import { ISanityValidator } from './Common/ISanityValidator'; -import { MetadataValidator } from './Validators/MetadataValidator'; +import { EnvironmentVariablesValidator } from './Validators/EnvironmentVariablesValidator'; const DefaultSanityValidators: ISanityValidator[] = [ - new MetadataValidator(), + new EnvironmentVariablesValidator(), ]; /* Helps to fail-fast on errors */ diff --git a/src/infrastructure/RuntimeSanity/Validators/EnvironmentValidator.ts b/src/infrastructure/RuntimeSanity/Validators/EnvironmentValidator.ts deleted file mode 100644 index fb383f7c..00000000 --- a/src/infrastructure/RuntimeSanity/Validators/EnvironmentValidator.ts +++ /dev/null @@ -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 { - constructor(factory: FactoryFunction = () => Environment.CurrentEnvironment) { - super(factory); - } - - public override name = 'environment'; - - public override shouldValidate(options: ISanityCheckOptions): boolean { - return options.validateEnvironment; - } -} diff --git a/src/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.ts b/src/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.ts new file mode 100644 index 00000000..072324ae --- /dev/null +++ b/src/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator.ts @@ -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 { + constructor( + factory: FactoryFunction = () => { + return EnvironmentVariablesFactory.Current.instance; + }, + ) { + super(factory); + } + + public override name = 'environment variables'; + + public override shouldValidate(options: ISanityCheckOptions): boolean { + return options.validateEnvironmentVariables; + } +} diff --git a/src/infrastructure/RuntimeSanity/Validators/MetadataValidator.ts b/src/infrastructure/RuntimeSanity/Validators/MetadataValidator.ts deleted file mode 100644 index a2c3630d..00000000 --- a/src/infrastructure/RuntimeSanity/Validators/MetadataValidator.ts +++ /dev/null @@ -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 { - constructor(factory: FactoryFunction = () => AppMetadataFactory.Current.instance) { - super(factory); - } - - public override name = 'metadata'; - - public override shouldValidate(options: ISanityCheckOptions): boolean { - return options.validateMetadata; - } -} diff --git a/src/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.ts b/src/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.ts new file mode 100644 index 00000000..553cbddb --- /dev/null +++ b/src/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator.ts @@ -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 { + constructor(factory: FactoryFunction = () => window) { + super(factory); + } + + public override name = 'window variables'; + + public override shouldValidate(options: ISanityCheckOptions): boolean { + return options.validateWindowVariables; + } +} diff --git a/src/infrastructure/Environment/SystemOperations/ISystemOperations.ts b/src/infrastructure/SystemOperations/ISystemOperations.ts similarity index 100% rename from src/infrastructure/Environment/SystemOperations/ISystemOperations.ts rename to src/infrastructure/SystemOperations/ISystemOperations.ts diff --git a/src/infrastructure/Environment/SystemOperations/NodeSystemOperations.ts b/src/infrastructure/SystemOperations/NodeSystemOperations.ts similarity index 100% rename from src/infrastructure/Environment/SystemOperations/NodeSystemOperations.ts rename to src/infrastructure/SystemOperations/NodeSystemOperations.ts diff --git a/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts b/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts new file mode 100644 index 00000000..b1be9800 --- /dev/null +++ b/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts @@ -0,0 +1,14 @@ +import { WindowVariables } from '../WindowVariables/WindowVariables'; +import { ISystemOperations } from './ISystemOperations'; + +export function getWindowInjectedSystemOperations( + windowVariables: Partial = window, +): ISystemOperations { + if (!windowVariables) { + throw new Error('missing window'); + } + if (!windowVariables.system) { + throw new Error('missing system'); + } + return windowVariables.system; +} diff --git a/src/infrastructure/WindowVariables/WindowVariables.ts b/src/infrastructure/WindowVariables/WindowVariables.ts new file mode 100644 index 00000000..06485ce6 --- /dev/null +++ b/src/infrastructure/WindowVariables/WindowVariables.ts @@ -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; +} diff --git a/src/infrastructure/Environment/WindowVariablesValidator.ts b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts similarity index 83% rename from src/infrastructure/Environment/WindowVariablesValidator.ts rename to src/infrastructure/WindowVariables/WindowVariablesValidator.ts index 0241f451..66d764e9 100644 --- a/src/infrastructure/Environment/WindowVariablesValidator.ts +++ b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts @@ -6,11 +6,8 @@ import { WindowVariables } from './WindowVariables'; * Checks for consistency in runtime environment properties injected by Electron preloader. */ export function validateWindowVariables(variables: Partial) { - if (!variables) { - throw new Error('missing variables'); - } if (!isObject(variables)) { - throw new Error(`window is not an object but ${typeof variables}`); + throw new Error('window is not an object'); } const errors = [...testEveryProperty(variables)]; if (errors.length > 0) { @@ -25,6 +22,7 @@ function* testEveryProperty(variables: Partial): Iterable): boolean { + if (!variables.isDesktop) { + return true; + } + return isObject(variables.log); +} + function testSystem(variables: Partial): boolean { if (!variables.isDesktop) { return true; } - return variables.system !== undefined && isObject(variables.system); + return isObject(variables.system); } function testIsDesktop(isDesktop: unknown): boolean { @@ -70,7 +75,7 @@ function isBoolean(variable: unknown): variable is boolean { } function isObject(variable: unknown): variable is object { - return typeof variable === 'object' - && variable !== null // the data type of null is an object + return Boolean(variable) // the data type of null is an object + && typeof variable === 'object' && !Array.isArray(variable); } diff --git a/src/infrastructure/WindowVariables/window-variables.d.ts b/src/infrastructure/WindowVariables/window-variables.d.ts new file mode 100644 index 00000000..15f0bd73 --- /dev/null +++ b/src/infrastructure/WindowVariables/window-variables.d.ts @@ -0,0 +1,6 @@ +import { WindowVariables } from './WindowVariables'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Window extends WindowVariables { } +} diff --git a/src/presentation/bootstrapping/ApplicationBootstrapper.ts b/src/presentation/bootstrapping/ApplicationBootstrapper.ts index 357350ed..d73ca3cd 100644 --- a/src/presentation/bootstrapping/ApplicationBootstrapper.ts +++ b/src/presentation/bootstrapping/ApplicationBootstrapper.ts @@ -3,6 +3,8 @@ import { IconBootstrapper } from './Modules/IconBootstrapper'; import { VueConstructor, IVueBootstrapper } from './IVueBootstrapper'; import { VueBootstrapper } from './Modules/VueBootstrapper'; import { TooltipBootstrapper } from './Modules/TooltipBootstrapper'; +import { RuntimeSanityValidator } from './Modules/RuntimeSanityValidator'; +import { AppInitializationLogger } from './Modules/AppInitializationLogger'; export class ApplicationBootstrapper implements IVueBootstrapper { public bootstrap(vue: VueConstructor): void { @@ -18,6 +20,8 @@ export class ApplicationBootstrapper implements IVueBootstrapper { new TreeBootstrapper(), new VueBootstrapper(), new TooltipBootstrapper(), + new RuntimeSanityValidator(), + new AppInitializationLogger(), ]; } } diff --git a/src/presentation/bootstrapping/ClientLoggerFactory.ts b/src/presentation/bootstrapping/ClientLoggerFactory.ts new file mode 100644 index 00000000..e6b9f933 --- /dev/null +++ b/src/presentation/bootstrapping/ClientLoggerFactory.ts @@ -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(); + } +} diff --git a/src/presentation/bootstrapping/DependencyProvider.ts b/src/presentation/bootstrapping/DependencyProvider.ts index c5743188..c08cdc25 100644 --- a/src/presentation/bootstrapping/DependencyProvider.ts +++ b/src/presentation/bootstrapping/DependencyProvider.ts @@ -2,15 +2,15 @@ import { InjectionKey, provide } from 'vue'; import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState'; import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication'; import { - useCollectionStateKey, useApplicationKey, useEnvironmentKey, + useCollectionStateKey, useApplicationKey, useRuntimeEnvironmentKey, } from '@/presentation/injectionSymbols'; import { IApplicationContext } from '@/application/Context/IApplicationContext'; -import { Environment } from '@/infrastructure/Environment/Environment'; +import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; export function provideDependencies(context: IApplicationContext) { registerSingleton(useApplicationKey, useApplication(context.app)); registerTransient(useCollectionStateKey, () => useCollectionState(context)); - registerSingleton(useEnvironmentKey, Environment.CurrentEnvironment); + registerSingleton(useRuntimeEnvironmentKey, RuntimeEnvironment.CurrentEnvironment); } function registerSingleton( diff --git a/src/presentation/bootstrapping/Modules/AppInitializationLogger.ts b/src/presentation/bootstrapping/Modules/AppInitializationLogger.ts new file mode 100644 index 00000000..652ffbd6 --- /dev/null +++ b/src/presentation/bootstrapping/Modules/AppInitializationLogger.ts @@ -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.'); + } +} diff --git a/src/presentation/bootstrapping/Modules/RuntimeSanityValidator.ts b/src/presentation/bootstrapping/Modules/RuntimeSanityValidator.ts new file mode 100644 index 00000000..12d88b6e --- /dev/null +++ b/src/presentation/bootstrapping/Modules/RuntimeSanityValidator.ts @@ -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, + }); + } +} diff --git a/src/presentation/components/App.vue b/src/presentation/components/App.vue index 620426e3..b8a87bcd 100644 --- a/src/presentation/components/App.vue +++ b/src/presentation/components/App.vue @@ -18,7 +18,6 @@ import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeBu import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue'; import TheSearchBar from '@/presentation/components/TheSearchBar.vue'; import { buildContext } from '@/application/Context/ApplicationContextFactory'; -import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks'; import { provideDependencies } from '../bootstrapping/DependencyProvider'; const singletonAppContext = await buildContext(); @@ -33,10 +32,6 @@ export default defineComponent({ }, setup() { provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts - validateRuntimeSanity({ - validateMetadata: true, - validateEnvironment: true, - }); }, }); diff --git a/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue index c6551ce0..422f62b3 100644 --- a/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue +++ b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue @@ -29,11 +29,10 @@ import { defineComponent, ref, computed, inject, } from 'vue'; -import { useCollectionStateKey, useEnvironmentKey } from '@/presentation/injectionSymbols'; +import { useCollectionStateKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols'; import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog'; import { Clipboard } from '@/infrastructure/Clipboard'; import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'; -import { Environment } from '@/infrastructure/Environment/Environment'; import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; @@ -56,10 +55,10 @@ export default defineComponent({ const { currentState, currentContext, onStateChange, events, } = inject(useCollectionStateKey)(); - const { isDesktop } = inject(useEnvironmentKey); + const { os, isDesktop } = inject(useRuntimeEnvironmentKey); const areInstructionsVisible = ref(false); - const canRun = computed(() => getCanRunState(currentState.value.os, isDesktop)); + const canRun = computed(() => getCanRunState(currentState.value.os, isDesktop, os)); const fileName = computed(() => buildFileName(currentState.value.collection.scripting)); const hasCode = ref(false); const instructions = computed(() => getDownloadInstructions( @@ -122,8 +121,12 @@ function getDownloadInstructions( return getInstructions(os, fileName); } -function getCanRunState(selectedOs: OperatingSystem, isDesktopVersion: boolean): boolean { - const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os; +function getCanRunState( + selectedOs: OperatingSystem, + isDesktopVersion: boolean, + hostOs: OperatingSystem, +): boolean { + const isRunningOnSelectedOs = selectedOs === hostOs; return isDesktopVersion && isRunningOnSelectedOs; } diff --git a/src/presentation/components/Shared/Hooks/UseEnvironment.ts b/src/presentation/components/Shared/Hooks/UseEnvironment.ts deleted file mode 100644 index b6c3a6f4..00000000 --- a/src/presentation/components/Shared/Hooks/UseEnvironment.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IEnvironment } from '@/infrastructure/Environment/IEnvironment'; - -export function useEnvironment(environment: IEnvironment) { - if (!environment) { - throw new Error('missing environment'); - } - return environment; -} diff --git a/src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts b/src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts new file mode 100644 index 00000000..f6fb00cd --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseRuntimeEnvironment.ts @@ -0,0 +1,8 @@ +import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment'; + +export function useRuntimeEnvironment(environment: IRuntimeEnvironment) { + if (!environment) { + throw new Error('missing environment'); + } + return environment; +} diff --git a/src/presentation/components/TheFooter/DownloadUrlList.vue b/src/presentation/components/TheFooter/DownloadUrlList.vue index 4d605576..7f584f50 100644 --- a/src/presentation/components/TheFooter/DownloadUrlList.vue +++ b/src/presentation/components/TheFooter/DownloadUrlList.vue @@ -20,7 +20,7 @@