├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .nvmrc ├── .stylelintrc.json ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── babel.config.js ├── dist └── .gitkeep ├── electron-builder.js ├── jest.config.js ├── package.json ├── src ├── globals.d.ts ├── index.ejs ├── lib │ ├── crash │ │ └── reporter.ts │ ├── frameworks │ │ ├── emitter.ts │ │ ├── factory.ts │ │ ├── framework.ts │ │ ├── index.ts │ │ ├── jest │ │ │ └── framework.ts │ │ ├── nugget.ts │ │ ├── phpunit-10 │ │ │ ├── framework.ts │ │ │ └── suite.ts │ │ ├── phpunit │ │ │ ├── framework.ts │ │ │ └── suite.ts │ │ ├── progress.ts │ │ ├── project.ts │ │ ├── repository.ts │ │ ├── sort.ts │ │ ├── status.ts │ │ ├── suite.ts │ │ ├── test.ts │ │ └── validator.ts │ ├── helpers │ │ ├── durations.ts │ │ ├── os.ts │ │ ├── paths.ts │ │ ├── strings.ts │ │ └── translation.ts │ ├── logger │ │ ├── format.ts │ │ ├── index.ts │ │ ├── levels.ts │ │ ├── main.ts │ │ ├── preload.ts │ │ └── renderer.ts │ ├── process │ │ ├── debug │ │ │ ├── .gitkeep │ │ │ ├── log-1608548686.json │ │ │ └── log-1608548704.json │ │ ├── errors.ts │ │ ├── factory.ts │ │ ├── pool.ts │ │ ├── process.ts │ │ ├── queue.ts │ │ ├── runners │ │ │ ├── index.ts │ │ │ ├── npm.ts │ │ │ └── yarn.ts │ │ ├── search.ts │ │ ├── shell.ts │ │ └── ssh.ts │ ├── reporters │ │ ├── jest │ │ │ └── index.js │ │ ├── phpunit-10 │ │ │ ├── bootstrap.php │ │ │ └── src │ │ │ │ ├── Console.php │ │ │ │ ├── Extension.php │ │ │ │ ├── ExtensionHook.php │ │ │ │ ├── Feedback.php │ │ │ │ ├── Lode.php │ │ │ │ ├── LodeReporter.php │ │ │ │ ├── Report.php │ │ │ │ ├── Stacktrace.php │ │ │ │ ├── Status.php │ │ │ │ ├── Subscriber │ │ │ │ ├── ApplicationFinishedSubscriber.php │ │ │ │ ├── Subscriber.php │ │ │ │ ├── TestConsideredRiskySubscriber.php │ │ │ │ ├── TestDeprecationTriggeredSubscriber.php │ │ │ │ ├── TestErrorTriggeredSubscriber.php │ │ │ │ ├── TestErroredSubscriber.php │ │ │ │ ├── TestFailedSubscriber.php │ │ │ │ ├── TestFinishedSubscriber.php │ │ │ │ ├── TestMarkedIncompleteSubscriber.php │ │ │ │ ├── TestNoticeTriggeredSubscriber.php │ │ │ │ ├── TestPassedSubscriber.php │ │ │ │ ├── TestPhpDeprecationTriggeredSubscriber.php │ │ │ │ ├── TestPhpNoticeTriggeredSubscriber.php │ │ │ │ ├── TestPhpWarningTriggeredSubscriber.php │ │ │ │ ├── TestPhpunitDeprecationTriggeredSubscriber.php │ │ │ │ ├── TestPhpunitErrorTriggeredSubscriber.php │ │ │ │ ├── TestPhpunitWarningTriggeredSubscriber.php │ │ │ │ ├── TestSkippedSubscriber.php │ │ │ │ ├── TestSuiteStartedSubscriber.php │ │ │ │ └── TestWarningTriggeredSubscriber.php │ │ │ │ └── Util.php │ │ └── phpunit │ │ │ ├── bootstrap.php │ │ │ └── src │ │ │ ├── 48-57 │ │ │ ├── LodeReporter.php │ │ │ └── Report.php │ │ │ ├── 60-65 │ │ │ └── LodeReporter.php │ │ │ ├── 70-80 │ │ │ └── Printer.php │ │ │ ├── Console.php │ │ │ ├── Feedback.php │ │ │ ├── Lode.php │ │ │ ├── LodeReporter.php │ │ │ ├── Printer.php │ │ │ ├── Report.php │ │ │ ├── Stacktrace.php │ │ │ └── Util.php │ ├── state │ │ ├── index.ts │ │ └── project.ts │ └── themes │ │ └── index.ts ├── main │ ├── application-window.ts │ ├── file.ts │ ├── index.dev.ts │ ├── index.ts │ ├── menu │ │ ├── application-menu.ts │ │ ├── file-menu.ts │ │ ├── framework-menu.ts │ │ ├── index.ts │ │ ├── menu.ts │ │ ├── project-menu.ts │ │ ├── repository-menu.ts │ │ ├── suite-menu.ts │ │ └── test-menu.ts │ └── updater.ts ├── preload │ ├── index.ts │ ├── ipc.ts │ └── lode.ts ├── renderer │ ├── components │ │ ├── Ansi.vue │ │ ├── App.vue │ │ ├── Collapsible.vue │ │ ├── Console.vue │ │ ├── Diff.vue │ │ ├── Draggable.vue │ │ ├── Duration.vue │ │ ├── Feedback.vue │ │ ├── Filename.vue │ │ ├── Framework.vue │ │ ├── FrameworkSettings.vue │ │ ├── Icon.vue │ │ ├── Indicator.vue │ │ ├── KeyValue.vue │ │ ├── Ledger.vue │ │ ├── MetaTable.vue │ │ ├── ModalController.vue │ │ ├── Nugget.vue │ │ ├── Pane.vue │ │ ├── Parameters.vue │ │ ├── Project.vue │ │ ├── ProjectLoader.vue │ │ ├── Results.vue │ │ ├── Scrollable.vue │ │ ├── SidebarFramework.vue │ │ ├── SidebarRepository.vue │ │ ├── Snippet.vue │ │ ├── Split.vue │ │ ├── TestInformation.vue │ │ ├── TestResult.vue │ │ ├── Titlebar.vue │ │ ├── Trace.vue │ │ ├── mixins │ │ │ ├── HasFile.js │ │ │ └── HasFrameworkMenu.js │ │ └── modals │ │ │ ├── About.vue │ │ │ ├── AddRepositories.vue │ │ │ ├── AlertStack.vue │ │ │ ├── ConfirmSwitchProject.vue │ │ │ ├── EditProject.vue │ │ │ ├── Licenses.vue │ │ │ ├── ManageFrameworks.vue │ │ │ ├── Modal.vue │ │ │ ├── Preferences.vue │ │ │ ├── ProjectLoadingFailed.vue │ │ │ ├── RemoveFramework.vue │ │ │ ├── RemoveProject.vue │ │ │ ├── RemoveRepository.vue │ │ │ ├── ResetSettings.vue │ │ │ ├── RunningUnderTranslation.vue │ │ │ ├── Terms.vue │ │ │ └── mixins │ │ │ ├── confirm.js │ │ │ └── modal.js │ ├── directives │ │ └── markdown.js │ ├── helpers │ │ └── validator.js │ ├── index.js │ ├── plugins │ │ ├── alerts.ts │ │ ├── code.js │ │ ├── durations.ts │ │ ├── input.js │ │ ├── modals.js │ │ ├── strings.js │ │ ├── translation.js │ │ └── unproxy.js │ └── store │ │ ├── index.js │ │ └── modules │ │ ├── alert.js │ │ ├── context.js │ │ ├── expand.js │ │ ├── filters.js │ │ ├── ledger.js │ │ ├── modals.js │ │ ├── settings.js │ │ ├── status.js │ │ ├── tabs.js │ │ └── theme.js ├── styles │ ├── animations.scss │ ├── app.scss │ ├── blocks │ │ ├── about.scss │ │ ├── ansi.scss │ │ ├── breadcrumbs.scss │ │ ├── buttons.scss │ │ ├── code.scss │ │ ├── collapsible.scss │ │ ├── console.scss │ │ ├── contents.scss │ │ ├── cta.scss │ │ ├── diff.scss │ │ ├── draggable.scss │ │ ├── feedback.scss │ │ ├── filename.scss │ │ ├── forms.scss │ │ ├── framework.scss │ │ ├── icons.scss │ │ ├── labels.scss │ │ ├── license.scss │ │ ├── loading.scss │ │ ├── meta.scss │ │ ├── modals.scss │ │ ├── nugget.scss │ │ ├── object.scss │ │ ├── pre.scss │ │ ├── preferences.scss │ │ ├── project.scss │ │ ├── results.scss │ │ ├── scrollable.scss │ │ ├── selective.scss │ │ ├── settings.scss │ │ ├── sidebar.scss │ │ ├── status.scss │ │ ├── tables.scss │ │ ├── terms.scss │ │ ├── test-information.scss │ │ ├── test-result.scss │ │ ├── test.scss │ │ ├── titlebar.scss │ │ └── trace.scss │ ├── breakpoints.scss │ ├── definitions.scss │ ├── functions.scss │ ├── globals.scss │ ├── highlight │ │ ├── common.scss │ │ ├── dark.scss │ │ └── light.scss │ ├── images │ │ ├── error.png │ │ ├── error@2x.png │ │ ├── octocat-spinner-16px.gif │ │ ├── octocat-spinner-32-EAF2F5.gif │ │ ├── octocat-spinner-32.gif │ │ ├── success.png │ │ └── success@2x.png │ ├── mixins.scss │ ├── typography.scss │ ├── variables.scss │ └── vendor.scss └── types │ ├── shims.d.ts │ └── vuex-shim.d.ts ├── static ├── LICENSE ├── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 512x512.png │ └── gem.svg └── licenses.json ├── support ├── assets │ ├── dmg-bg.png │ ├── dmg-bg.tiff │ └── dmg-bg@2x.png ├── entitlements.mac.plist ├── release-notes.example.md └── release-notes.md ├── tests ├── cypress │ ├── config.ts │ ├── e2e │ │ ├── frameworks.js │ │ ├── projects.js │ │ ├── repositories.js │ │ └── theme.js │ └── support │ │ ├── assertions.js │ │ ├── index.js │ │ ├── ipc.js │ │ └── process.js ├── fixtures │ ├── framework │ │ ├── jest │ │ │ └── 26 │ │ │ │ ├── options.json │ │ │ │ └── suites.json │ │ ├── ledger.json │ │ ├── phpunit │ │ │ └── 8.0 │ │ │ │ ├── options.json │ │ │ │ ├── suites.json │ │ │ │ └── tests.json │ │ ├── project.json │ │ ├── repositories.json │ │ └── types.json │ └── process │ │ ├── 1.json │ │ ├── 10.json │ │ ├── 11.json │ │ ├── 12.json │ │ ├── 13.json │ │ ├── 14.json │ │ ├── 15.json │ │ ├── 16.json │ │ ├── 17.json │ │ ├── 18.json │ │ ├── 19.json │ │ ├── 2.json │ │ ├── 20.json │ │ ├── 21.json │ │ ├── 22.json │ │ ├── 23.json │ │ ├── 24.json │ │ ├── 25.json │ │ ├── 26.json │ │ ├── 27.json │ │ ├── 28.json │ │ ├── 29.json │ │ ├── 3.json │ │ ├── 4.json │ │ ├── 5.json │ │ ├── 6.json │ │ ├── 7.json │ │ ├── 8.json │ │ ├── 9.json │ │ └── decoded.json ├── lib │ ├── frameworks │ │ ├── __snapshots__ │ │ │ └── framework.spec.js.snap │ │ ├── emitter.spec.js │ │ ├── factory.spec.js │ │ ├── framework.spec.js │ │ ├── jest │ │ │ └── framework.spec.js │ │ └── status.spec.js │ ├── helpers │ │ ├── durations.spec.js │ │ ├── paths.spec.js │ │ └── strings.spec.js │ └── process │ │ ├── factory.spec.js │ │ ├── pool.spec.js │ │ ├── process.spec.js │ │ └── runners │ │ ├── npm.spec.js │ │ └── yarn.spec.js ├── mocks │ ├── electron.js │ └── setup.js ├── renderer │ ├── components │ │ ├── TestInformation.spec.js │ │ └── __snapshots__ │ │ │ └── TestInformation.spec.js.snap │ └── directives │ │ └── markdown.spec.js └── setup.js ├── tsconfig.json ├── workflow ├── app-info.js ├── build.js ├── clean-build.js ├── clean.js ├── cypress.js ├── decipher.js ├── icons.js ├── licenses.js ├── release-after.js ├── release-before.js ├── reporters │ ├── phpunit.js │ └── webpack.jest.config.js ├── run-dev.js ├── runners.js ├── webpack.base.config.js ├── webpack.main.config.js ├── webpack.preload.config.js └── webpack.renderer.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | 8 | [*.{js,ts,scss}] 9 | indent_size = 4 10 | 11 | [{package.json,.eslintrc.json,babel.config.js,*.yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/** 2 | /dist/** 3 | /static/** 4 | /src/lib/reporters/** 5 | /src/lib/process/debug/** 6 | /src/types/** 7 | /babel.config.js 8 | /workflow 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report" 3 | about: Report a problem encountered while using Lode 4 | 5 | --- 6 | 7 | ### Describe the bug 8 | 9 | A clear and concise description of what the bug is. 10 | 11 | ### Version & OS 12 | 13 | Open 'About Lode' menu to see your version. Also include what operating system you are using. 14 | 15 | ### Steps to reproduce the behavior 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | ### Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | ### Actual behavior 27 | 28 | A clear and concise description of what actually happened. 29 | 30 | ### Screenshots 31 | 32 | Add screenshots to help explain your problem, if applicable. 33 | 34 | ### Logs 35 | 36 | Attach your logs by opening the `Help` menu and selecting `Troubleshooting > Show Logs...`, if applicable. 37 | 38 | ### Additional context 39 | 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request" 3 | about: Request a feature that you think would improve Lode for everyone 4 | 5 | --- 6 | 7 | ### Describe the feature or problem you’d like to solve 8 | 9 | A clear and concise description of what the feature or problem is. 10 | 11 | ### Proposed solution 12 | 13 | How will it benefit Lode and its useΩrs? 14 | 15 | ### Additional context 16 | 17 | Add any other context like screenshots or mockups are helpful, if applicable. 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [develop, main] 6 | pull_request: 7 | branches: [develop] 8 | schedule: 9 | - cron: '38 7 * * 5' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | language: ['javascript'] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v3 25 | with: 26 | languages: ${{ matrix.language }} 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v3 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/* 3 | build/* 4 | node_modules/ 5 | npm-debug.log 6 | npm-debug.log.* 7 | thumbs.db 8 | !.gitkeep 9 | src/main/lib/process/debug/*.json 10 | src/lib/reporters/phpunit/bootstrap 11 | support/icons 12 | tests/coverage 13 | tests/cypress/videos 14 | tests/cypress/screenshots 15 | yarn-error.log 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.1 2 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard-scss", 4 | "stylelint-config-html/html", 5 | "stylelint-config-html/vue" 6 | ], 7 | "rules": { 8 | "comment-whitespace-inside": "always", 9 | "indentation": 4, 10 | "max-nesting-depth": [7, { "severity": "warning"}], 11 | "no-descending-specificity": null, 12 | "number-leading-zero": "never", 13 | "scss/comment-no-empty": null, 14 | "selector-class-pattern": null, 15 | "selector-max-compound-selectors": null, 16 | "selector-no-qualifying-type": null 17 | }, 18 | "ignoreFiles": [ 19 | "**/*.js", 20 | "**/*.ts", 21 | "**/*.json", 22 | "**/reporters/**", 23 | "src/styles/images/**" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Electron: Main", 7 | "type": "node", 8 | "request": "launch", 9 | "program": "${workspaceFolder}/src/main/index.dev.ts", 10 | "stopOnEntry": false, 11 | "args": [], 12 | "cwd": "${workspaceRoot}", 13 | "runtimeExecutable": "node", 14 | "runtimeArgs": [ 15 | "${workspaceRoot}/workflow/run-dev.js" 16 | ], 17 | "env": { 18 | "IS_DEV": "true" 19 | }, 20 | "sourceMaps": true 21 | }, 22 | { 23 | "name": "Electron: Renderer", 24 | "type": "chrome", 25 | "request": "attach", 26 | "port": 9223, 27 | "webRoot": "${workspaceFolder}", 28 | "timeout": 30000 29 | } 30 | ], 31 | "compounds": [ 32 | { 33 | "name": "Electron: All", 34 | "configurations": [ 35 | "Electron: Main", 36 | "Electron: Renderer" 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Tomas Buteler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "comments": false, 3 | "env": { 4 | "main": { 5 | "presets": [ 6 | ["@babel/preset-env", { 7 | "targets": { "node": 14 } 8 | }] 9 | ] 10 | }, 11 | "renderer": { 12 | "presets": [ 13 | ["@babel/preset-env", { 14 | "modules": false 15 | }] 16 | ] 17 | }, 18 | "reporters": { 19 | "presets": [ 20 | ["@babel/preset-env", { 21 | "modules": false 22 | }] 23 | ], 24 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 25 | }, 26 | "test": { 27 | "presets": [ 28 | ["@babel/preset-env", { 29 | "modules": false 30 | }] 31 | ], 32 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 33 | } 34 | }, 35 | "plugins": ["lodash", "@babel/plugin-transform-runtime"] 36 | } 37 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/dist/.gitkeep -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'globalSetup': './tests/setup.js', 3 | 'globals': { 4 | __DEV__: true, 5 | __SILENT__: true, 6 | __WIN32__: process.platform === 'win32', 7 | __DARWIN__: process.platform === 'darwin', 8 | __LINUX__: process.platform === 'linux', 9 | 'ts-jest': { 10 | 'babelConfig': true, 11 | 'tsconfig': 'tsconfig.json' 12 | } 13 | }, 14 | 'setupFiles': [ 15 | './tests/mocks/setup.js' 16 | ], 17 | 'moduleFileExtensions': [ 18 | 'js', 19 | 'ts', 20 | 'json', 21 | 'vue' 22 | ], 23 | 'moduleNameMapper': { 24 | '^@/(.*)$': '/src/renderer/$1', 25 | '^@main/(.*)$': '/src/main/$1', 26 | '^@lib/(.*)$': '/src/lib/$1' 27 | }, 28 | 'testPathIgnorePatterns': 29 | [ 30 | '/build/', 31 | '/dist/', 32 | '/node_modules/', 33 | '/src/' 34 | ], 35 | 'transform': { 36 | '.*\\.(vue)$': 'vue-jest', 37 | '^.+\\.(ts)$': 'ts-jest', 38 | '^.+\\.js$': 'babel-jest' 39 | }, 40 | 'clearMocks': true, 41 | 'collectCoverage': false, 42 | 'collectCoverageFrom': [ 43 | 'src/**', 44 | '!src/styles/**' 45 | ], 46 | 'coverageDirectory': 'tests/coverage', 47 | 'coverageReporters': ['json', 'html'] 48 | } 49 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | Lode 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/crash/reporter.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/electron' 2 | 3 | if (process.env.NODE_ENV !== 'development') { 4 | Sentry.init({ 5 | dsn: __CRASH_URL__ 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/frameworks/emitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { ApplicationWindow } from '@main/application-window' 3 | 4 | /** 5 | * A special event emitter that can also emit events 6 | * to the renderer process from a given window. 7 | */ 8 | export class ProjectEventEmitter extends EventEmitter { 9 | protected window: ApplicationWindow 10 | 11 | constructor (window: ApplicationWindow) { 12 | super() 13 | this.window = window 14 | } 15 | 16 | /** 17 | * Emit an event to the renderer process. 18 | */ 19 | protected emitToRenderer (event: string, ...args: any[]): void { 20 | if (this.window.canReceiveEvents()) { 21 | this.window.send(event, args) 22 | } 23 | } 24 | 25 | /** 26 | * Get the emitter's application window. 27 | */ 28 | public getApplicationWindow (): ApplicationWindow { 29 | return this.window 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/frameworks/factory.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationWindow } from '@main/application-window' 2 | import { FrameworkOptions, IFramework } from './framework' 3 | import { getFrameworkByType } from '@lib/frameworks' 4 | 5 | export class FrameworkFactory { 6 | /** 7 | * Make a new framework instance. 8 | * 9 | * @param window The application window which will own the framework 10 | * @param options The options to make the framework with 11 | */ 12 | public static make ( 13 | window: ApplicationWindow, 14 | options: FrameworkOptions 15 | ): IFramework { 16 | const Framework = getFrameworkByType(options.type) 17 | 18 | if (Framework) { 19 | // Create a new framework with hydrated options, in case defaults 20 | // ever change significantly from any persisted state. 21 | return new Framework(window, Framework.hydrate(options)) 22 | } 23 | 24 | throw new Error(`Unknown framework type "${options.type}"`) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/frameworks/index.ts: -------------------------------------------------------------------------------- 1 | import { find } from 'lodash' 2 | import { Jest } from './jest/framework' 3 | import { PHPUnit10 } from './phpunit-10/framework' 4 | import { PHPUnit } from './phpunit/framework' 5 | 6 | export { Jest } 7 | export { PHPUnit10 } 8 | export { PHPUnit } 9 | 10 | export const Frameworks = [Jest, PHPUnit10, PHPUnit] 11 | 12 | /** 13 | * Get a framework class by its type. 14 | * 15 | * @param type The slug representing the framework type. 16 | */ 17 | export function getFrameworkByType (type: string): typeof Jest | typeof PHPUnit10 | typeof PHPUnit | undefined { 18 | return find(Frameworks, framework => framework.getDefaults().type === type) 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/frameworks/phpunit-10/suite.ts: -------------------------------------------------------------------------------- 1 | import { clipboard } from 'electron' 2 | import { Suite } from '@lib/frameworks/suite' 3 | 4 | export class PHPUnit10Suite extends Suite { 5 | /** 6 | * Get this suite's class name. 7 | */ 8 | public getClassName (): string { 9 | return this.getMeta('class', '').replace(/\\/g, '\\\\') 10 | } 11 | 12 | /** 13 | * Append items to a PHPUnit suite's context menu. 14 | */ 15 | public contextMenu (): Array { 16 | return [{ 17 | label: __DARWIN__ 18 | ? 'Copy Class Name' 19 | : 'Copy class name', 20 | click: () => { 21 | clipboard.writeText(this.getClassName() || '') 22 | }, 23 | enabled: !!this.getClassName(), 24 | before: ['copy-local'] 25 | }] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/frameworks/phpunit/suite.ts: -------------------------------------------------------------------------------- 1 | import { clipboard } from 'electron' 2 | import { Suite } from '@lib/frameworks/suite' 3 | 4 | export class PHPUnitSuite extends Suite { 5 | /** 6 | * Get this suite's class name. 7 | */ 8 | public getClassName (): string { 9 | return this.getMeta('class', '').replace(/\\/g, '\\\\') 10 | } 11 | 12 | /** 13 | * Append items to a PHPUnit suite's context menu. 14 | */ 15 | public contextMenu (): Array { 16 | return [{ 17 | label: __DARWIN__ 18 | ? 'Copy Class Name' 19 | : 'Copy class name', 20 | click: () => { 21 | clipboard.writeText(this.getClassName() || '') 22 | }, 23 | enabled: !!this.getClassName(), 24 | before: ['copy-local'] 25 | }] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/frameworks/progress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A ledger of run progress. 3 | */ 4 | export type ProgressLedger = { 5 | run: number, 6 | total: number 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/frameworks/sort.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash' 2 | 3 | /** 4 | * The sort direction possibilities. 5 | */ 6 | type SortDirection = 'asc' | 'desc' 7 | 8 | /** 9 | * A list of possible framework sort options. 10 | */ 11 | export type FrameworkSort = 'framework' | 'name' 12 | 13 | export const sortOptions: { [key in FrameworkSort]: string } = { 14 | framework: 'Running order', 15 | name: 'Name' 16 | } 17 | 18 | export const sortDirections: { [key in FrameworkSort]: SortDirection } = { 19 | framework: 'asc', 20 | name: 'asc' 21 | } 22 | 23 | /** 24 | * Map a sort option to its display name. 25 | * 26 | * @param sort The sort option to map to a display name. 27 | */ 28 | export function sortDisplayName (sort: FrameworkSort): string { 29 | return get(sortOptions, sort, 'Unknown sort') 30 | } 31 | 32 | /** 33 | * Return the reverse of a given direction. 34 | * 35 | * @param direction The direction to return the reverse of. 36 | */ 37 | export function reverseDirection (direction: SortDirection): SortDirection { 38 | return direction === 'asc' ? 'desc' : 'asc' 39 | } 40 | 41 | /** 42 | * Map a sort option to its default direction. 43 | * 44 | * @param sort The sort option to map to a direction. 45 | * @param reverse Whether to reverse the default direction. 46 | */ 47 | export function sortDirection (sort: FrameworkSort, reverse: boolean): SortDirection { 48 | const direction = get(sortDirections, sort, 'asc') 49 | return reverse ? reverseDirection(direction) : direction 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/helpers/paths.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path' 2 | 3 | export function getResourceDirectory (): string { 4 | return __DEV__ 5 | ? Path.join(process.cwd(), 'dist') 6 | : Path.join(process.resourcesPath, 'app.asar.unpacked', 'dist') 7 | } 8 | 9 | /** 10 | * Modifies a given path to return its location as an unpacked asset 11 | * (i.e. not part of the app's package, so publicly available. This is 12 | * useful for reporters, which have to be read or injected by the 13 | * filesystem during test framework runs.) 14 | * 15 | * @param loc The path to process. 16 | */ 17 | export function unpacked (loc: string): string { 18 | const s = Path.sep 19 | return loc.replace(/[\\\/]?\bapp\.asar\b[\\\/]?/, `${s}app.asar.unpacked${s}`) 20 | } 21 | 22 | /** 23 | * Rebuild a POSIX path into a platform-specific one, by replacing 24 | * its path separators. This is because we hardcode all our paths 25 | * as POSIX for readability. 26 | * 27 | * @param loc The path to process. 28 | */ 29 | export function loc (loc: string): string { 30 | return loc.split('/').join(Path.sep) 31 | } 32 | 33 | /** 34 | * Force a path (potentially Windows-specific) into a POSIX one. Not 35 | * necessarily bullet-proof, but could be useful for some transformations. 36 | * 37 | * @param loc The path to process. 38 | */ 39 | export function posix (loc: string): string { 40 | return loc.split(Path.sep).join('/') 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/logger/format.ts: -------------------------------------------------------------------------------- 1 | export function formatError (error: Error, title?: string): string { 2 | return title 3 | ? `${title}\n${error.name}: ${error.message}` 4 | : `${error.name}: ${error.message}` 5 | } 6 | 7 | export function formatLogMessage (message: string | object, error?: Error): string { 8 | if (typeof message === 'object') { 9 | message = JSON.stringify(message) 10 | } 11 | return error ? formatError(error, message) : message 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/logger/levels.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = 'error' | 'warn' | 'info' | 'debug' 2 | -------------------------------------------------------------------------------- /src/lib/logger/main.ts: -------------------------------------------------------------------------------- 1 | import { log } from './index' 2 | import { formatLogMessage } from './format' 3 | 4 | const g = global as any 5 | 6 | g.log = { 7 | error (message: string | object, error?: Error) { 8 | log('error', '[main]: ' + formatLogMessage(message, error)) 9 | }, 10 | warn (message: string | object, error?: Error) { 11 | log('warn', '[main]: ' + formatLogMessage(message, error)) 12 | }, 13 | info (message: string | object, error?: Error) { 14 | log('info', '[main]: ' + formatLogMessage(message, error)) 15 | }, 16 | debug (message: string | object, error?: Error) { 17 | log('debug', '[main]: ' + formatLogMessage(message, error)) 18 | } 19 | } as ILogger 20 | -------------------------------------------------------------------------------- /src/lib/logger/preload.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | import { LogLevel } from './levels' 3 | import { formatLogMessage } from './format' 4 | 5 | const g = global as any 6 | 7 | function log (level: LogLevel, message: string | object, error?: Error) { 8 | ipcRenderer.send('log', level, '[preload]: ' + formatLogMessage(message, error)) 9 | } 10 | 11 | g.log = { 12 | error (message: string | object, error?: Error) { 13 | log('error', message, error) 14 | console.error(formatLogMessage(message, error)) 15 | }, 16 | warn (message: string | object, error?: Error) { 17 | log('warn', message, error) 18 | console.warn(formatLogMessage(message, error)) 19 | }, 20 | info (message: string | object, error?: Error) { 21 | log('info', message, error) 22 | console.info(formatLogMessage(message, error)) 23 | }, 24 | debug (message: string | object, error?: Error) { 25 | log('debug', message, error) 26 | console.debug(formatLogMessage(message, error)) 27 | } 28 | } as ILogger 29 | -------------------------------------------------------------------------------- /src/lib/logger/renderer.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from './levels' 2 | import { formatLogMessage } from './format' 3 | 4 | const g = global as any 5 | 6 | function log (level: LogLevel, message: string | object, error?: Error) { 7 | Lode.ipc.send('log', level, '[renderer]: ' + formatLogMessage(message, error)) 8 | } 9 | 10 | g.log = { 11 | error (message: string | object, error?: Error) { 12 | log('error', message, error) 13 | console.error(formatLogMessage(message, error)) 14 | }, 15 | warn (message: string | object, error?: Error) { 16 | log('warn', message, error) 17 | console.warn(formatLogMessage(message, error)) 18 | }, 19 | info (message: string | object, error?: Error) { 20 | log('info', message, error) 21 | console.info(formatLogMessage(message, error)) 22 | }, 23 | debug (message: string | object, error?: Error) { 24 | log('debug', message, error) 25 | console.debug(formatLogMessage(message, error)) 26 | } 27 | } as ILogger 28 | -------------------------------------------------------------------------------- /src/lib/process/debug/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/lib/process/debug/.gitkeep -------------------------------------------------------------------------------- /src/lib/process/errors.ts: -------------------------------------------------------------------------------- 1 | import { IProcess } from '@lib/process/process' 2 | 3 | /** 4 | * An error with a code number property. 5 | */ 6 | export interface ErrorWithCode extends Error { 7 | code?: string | number | null 8 | } 9 | 10 | /** 11 | * An error thrown by our standard process. 12 | */ 13 | export class ProcessError extends Error implements ErrorWithCode { 14 | process?: string 15 | code?: string | number | null | undefined 16 | 17 | /** 18 | * Set the process that originated the error. 19 | * 20 | * @param process The process to set to. 21 | */ 22 | public setProcess (process: IProcess): this { 23 | this.process = process.toString() 24 | return this 25 | } 26 | 27 | /** 28 | * Get the process that originated the error as a plain 29 | * object, parsed from its string representation. 30 | * 31 | * @param process The process to set to. 32 | */ 33 | public getProcess (): object | undefined { 34 | return this.process ? JSON.parse(this.process) : undefined 35 | } 36 | 37 | /** 38 | * The error code. 39 | * 40 | * @param code The error code we're setting. 41 | */ 42 | public setCode (code?: string | number | null): this { 43 | this.code = code 44 | return this 45 | } 46 | 47 | /** 48 | * Transform this error to a string. 49 | */ 50 | public toString (): string { 51 | return this.message 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/process/factory.ts: -------------------------------------------------------------------------------- 1 | import { find } from 'lodash' 2 | import { ProcessId, ProcessOptions, IProcess, DefaultProcess } from '@lib/process/process' 3 | import { Runners } from '@lib/process/runners' 4 | import pool from '@lib/process/pool' 5 | 6 | export class ProcessFactory { 7 | /** 8 | * Make a new process according to the given options. 9 | * 10 | * @param options The options for the process we're making. 11 | * @param poolId An optional id with which the newly made process will be added to the pool. 12 | */ 13 | public static make (options: ProcessOptions, poolId?: ProcessId): IProcess { 14 | let spawned: IProcess | null = null 15 | 16 | if (options.forceRunner) { 17 | // If a runner has been pre-determined, try to find it within list of 18 | // existing runners and create a process with it, if possible. 19 | const Runner = find(Runners, runner => runner.type === options.forceRunner) 20 | if (Runner) { 21 | spawned = new Runner(options) 22 | } 23 | } else { 24 | // If no runner was specificed, we'll try to determine which runner to 25 | // use by feeding each of them the command. 26 | for (let i = 0; i < Runners.length; i++) { 27 | if (Runners[i].owns(options.command)) { 28 | spawned = new Runners[i](options) 29 | } 30 | } 31 | } 32 | 33 | spawned = spawned || new DefaultProcess(options) 34 | 35 | pool.add(spawned, poolId) 36 | 37 | return spawned 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/process/pool.ts: -------------------------------------------------------------------------------- 1 | import { ProcessId, IProcess } from '@lib/process/process' 2 | 3 | /** 4 | * A pool of running processes. 5 | */ 6 | class ProcessPool { 7 | public readonly processes: { [type in ProcessId]: IProcess } = {} 8 | 9 | /** 10 | * Add a new process to the pool. 11 | * 12 | * @param process The process being pooled. 13 | * @param id An optional id with which process will be added to the pool. 14 | */ 15 | public add (process: IProcess, id?: ProcessId): void { 16 | // If id was given, we'll use it, otherwise we'll 17 | // try to get it from the process itself. 18 | if (!id) { 19 | id = process.getId() 20 | 21 | // If we can't acquire the id, don't pool it. 22 | if (!id) { 23 | return 24 | } 25 | } 26 | 27 | this.processes[id!] = process 28 | 29 | // Once the process closes, remove it from the pool. 30 | process.on('close', () => { 31 | this.remove(id!) 32 | }) 33 | } 34 | 35 | /** 36 | * Find a pooled and running process using its id. 37 | * 38 | * @param id The id of the process trying to be found. 39 | */ 40 | public findProcess (id: ProcessId): IProcess | undefined { 41 | return this.processes[id] 42 | } 43 | 44 | /** 45 | * Remove a process from the pool by its id. 46 | */ 47 | public remove (id: ProcessId): void { 48 | if (typeof this.processes[id] !== 'undefined') { 49 | delete this.processes[id] 50 | } 51 | } 52 | 53 | /** 54 | * Clear the process pool. 55 | */ 56 | public clear (): void { 57 | Object.keys(this.processes).forEach(id => { 58 | this.remove(id) 59 | }) 60 | } 61 | } 62 | 63 | const pool = new ProcessPool() 64 | 65 | export default pool 66 | -------------------------------------------------------------------------------- /src/lib/process/queue.ts: -------------------------------------------------------------------------------- 1 | import Bottleneck from 'bottleneck' 2 | import { state } from '@lib/state' 3 | 4 | export interface IQueue { 5 | add (job: any): void 6 | stop (): void 7 | } 8 | 9 | class Queue implements IQueue { 10 | protected limiter: Bottleneck 11 | 12 | constructor () { 13 | this.limiter = new Bottleneck({ 14 | maxConcurrent: state.get('concurrency') 15 | }) 16 | 17 | // Listen for config changes on concurrency to update the limiter 18 | state.on('set:concurrency', (value: number) => { 19 | this.limiter.updateSettings({ 20 | maxConcurrent: value 21 | }) 22 | }) 23 | } 24 | 25 | public add (job: any): void { 26 | const wrapped = this.limiter.wrap(job) 27 | wrapped() 28 | } 29 | 30 | public stop (): void { 31 | this.limiter.stop() 32 | } 33 | } 34 | 35 | export const queue = new Queue() 36 | -------------------------------------------------------------------------------- /src/lib/process/runners/index.ts: -------------------------------------------------------------------------------- 1 | import { YarnProcess } from './yarn' 2 | import { NpmProcess } from './npm' 3 | 4 | export { YarnProcess } 5 | export { NpmProcess } 6 | 7 | export const Runners = [YarnProcess, NpmProcess] 8 | -------------------------------------------------------------------------------- /src/lib/process/runners/npm.ts: -------------------------------------------------------------------------------- 1 | import { compact, concat, drop } from 'lodash' 2 | import { IProcessEnvironment, IProcess, DefaultProcess } from '@lib/process/process' 3 | 4 | export class NpmProcess extends DefaultProcess implements IProcess { 5 | static readonly type: string = 'npm' 6 | 7 | /** 8 | * Whether this process owns a given command. 9 | * 10 | * @param command The command we're checking to match an NPM runner. 11 | */ 12 | public static owns (command: string): boolean { 13 | return command.toLowerCase().search(/\bnpm(\.cmd)?(?!\.) run\b/) > -1 14 | } 15 | 16 | /** 17 | * Return the array of arguments with which to spawn the child process. 18 | * NPM requires arguments to be preceded by '--', so this is where we 19 | * will enforce that syntax. We also need to patch the binary path for 20 | * Windows environments. 21 | */ 22 | protected spawnArguments (args: Array): Array { 23 | if (!args.length) { 24 | return args 25 | } 26 | 27 | let binary = args.shift() 28 | if (this.platform === 'win32' && binary === 'npm') { 29 | binary = 'npm.cmd' 30 | } 31 | 32 | // Drop the "run", which we don't need to manipulate. 33 | args = drop(args, 1) 34 | 35 | // First argument after npm run is our script, so shift it. 36 | const script = args.shift() 37 | 38 | // Recreate the arguments by prefixing remaining ones with '--'. 39 | return compact(concat(binary!, 'run', (script || ''), args.length ? '--' : '', args)) 40 | } 41 | 42 | /** 43 | * Return the env object with which to spawn the child process. 44 | */ 45 | protected spawnEnv (env: IProcessEnvironment): IProcessEnvironment { 46 | return { 47 | ...env, 48 | ...{ 49 | // Disable npm update notifier 50 | NO_UPDATE_NOTIFIER: 1 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/process/runners/yarn.ts: -------------------------------------------------------------------------------- 1 | import { concat } from 'lodash' 2 | import { IProcess, DefaultProcess } from '@lib/process/process' 3 | 4 | export class YarnProcess extends DefaultProcess implements IProcess { 5 | static readonly type: string = 'yarn' 6 | 7 | /** 8 | * Whether this process owns a given command. 9 | * 10 | * @param command The command we're checking to match a Yarn runner. 11 | */ 12 | public static owns (command: string): boolean { 13 | return command.toLowerCase().search(/\byarn(\.js|\.cmd)?(?!\.)\b/) > -1 14 | } 15 | 16 | /** 17 | * Return the array of arguments with which to spawn the child process. 18 | * We need to patch the Yarn binary path for Windows environments. 19 | */ 20 | protected spawnArguments (args: Array): Array { 21 | if (!args.length) { 22 | return args 23 | } 24 | 25 | let binary = args.shift() 26 | if (this.platform === 'win32' && binary === 'yarn') { 27 | binary = 'yarn.cmd' 28 | } 29 | 30 | // Recreate the arguments by prefixing remaining ones with '--'. 31 | return concat(binary!, args) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/process/search.ts: -------------------------------------------------------------------------------- 1 | export class BufferedSearch { 2 | terms: { 3 | [index: string]: { 4 | buffer: string, 5 | matched: boolean 6 | } 7 | } = {} 8 | 9 | term (term: string, string: string): boolean { 10 | // Have we started looking for this yet? If not, prepare buffer. 11 | if (!this.terms[term]) { 12 | this.terms[term] = { 13 | buffer: '', 14 | matched: false 15 | } 16 | } else if (this.terms[term].matched) { 17 | return true 18 | } 19 | 20 | // Strip string of whitespace and append to buffer 21 | this.terms[term].buffer += string.replace(/\s+/g, '') 22 | 23 | let search = '' 24 | const characters = term.split('') 25 | for (let i = 0; i < characters.length; i++) { 26 | // Create search term substring 27 | search += characters[i] 28 | 29 | // If length of term substring exceeds that of the buffer, return. 30 | // Buffer will be kept and next call might yield a result. 31 | if (search.length > this.terms[term].buffer.length) { 32 | return false 33 | } 34 | 35 | // Search for term substring inside buffer 36 | const index = this.terms[term].buffer.indexOf(search) 37 | 38 | if (index === -1) { 39 | // If it doesn't match, return false and discard buffer 40 | this.terms[term].buffer = '' 41 | this.terms[term].matched = false 42 | return false 43 | } else if (index > 0) { 44 | // If it matches beyond start, discard content preceding match 45 | this.terms[term].buffer = this.terms[term].buffer.substring(index) 46 | } 47 | } 48 | 49 | // If full length of term was iterated through and matches occurred 50 | // consistently in the buffer, return true. 51 | return true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/process/shell.ts: -------------------------------------------------------------------------------- 1 | import * as shellEnv from 'shell-env' 2 | import * as defaultShell from 'default-shell' 3 | 4 | /** 5 | * The names of any env vars that we shouldn't copy from the shell environment. 6 | */ 7 | const BlacklistedNames = new Set(['LOCAL_GIT_DIRECTORY']) 8 | 9 | /** 10 | * Merge environment variables from shell into the current process, if needed. 11 | */ 12 | export function mergeEnvFromShell (): void { 13 | if (!needsEnv(process)) { 14 | return 15 | } 16 | 17 | const env = shellEnv.sync(getUserShell()) 18 | for (const key in env) { 19 | if (BlacklistedNames.has(key)) { 20 | continue 21 | } 22 | 23 | process.env[key] = env[key] 24 | } 25 | } 26 | 27 | /** 28 | * Whether the current process needs to have shell environment 29 | * variables merged in. 30 | * 31 | * @param process The process to inspect. 32 | */ 33 | function needsEnv (process: NodeJS.Process): boolean { 34 | return __DARWIN__ && !process.env.PWD 35 | } 36 | 37 | /** 38 | * Get the user-defined shell, if any. 39 | */ 40 | function getUserShell () { 41 | if (process.env.SHELL) { 42 | return process.env.SHELL 43 | } 44 | 45 | return defaultShell 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/bootstrap.php: -------------------------------------------------------------------------------- 1 | registerSubscriber(new \LodeApp\PHPUnit\ExtensionHook()); 38 | } 39 | 40 | // Remember actual bootstrap location in case PHPUnit boots another process 41 | // for tests running in isolation. 42 | file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'bootstrap', $bootstrap); 43 | 44 | // Now that Lode is set up, require the original user-defined bootstrap. 45 | require_once preg_replace($pattern, '', $bootstrap); 46 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/ExtensionHook.php: -------------------------------------------------------------------------------- 1 | configuration(), 20 | $facade, 21 | ); 22 | 23 | $extensionBootstrapper->bootstrap(Extension::class, []); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Lode.php: -------------------------------------------------------------------------------- 1 | share(new Console); 29 | } 30 | 31 | /** 32 | * Resolve the given type from the container. Will instatiate 33 | * and set the container if it's not yet available. 34 | * 35 | * @param string $abstract 36 | * @return mixed 37 | */ 38 | public static function make($abstract) 39 | { 40 | if (is_null(static::$instance)) { 41 | static::$instance = new static; 42 | } 43 | 44 | return call_user_func_array([static::$instance, 'resolve'], [$abstract]); 45 | } 46 | 47 | /** 48 | * Normalize a class name. 49 | */ 50 | private function normalizeName($className): string 51 | { 52 | return ltrim(strtolower($className), '\\'); 53 | } 54 | 55 | /** 56 | * Share an instance through the container. 57 | */ 58 | private function share($obj): void 59 | { 60 | $normalizedName = $this->normalizeName(get_class($obj)); 61 | $this->shares[$normalizedName] = $obj; 62 | } 63 | 64 | /** 65 | * Return th shared class from the container. 66 | */ 67 | private function resolve($abstract): mixed 68 | { 69 | $normalizedClass = $this->normalizeName($abstract); 70 | if (isset($this->shares[$normalizedClass])) { 71 | return $this->shares[$normalizedClass]; 72 | } 73 | 74 | return null; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Status.php: -------------------------------------------------------------------------------- 1 | self::EMPTY, 24 | Test\DeprecationTriggered::class => self::WARNING, 25 | Test\Errored::class => self::FAILED, 26 | Test\ErrorTriggered::class => self::WARNING, 27 | Test\Failed::class => self::FAILED, 28 | Test\MarkedIncomplete::class => self::INCOMPLETE, 29 | Test\NoticeTriggered::class => self::WARNING, 30 | Test\Passed::class => self::PASSED, 31 | Test\PhpDeprecationTriggered::class => self::WARNING, 32 | Test\PhpNoticeTriggered::class => self::WARNING, 33 | Test\PhpunitDeprecationTriggered::class => self::WARNING, 34 | Test\PhpunitErrorTriggered::class => self::WARNING, 35 | Test\PhpunitWarningTriggered::class => self::WARNING, 36 | Test\PhpWarningTriggered::class => self::WARNING, 37 | Test\Skipped::class => self::SKIPPED, 38 | Test\WarningTriggered::class => self::WARNING, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/ApplicationFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->printResult(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/Subscriber.php: -------------------------------------------------------------------------------- 1 | reporter; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestConsideredRiskySubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestDeprecationTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestErrorTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestErroredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestFailedSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestMarkedIncompleteSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestNoticeTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestPassedSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestPhpDeprecationTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestPhpNoticeTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestPhpWarningTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestPhpunitDeprecationTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestPhpunitErrorTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestPhpunitWarningTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestSkippedSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestSuiteStartedSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->startTestSuite($event->testSuite())) { 15 | exit; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Subscriber/TestWarningTriggeredSubscriber.php: -------------------------------------------------------------------------------- 1 | reporter()->handleEvent($event); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit-10/src/Util.php: -------------------------------------------------------------------------------- 1 | offsetExists($key); 21 | } 22 | 23 | return array_key_exists($key, $array); 24 | } 25 | 26 | /** 27 | * Safely get the value from an array using its key, 28 | * with an optional fallback 29 | * 30 | * @param \ArrayAccess|array $array 31 | * @param string $key 32 | * @param mixed $default 33 | * @return mixed 34 | */ 35 | public static function get($array, $key, $default = null) 36 | { 37 | if (is_null($key)) { 38 | return $array; 39 | } 40 | 41 | if (static::exists($array, $key)) { 42 | return $array[$key]; 43 | } 44 | 45 | return $default; 46 | } 47 | 48 | /** 49 | * Remove falsey values from an (associative) array. 50 | */ 51 | public static function compact(array $array): array 52 | { 53 | foreach ($array as $key => $value) { 54 | if (!$value) { 55 | unset($array[$key]); 56 | } 57 | } 58 | return $array; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit/src/70-80/Printer.php: -------------------------------------------------------------------------------- 1 | share(new Console); 27 | } 28 | 29 | /** 30 | * Resolve the given type from the container. Will instatiate 31 | * and set the container if it's not yet available. 32 | * 33 | * @param string $abstract 34 | * @return mixed 35 | */ 36 | public static function make($abstract) 37 | { 38 | if (is_null(static::$instance)) { 39 | static::$instance = new static; 40 | } 41 | 42 | return call_user_func_array([static::$instance, 'resolve'], [$abstract]); 43 | } 44 | 45 | /** 46 | * Normalize a class name. 47 | * 48 | * @param string $abstract 49 | * @return mixed 50 | */ 51 | protected function normalizeName($className) 52 | { 53 | return ltrim(strtolower($className), '\\'); 54 | } 55 | 56 | /** 57 | * Share an instance through the container. 58 | * 59 | * @param string $abstract 60 | * @return mixed 61 | */ 62 | protected function share($obj) 63 | { 64 | $normalizedName = $this->normalizeName(get_class($obj)); 65 | $this->shares[$normalizedName] = $obj; 66 | } 67 | 68 | /** 69 | * Return th shared class from the container. 70 | * 71 | * @param string $abstract 72 | * @return mixed 73 | */ 74 | protected function resolve($abstract) 75 | { 76 | $normalizedClass = $this->normalizeName($abstract); 77 | if (isset($this->shares[$normalizedClass])) { 78 | return $this->shares[$normalizedClass]; 79 | } 80 | 81 | return null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/reporters/phpunit/src/Printer.php: -------------------------------------------------------------------------------- 1 | offsetExists($key); 19 | } 20 | 21 | return array_key_exists($key, $array); 22 | } 23 | 24 | /** 25 | * Safely get the value from an array using its key, 26 | * with an optional fallback 27 | * 28 | * @param \ArrayAccess|array $array 29 | * @param string $key 30 | * @param mixed $default 31 | * @return mixed 32 | */ 33 | public static function get($array, $key, $default = null) 34 | { 35 | if (is_null($key)) { 36 | return $array; 37 | } 38 | 39 | if (static::exists($array, $key)) { 40 | return $array[$key]; 41 | } 42 | 43 | return $default; 44 | } 45 | 46 | /** 47 | * Remove falsey values from an (associative) array. 48 | * 49 | * @param array $array 50 | */ 51 | public static function compact(array $array): array 52 | { 53 | foreach ($array as $key => $value) { 54 | if (!$value) { 55 | unset($array[$key]); 56 | } 57 | } 58 | return $array; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/state/project.ts: -------------------------------------------------------------------------------- 1 | import ElectronStore from 'electron-store' 2 | import { ProjectOptions } from '@lib/frameworks/project' 3 | 4 | export class Project { 5 | protected store: any 6 | 7 | constructor (id: string) { 8 | log.info('Initializing project store: ' + id) 9 | this.store = new ElectronStore({ 10 | encryptionKey: 'v1', 11 | name: 'project', 12 | defaults: { 13 | options: { 14 | id 15 | } 16 | }, 17 | fileExtension: 'db', 18 | cwd: `Projects/${id}` 19 | }) 20 | } 21 | 22 | public getPath (): string { 23 | return this.store.path.replace(/\/project\.db$/i, '') 24 | } 25 | 26 | public get (key?: string, fallback?: any): any { 27 | if (!key) { 28 | return this.store.store 29 | } 30 | 31 | return this.store.get(key, fallback) 32 | } 33 | 34 | public set (key: string, value?: any): void { 35 | this.store.set(key, value) 36 | } 37 | 38 | public save (options: ProjectOptions): void { 39 | this.store.set('options', { 40 | ...this.store.get('options'), 41 | ...options 42 | }) 43 | } 44 | 45 | public toJSON (): ProjectOptions { 46 | return this.store.get('options') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/themes/index.ts: -------------------------------------------------------------------------------- 1 | import { nativeTheme } from 'electron' 2 | import { 3 | isMacOSMojaveOrLater, 4 | isWindows10And1809Preview17666OrLater 5 | } from '@lib/helpers/os' 6 | 7 | /** 8 | * The list of available theme names. 9 | */ 10 | export type ThemeName = 'light' | 'dark' | 'system' 11 | 12 | /** 13 | * Whether or not the current OS supports System Theme Changes 14 | */ 15 | export function supportsSystemThemeChanges (): boolean { 16 | if (__DARWIN__) { 17 | return isMacOSMojaveOrLater() 18 | } else if (__WIN32__) { 19 | // Its technically possible this would still work on prior versions of Windows 10 but 1809 20 | // was released October 2nd, 2018 and the feature can just be "attained" by upgrading 21 | // See https://github.com/desktop/desktop/issues/9015 for more 22 | return isWindows10And1809Preview17666OrLater() 23 | } 24 | 25 | return false 26 | } 27 | 28 | export function initializeTheme (theme: ThemeName): void { 29 | if (theme !== 'system') { 30 | nativeTheme.themeSource = theme 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/index.dev.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | 3 | app.whenReady().then(() => { 4 | // @TODO: install Vue Devtools when they are compatible again with Electron 10 5 | // const installExtension = require('electron-devtools-installer') 6 | // installExtension(installExtension.VUEJS_DEVTOOLS) 7 | // .then(() => {}) 8 | // .catch((err: Error) => { 9 | // console.log('Unable to install `vue-devtools`: \n', err) 10 | // }) 11 | }) 12 | 13 | // Require `main` process to boot app 14 | require('./index') 15 | -------------------------------------------------------------------------------- /src/main/menu/file-menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from '@main/menu' 2 | import { File } from '@main/file' 3 | import { clipboard } from 'electron' 4 | 5 | export class FileMenu extends Menu { 6 | constructor (filePath: string, webContents: Electron.WebContents) { 7 | super(webContents) 8 | 9 | this 10 | .add({ 11 | id: 'reveal', 12 | label: __DARWIN__ 13 | ? 'Reveal in Finder' 14 | : __WIN32__ 15 | ? 'Show in Explorer' 16 | : 'Show in your File Manager', 17 | click: () => { 18 | File.reveal(filePath) 19 | }, 20 | enabled: File.exists(filePath) 21 | }) 22 | .add({ 23 | id: 'copy', 24 | label: __DARWIN__ 25 | ? 'Copy File Path' 26 | : 'Copy file path', 27 | click: () => { 28 | clipboard.writeText(filePath) 29 | }, 30 | enabled: File.exists(filePath) 31 | }) 32 | .add({ 33 | id: 'open', 34 | label: __DARWIN__ 35 | ? 'Open with Default Program' 36 | : 'Open with default program', 37 | click: () => { 38 | File.open(filePath) 39 | }, 40 | enabled: File.isSafe(filePath) && File.exists(filePath) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application-menu' 2 | export * from './menu' 3 | export * from './project-menu' 4 | export * from './repository-menu' 5 | export * from './framework-menu' 6 | export * from './suite-menu' 7 | export * from './test-menu' 8 | export * from './file-menu' 9 | -------------------------------------------------------------------------------- /src/main/menu/test-menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from '@main/menu' 2 | import { clipboard } from 'electron' 3 | import { ISuite } from '@lib/frameworks/suite' 4 | import { ITest } from '@lib/frameworks/test' 5 | 6 | export class TestMenu extends Menu { 7 | constructor (suite: ISuite, test: ITest, webContents: Electron.WebContents) { 8 | super(webContents) 9 | 10 | const originalName = test.getName() !== test.getDisplayName() ? test.getName() : '' 11 | this 12 | .add({ 13 | label: __DARWIN__ 14 | ? 'Copy Test Name' 15 | : 'Copy test name', 16 | click: () => { 17 | clipboard.writeText(test.getDisplayName() || test.getName()) 18 | } 19 | }) 20 | .addIf(!!originalName, { 21 | label: __DARWIN__ 22 | ? 'Copy Original Test Name' 23 | : 'Copy original test name', 24 | click: () => { 25 | clipboard.writeText(originalName) 26 | } 27 | }) 28 | .separator() 29 | .add({ 30 | label: __DARWIN__ 31 | ? 'Open Suite with Default Program' 32 | : 'Open suite with default program', 33 | click: () => { 34 | suite.open() 35 | }, 36 | enabled: suite.canBeOpened() 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import '@lib/logger/preload' 2 | 3 | import { contextBridge } from 'electron' 4 | import { Lode } from './lode' 5 | 6 | contextBridge.exposeInMainWorld('Lode', Lode) 7 | -------------------------------------------------------------------------------- /src/preload/lode.ts: -------------------------------------------------------------------------------- 1 | import { Ipc } from './ipc' 2 | 3 | class Preload { 4 | public readonly ipc: Ipc 5 | public readonly copyToClipboard: (string: string) => void 6 | public readonly openExternal: (link: string) => void 7 | 8 | constructor () { 9 | this.ipc = new Ipc() 10 | 11 | this.copyToClipboard = (string: string): void => { 12 | this.ipc.send('copy-to-clipboard', string) 13 | } 14 | 15 | this.openExternal = (link: string): void => { 16 | this.ipc.send('open-external-link', link) 17 | } 18 | } 19 | } 20 | 21 | export const Lode = new Preload() 22 | -------------------------------------------------------------------------------- /src/renderer/components/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | -------------------------------------------------------------------------------- /src/renderer/components/Collapsible.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 50 | -------------------------------------------------------------------------------- /src/renderer/components/Console.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 54 | -------------------------------------------------------------------------------- /src/renderer/components/Draggable.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/renderer/components/Duration.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/renderer/components/Filename.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 59 | -------------------------------------------------------------------------------- /src/renderer/components/Icon.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/renderer/components/Indicator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | -------------------------------------------------------------------------------- /src/renderer/components/KeyValue.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | -------------------------------------------------------------------------------- /src/renderer/components/MetaTable.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/renderer/components/ModalController.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /src/renderer/components/Pane.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/renderer/components/Parameters.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/renderer/components/ProjectLoader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/renderer/components/Snippet.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 51 | -------------------------------------------------------------------------------- /src/renderer/components/Split.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /src/renderer/components/mixins/HasFile.js: -------------------------------------------------------------------------------- 1 | import * as Path from 'path' 2 | import { mapGetters } from 'vuex' 3 | import Filename from '@/components/Filename.vue' 4 | 5 | export default { 6 | components: { 7 | Filename 8 | }, 9 | data () { 10 | return { 11 | activeContextMenu: null 12 | } 13 | }, 14 | computed: { 15 | ...mapGetters({ 16 | rootPath: 'context/rootPath', 17 | repositoryPath: 'context/repositoryPath' 18 | }) 19 | }, 20 | methods: { 21 | relativePath (path) { 22 | return path 23 | 24 | // @TODO: figure out why path doesn't work (i.e. is `undefined`) 25 | // if (!this.rootPath || !path.startsWith('/')) { 26 | // return path 27 | // } 28 | 29 | // return Path.relative(this.rootPath, path) 30 | }, 31 | absoluteLocalPath (file) { 32 | return Path.join(this.repositoryPath, this.relativePath(file)) 33 | }, 34 | onContextMenu (file, index) { 35 | this.activeContextMenu = index 36 | Lode.ipc.invoke('file-context-menu', this.absoluteLocalPath(file)).finally(() => { 37 | this.activeContextMenu = null 38 | }) 39 | }, 40 | hasContextMenu (index) { 41 | return this.activeContextMenu === index 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/components/mixins/HasFrameworkMenu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data () { 3 | return { 4 | menuActive: false 5 | } 6 | }, 7 | methods: { 8 | onContextMenu () { 9 | let rect 10 | if (this.$el.querySelector('.more-actions')) { 11 | rect = JSON.parse(JSON.stringify(this.$el.querySelector('.more-actions').getBoundingClientRect())) 12 | } 13 | this.menuActive = true 14 | Lode.ipc.invoke('framework-context-menu', this.model.id, rect) 15 | .finally(() => { 16 | this.menuActive = false 17 | const button = this.$el.querySelector('.more-actions') 18 | if (button) { 19 | button.blur() 20 | } 21 | }) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/components/modals/About.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 65 | -------------------------------------------------------------------------------- /src/renderer/components/modals/ConfirmSwitchProject.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 45 | -------------------------------------------------------------------------------- /src/renderer/components/modals/Licenses.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | -------------------------------------------------------------------------------- /src/renderer/components/modals/ProjectLoadingFailed.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /src/renderer/components/modals/RemoveFramework.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 37 | -------------------------------------------------------------------------------- /src/renderer/components/modals/RemoveProject.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /src/renderer/components/modals/RemoveRepository.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 43 | -------------------------------------------------------------------------------- /src/renderer/components/modals/ResetSettings.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /src/renderer/components/modals/RunningUnderTranslation.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 42 | -------------------------------------------------------------------------------- /src/renderer/components/modals/Terms.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 41 | -------------------------------------------------------------------------------- /src/renderer/components/modals/mixins/confirm.js: -------------------------------------------------------------------------------- 1 | import Modal from '@/components/modals/mixins/modal' 2 | 3 | export default { 4 | mixins: [ 5 | Modal 6 | ], 7 | props: { 8 | resolve: { 9 | type: Function, 10 | required: true 11 | }, 12 | reject: { 13 | type: Function, 14 | required: true 15 | } 16 | }, 17 | methods: { 18 | confirm (data) { 19 | this.resolve(data) 20 | this.close() 21 | }, 22 | cancel () { 23 | this.reject() 24 | this.close() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/components/modals/mixins/modal.js: -------------------------------------------------------------------------------- 1 | import Modal from '@/components/modals/Modal.vue' 2 | 3 | export default { 4 | components: { 5 | Modal 6 | }, 7 | emits: ['hide'], 8 | methods: { 9 | close () { 10 | this.$emit('hide') 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/directives/markdown.js: -------------------------------------------------------------------------------- 1 | import { castArray } from 'lodash' 2 | import Strings from '@lib/helpers/strings' 3 | 4 | // Use an object for each binding so we can store original templates 5 | // in case dynamic content changes and we need to re-compute markup. 6 | class MarkdownDirective { 7 | constructor (helper, el, binding) { 8 | this.helper = helper 9 | this.template = (binding.modifiers.set ? false : binding.value) || el.innerText || el.textContent 10 | } 11 | 12 | html (el, binding) { 13 | let text = this.template 14 | // If set modifier is present, use value as replacers 15 | if (binding.modifiers.set) { 16 | text = this.helper.set(this.template, ...castArray(binding.value)) 17 | } else if (binding.modifiers.plural) { 18 | text = this.helper.plural(this.template, binding.value) 19 | } 20 | 21 | return binding.modifiers.block 22 | ? this.helper.markdownBlock(text, true) 23 | : this.helper.markdown(text) 24 | } 25 | } 26 | 27 | export default function (locale = 'en-US') { 28 | return { 29 | beforeMount (el, binding, vnode) { 30 | el.markdown = new MarkdownDirective(new Strings(locale), el, binding) 31 | el.innerHTML = el.markdown.html(el, binding) 32 | }, 33 | updated (el, binding, vnode) { 34 | el.innerHTML = el.markdown.html(el, binding) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/helpers/validator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A generic validation class. 3 | */ 4 | export default class Validator { 5 | /** 6 | * Whether the current instance is valid. 7 | * @param ValidationErrors errors 8 | */ 9 | constructor (errors) { 10 | this.errors = errors || {} 11 | } 12 | 13 | refresh (errors) { 14 | this.errors = errors || {} 15 | } 16 | 17 | /** 18 | * Whether the current instance is valid. 19 | */ 20 | isValid () { 21 | return this.hasErrors() 22 | } 23 | 24 | /** 25 | * Reset errors in the current instance. 26 | */ 27 | reset (fields) { 28 | Object.keys(this.errors).forEach(key => { 29 | if (!fields || fields.includes(key)) { 30 | this.errors[key] = [] 31 | } 32 | }) 33 | } 34 | 35 | /** 36 | * Whether the current instance has any errors for the given key. 37 | * 38 | * @param key The key to check for errors. 39 | */ 40 | hasErrors (key) { 41 | if (typeof key === 'undefined') { 42 | let hasErrors = true 43 | Object.keys(this.errors).forEach(key => { 44 | if (this.errors[key].length > 0) { 45 | hasErrors = false 46 | } 47 | }) 48 | return hasErrors 49 | } 50 | 51 | return this.errors[key] && this.errors[key].length > 0 52 | } 53 | 54 | /** 55 | * Get errors for the given key. 56 | * 57 | * @param key The key to get errors from. 58 | */ 59 | getErrors (key) { 60 | if (!this.hasErrors(key)) { 61 | return '' 62 | } 63 | 64 | return this.errors[key].join('; ') 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/renderer/plugins/alerts.ts: -------------------------------------------------------------------------------- 1 | import type { App, State } from 'vue' 2 | import type { Store } from 'vuex' 3 | 4 | export default class Alerts { 5 | private store: Store 6 | 7 | constructor (store: Store) { 8 | this.store = store 9 | } 10 | 11 | install (app: App) { 12 | app.config.globalProperties.$alert = this 13 | } 14 | 15 | show (alert: any) { 16 | this.store.dispatch('alert/show', alert) 17 | } 18 | 19 | hide () { 20 | this.store.dispatch('alert/hide') 21 | } 22 | 23 | clear () { 24 | this.store.dispatch('alert/clear') 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/plugins/durations.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import BaseDurations from '@lib/helpers/durations' 3 | 4 | export default class Durations { 5 | private locale: string 6 | 7 | constructor (locale: string = 'en-US') { 8 | this.locale = locale 9 | } 10 | 11 | install (app: App) { 12 | app.config.globalProperties.$duration = new BaseDurations(this.locale) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/plugins/modals.js: -------------------------------------------------------------------------------- 1 | export default class Modals { 2 | constructor (store) { 3 | this.store = store 4 | this.modals = [] 5 | } 6 | 7 | install (app) { 8 | app.config.globalProperties.$modal = this 9 | } 10 | 11 | open (name, properties = {}, callback = null) { 12 | this.store.dispatch('modals/open', name) 13 | this.modals.push({ properties, callback }) 14 | } 15 | 16 | confirm (name, properties = {}) { 17 | return new Promise((resolve, reject) => { 18 | this.store.dispatch('modals/open', name) 19 | this.modals.push({ properties: { ...properties, ...{ resolve, reject }}}) 20 | }) 21 | } 22 | 23 | confirmIf (condition, name, properties = {}) { 24 | if (typeof condition === 'function') { 25 | condition = condition() 26 | } 27 | // If no confirmation is required, return a promise that resolves 28 | // automatically, for consistency. 29 | return condition ? this.confirm(name, properties) : new Promise((resolve, reject) => { 30 | resolve() 31 | }) 32 | } 33 | 34 | close () { 35 | this.store.dispatch('modals/close') 36 | const modal = this.modals.pop() 37 | if (modal.callback) { 38 | // Set a timeout before triggering callback in case callback is going 39 | // to instantiate a similar modal. Not doing so could cause the modal 40 | // to be cached by Vue, thus not rendering properly (i.e. not calling 41 | // `created` or `mounted` lifecycle events on the new modal). 42 | setTimeout(() => { 43 | modal.callback.call() 44 | }) 45 | } 46 | } 47 | 48 | clear () { 49 | this.store.dispatch('modals/clear') 50 | this.modals = [] 51 | } 52 | 53 | getProperties (index) { 54 | return this.modals[index].properties 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/plugins/strings.js: -------------------------------------------------------------------------------- 1 | import BaseStrings from '@lib/helpers/strings' 2 | 3 | export default class Strings { 4 | constructor (locale = 'en-US') { 5 | this.locale = locale 6 | } 7 | 8 | install (app) { 9 | app.config.globalProperties.$string = new BaseStrings(this.locale) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/plugins/translation.js: -------------------------------------------------------------------------------- 1 | import BaseTranslation from '@lib/helpers/translation' 2 | 3 | export default class Translation { 4 | install (app) { 5 | app.config.globalProperties.$trans = new BaseTranslation() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/plugins/unproxy.js: -------------------------------------------------------------------------------- 1 | export default class Unproxy { 2 | install (app) { 3 | app.config.globalProperties.$unproxy = value => JSON.parse(JSON.stringify(value)) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | // Load all modules automatically 4 | const context = require.context('@/store/modules', true, /\.js$/) 5 | const modules = {} 6 | context.keys().forEach((key) => { 7 | modules[key.replace(/^\.\/([aA0-zZ9]+)\.js$/, '$1')] = context(key).default 8 | }) 9 | 10 | export default createStore({ 11 | modules, 12 | strict: true 13 | }) 14 | -------------------------------------------------------------------------------- /src/renderer/store/modules/alert.js: -------------------------------------------------------------------------------- 1 | import app from '@' 2 | 3 | export default { 4 | namespaced: true, 5 | state: { 6 | alerts: [] 7 | }, 8 | mutations: { 9 | ADD (state, payload) { 10 | state.alerts.push(payload) 11 | }, 12 | REMOVE (state) { 13 | state.alerts.pop() 14 | }, 15 | CLEAR (state) { 16 | state.alerts = [] 17 | } 18 | }, 19 | actions: { 20 | show: ({ state, commit }, payload) => { 21 | commit('ADD', payload) 22 | if (state.alerts.length === 1) { 23 | app.config.globalProperties.$modal.open('AlertStack', {}, () => { 24 | commit('CLEAR') 25 | }) 26 | } 27 | }, 28 | hide: ({ commit }) => { 29 | commit('REMOVE') 30 | }, 31 | clear: ({ commit }) => { 32 | commit('CLEAR') 33 | } 34 | }, 35 | getters: { 36 | alerts: state => { 37 | return state.alerts 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/store/modules/expand.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: {}, 4 | mutations: { 5 | TOGGLE (state, identifier) { 6 | if (!state[identifier]) { 7 | state[identifier] = true 8 | return 9 | } 10 | delete state[identifier] 11 | }, 12 | EXPAND (state, identifier) { 13 | state[identifier] = true 14 | }, 15 | COLLAPSE (state, identifier) { 16 | delete state[identifier] 17 | }, 18 | COLLAPSE_ALL (state) { 19 | for (const identifier in state) { 20 | delete state[identifier] 21 | } 22 | } 23 | }, 24 | actions: { 25 | toggle: ({ state, commit }, identifier) => { 26 | commit('TOGGLE', identifier) 27 | }, 28 | expand: ({ state, commit }, identifier) => { 29 | commit('EXPAND', identifier) 30 | }, 31 | collapse: ({ state, commit }, identifier) => { 32 | commit('COLLAPSE', identifier) 33 | }, 34 | collapseAll: ({ state, commit }) => { 35 | commit('COLLAPSE_ALL') 36 | } 37 | }, 38 | getters: { 39 | expanded: state => id => { 40 | return !!state[id] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/store/modules/filters.js: -------------------------------------------------------------------------------- 1 | import { get, identity, isArray, isEmpty, pickBy } from 'lodash' 2 | 3 | export default { 4 | namespaced: true, 5 | state: {}, 6 | mutations: { 7 | SET (state, { id, filters }) { 8 | // Set by merging current state and removing falsy or empty values 9 | state[id] = pickBy({ 10 | ...get(state, id, {}), 11 | ...filters 12 | }, value => isArray(value) ? !isEmpty(value) : identity(value)) 13 | }, 14 | RESET (state) { 15 | Object.keys(state).forEach(id => { 16 | delete state[id] 17 | }) 18 | } 19 | }, 20 | getters: { 21 | all: state => id => { 22 | return state[id] || {} 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/store/modules/ledger.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: { 4 | ledger: {} 5 | }, 6 | mutations: { 7 | SET (state, payload) { 8 | state.ledger = {} 9 | state.ledger = { 10 | ...state.ledger, 11 | ...payload 12 | } 13 | }, 14 | UPDATE (state, payload) { 15 | state.ledger = { 16 | ...state.ledger, 17 | ...payload 18 | } 19 | } 20 | }, 21 | getters: { 22 | ledger: state => { 23 | return state.ledger 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/store/modules/modals.js: -------------------------------------------------------------------------------- 1 | import { last } from 'lodash' 2 | 3 | export default { 4 | namespaced: true, 5 | state: { 6 | modals: [] 7 | }, 8 | mutations: { 9 | ADD (state, name) { 10 | state.modals.push(name) 11 | }, 12 | REMOVE (state) { 13 | state.modals.pop() 14 | }, 15 | CLEAR (state) { 16 | state.modals = [] 17 | } 18 | }, 19 | actions: { 20 | open: ({ state, commit, dispatch, getters }, name) => { 21 | if (!getters['isOpen'](name)) { 22 | commit('ADD', name) 23 | dispatch('change') 24 | } 25 | }, 26 | close: ({ state, commit, dispatch }) => { 27 | commit('REMOVE') 28 | dispatch('change') 29 | }, 30 | clear: ({ state, commit, dispatch }) => { 31 | commit('CLEAR') 32 | dispatch('change') 33 | }, 34 | change: ({ state, commit }) => { 35 | if (state.modals.length) { 36 | document.body.classList.add('modal-open') 37 | return 38 | } 39 | document.body.classList.remove('modal-open') 40 | } 41 | }, 42 | getters: { 43 | isOpen: state => name => { 44 | return last(state.modals) === name 45 | }, 46 | hasModals: state => { 47 | return state.modals.length > 0 48 | }, 49 | modals: state => { 50 | return state.modals 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash' 2 | 3 | export default { 4 | namespaced: true, 5 | state: { 6 | }, 7 | getters: { 8 | value: state => key => { 9 | if (!key) { 10 | return state 11 | } 12 | return get(state, key) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/store/modules/status.js: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash' 2 | 3 | export default { 4 | namespaced: true, 5 | state: { 6 | status: {} 7 | }, 8 | mutations: { 9 | SET (state, payload) { 10 | state.status = {} 11 | state.status = { 12 | ...state.status, 13 | ...payload 14 | } 15 | }, 16 | UPDATE (state, payload) { 17 | state.status = { 18 | ...state.status, 19 | ...payload 20 | } 21 | } 22 | }, 23 | getters: { 24 | nugget: state => nugget => { 25 | return get(state.status, nugget, 'idle') 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/store/modules/tabs.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: { 4 | lastActive: '' 5 | }, 6 | mutations: { 7 | SET_LAST_ACTIVE (state, payload) { 8 | state.lastActive = payload 9 | }, 10 | CLEAR (state) { 11 | state.lastActive = '' 12 | } 13 | }, 14 | actions: { 15 | setLastActive: ({ commit }, tab) => { 16 | commit('SET_LAST_ACTIVE', tab) 17 | }, 18 | clear: ({ commit }) => { 19 | commit('CLEAR') 20 | } 21 | }, 22 | getters: { 23 | lastActive: state => { 24 | return state.lastActive 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/store/modules/theme.js: -------------------------------------------------------------------------------- 1 | import { OneHalfDark, OneHalfLight } from 'xterm-theme' 2 | 3 | // xterm.js will process colors loaded as themes, so 4 | // we need to give it hex colors as strings rather 5 | // than CSS variables. So we'll set up the terminal 6 | // theme as an application state, mutating it when 7 | // theme changes from main process. 8 | const getColors = theme => { 9 | if (theme === 'dark') { 10 | return { 11 | ...OneHalfDark, 12 | // Must match var(--secondary-background-color) 13 | background: '#22272e' 14 | } 15 | } 16 | 17 | return { 18 | ...OneHalfLight, 19 | // Must match var(--secondary-background-color) 20 | background: '#f6f8fa' 21 | } 22 | } 23 | 24 | export default { 25 | namespaced: true, 26 | state: { 27 | colors: {} 28 | }, 29 | mutations: { 30 | SET (state, theme) { 31 | state.colors = getColors(theme) 32 | } 33 | }, 34 | getters: { 35 | colors: state => { 36 | return state.colors 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/animations.scss: -------------------------------------------------------------------------------- 1 | // ======================================================================== 2 | // Animations 3 | // ======================================================================== 4 | 5 | .fade-enter-active, 6 | .fade-leave-active { 7 | transition: opacity .2s; 8 | } 9 | 10 | .fade-enter-from, 11 | .fade-leave-active { 12 | opacity: 0; 13 | } 14 | 15 | @keyframes rotate { 16 | 0% { 17 | transform: rotate(0deg); 18 | } 19 | 20 | 100% { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | 25 | @keyframes fade-in-top { 26 | from { 27 | opacity: 0; 28 | transform: translateY(-15px); 29 | } 30 | 31 | to { 32 | opacity: 1; 33 | transform: translateX(0); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import "functions"; 2 | @import "definitions"; 3 | @import "mixins"; 4 | @import "breakpoints"; 5 | @import "vendor"; 6 | @import "animations"; 7 | @import "variables"; 8 | @import "typography"; 9 | @import "globals"; 10 | @import "blocks/about"; 11 | @import "blocks/ansi"; 12 | @import "blocks/breadcrumbs"; 13 | @import "blocks/buttons"; 14 | @import "blocks/code"; 15 | @import "blocks/collapsible"; 16 | @import "blocks/console"; 17 | @import "blocks/contents"; 18 | @import "blocks/cta"; 19 | @import "blocks/diff"; 20 | @import "blocks/draggable"; 21 | @import "blocks/feedback"; 22 | @import "blocks/filename"; 23 | @import "blocks/forms"; 24 | @import "blocks/framework"; 25 | @import "blocks/icons"; 26 | @import "blocks/labels"; 27 | @import "blocks/license"; 28 | @import "blocks/loading"; 29 | @import "blocks/meta"; 30 | @import "blocks/modals"; 31 | @import "blocks/nugget"; 32 | @import "blocks/object"; 33 | @import "blocks/status"; 34 | @import "blocks/pre"; 35 | @import "blocks/preferences"; 36 | @import "blocks/project"; 37 | @import "blocks/results"; 38 | @import "blocks/scrollable"; 39 | @import "blocks/selective"; 40 | @import "blocks/settings"; 41 | @import "blocks/sidebar"; 42 | @import "blocks/tables"; 43 | @import "blocks/terms"; 44 | @import "blocks/test"; 45 | @import "blocks/test-information"; 46 | @import "blocks/test-result"; 47 | @import "blocks/titlebar"; 48 | @import "blocks/trace"; 49 | @import "highlight/common"; 50 | @import "highlight/light"; 51 | @import "highlight/dark"; 52 | -------------------------------------------------------------------------------- /src/styles/blocks/about.scss: -------------------------------------------------------------------------------- 1 | .about { 2 | text-align: center; 3 | 4 | > img { 5 | height: 80px; 6 | width: 80px; 7 | } 8 | 9 | h4 { 10 | margin-bottom: var(--spacing); 11 | } 12 | 13 | .version { 14 | span { 15 | display: block; 16 | } 17 | } 18 | 19 | .legal { 20 | font-size: 0; 21 | 22 | a { 23 | border-right: 1px solid var(--color-border-muted); 24 | display: inline-block; 25 | font-size: 1rem; 26 | height: 14px; 27 | line-height: 14px; 28 | padding: 0 4px; 29 | } 30 | 31 | :last-child { 32 | border: 0; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/blocks/ansi.scss: -------------------------------------------------------------------------------- 1 | .ansi { 2 | &.is-loading { 3 | overflow: hidden; 4 | } 5 | 6 | .parsed { 7 | .xterm-viewport { 8 | display: none; 9 | } 10 | 11 | .xterm-screen { 12 | height: auto !important; 13 | width: 100% !important; 14 | } 15 | 16 | .xterm-rows { 17 | background-color: transparent; 18 | user-select: text; 19 | width: 100% !important; 20 | 21 | > div { 22 | // Force line wrapping of terminal content 23 | display: inline-block; 24 | width: 100% !important; 25 | height: auto !important; 26 | word-break: break-all; 27 | 28 | span { 29 | width: auto !important; 30 | } 31 | } 32 | } 33 | 34 | .xterm-cursor { 35 | display: none; 36 | } 37 | } 38 | 39 | .terminal-mount { 40 | height: 0 !important; 41 | visibility: hidden; 42 | width: 0 !important; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/blocks/breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | .breadcrumbs { 4 | .breadcrumb-item { 5 | color: var(--color-fg-subtle); 6 | margin-left: 0; 7 | 8 | &::after { 9 | border: 0; 10 | color: var(--color-fg-subtle); 11 | opacity: .5; 12 | content: "●"; 13 | } 14 | } 15 | 16 | ol { 17 | > :last-child { 18 | &::after { 19 | content: ""; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/blocks/code.scss: -------------------------------------------------------------------------------- 1 | .ansi, 2 | .snippet { 3 | position: relative; 4 | 5 | button { 6 | appearance: none; 7 | border-radius: 3px; 8 | border: 1px solid var(--primary-border-color); 9 | float: right; 10 | height: 30px; 11 | margin-left: 4px; 12 | opacity: 0; 13 | outline: none; 14 | padding-left: 0; 15 | padding-right: 0; 16 | position: absolute; 17 | right: 4px; 18 | top: 4px; 19 | transition: opacity 100ms ease-in; 20 | width: 35px; 21 | z-index: 1; 22 | 23 | &:nth-of-type(2) { 24 | right: 42px; 25 | } 26 | 27 | i { 28 | color: var(--color-fg-subtler); 29 | position: relative; 30 | top: -1px; 31 | } 32 | 33 | &:hover { 34 | i { 35 | color: var(--color-link); 36 | } 37 | } 38 | } 39 | 40 | &:not(.is-loading):hover { 41 | button { 42 | opacity: 1; 43 | transition-delay: 200ms; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/styles/blocks/console.scss: -------------------------------------------------------------------------------- 1 | .console { 2 | margin-bottom: var(--spacing-double); 3 | 4 | .Box-body { 5 | word-break: break-all; 6 | } 7 | 8 | &.console--ansi, 9 | &.console--snippet { 10 | .content { 11 | .ansi, 12 | .snippet { 13 | pre { 14 | background-color: var(--secondary-background-color); 15 | border: 0; 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/blocks/contents.scss: -------------------------------------------------------------------------------- 1 | .contents { 2 | display: flex; 3 | flex: 0 0 auto; 4 | flex-direction: row; 5 | height: 100%; 6 | position: relative; 7 | 8 | @include linux { 9 | border-top: 1px solid var(--color-sidebar-border); 10 | } 11 | 12 | .no-projects { 13 | flex: 1 1 auto; 14 | padding: 210px 0 0; 15 | text-align: center; 16 | width: 100%; 17 | 18 | @include darwin { 19 | -webkit-app-region: drag; 20 | } 21 | 22 | h1 { 23 | margin-bottom: var(--spacing-double); 24 | } 25 | 26 | > button { 27 | -webkit-app-region: no-drag; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/blocks/cta.scss: -------------------------------------------------------------------------------- 1 | .cta { 2 | animation: fade-in-top .4s forwards; 3 | 4 | code { 5 | background-color: var(--secondary-background-color-dark); 6 | padding: 2px; 7 | } 8 | 9 | .btn { 10 | + .btn { 11 | margin-left: var(--spacing); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/blocks/diff.scss: -------------------------------------------------------------------------------- 1 | .diff { 2 | margin-bottom: var(--spacing-double); 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/blocks/draggable.scss: -------------------------------------------------------------------------------- 1 | // Dedicated draggable element to avoid 2 | // conflict with gutter dragging. 3 | .draggable { 4 | -webkit-app-region: drag; 5 | display: block; 6 | height: 100%; 7 | max-height: 43px; 8 | position: absolute; 9 | right: 0; 10 | top: 0; 11 | width: calc(100% - var(--pane-gutter-width)); 12 | 13 | // Windows has no draggable helpers because 14 | // it has a title bar. 15 | @include win32 { 16 | display: none !important; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/blocks/feedback.scss: -------------------------------------------------------------------------------- 1 | .feedback { 2 | h4, 3 | .message { 4 | user-select: text; 5 | word-break: break-word; 6 | } 7 | 8 | .message { 9 | color: var(--color-fg-muted); 10 | font-size: 1.2rem; 11 | margin-bottom: var(--spacing-and-half); 12 | margin-top: var(--spacing-half); 13 | 14 | :not(:empty) { 15 | &:not(.ansi) { 16 | white-space: pre-wrap; 17 | } 18 | } 19 | 20 | code { 21 | background-color: var(--color-scale-blue-1); 22 | border-radius: 2px; 23 | color: var(--color-scale-blue-8); 24 | padding: 1px 5px; 25 | word-break: break-all; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/blocks/filename.scss: -------------------------------------------------------------------------------- 1 | .filename { 2 | @include clearfix; 3 | 4 | font-size: 0; 5 | user-select: text; 6 | 7 | > span { 8 | font-size: 1rem; 9 | } 10 | 11 | > .dir { 12 | color: var(--color-fg-subtler); 13 | word-break: break-all; 14 | } 15 | 16 | // Truncate variation has a different structure. 17 | &--truncate { 18 | color: var(--color-fg-subtler); 19 | font-size: 1rem; 20 | word-break: break-all; 21 | 22 | > strong { 23 | color: var(--color-fg-default); 24 | font-weight: var(--font-weight-normal); 25 | word-break: normal; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/blocks/icons.scss: -------------------------------------------------------------------------------- 1 | i { 2 | display: inline-block; 3 | fill: currentcolor; 4 | 5 | &.rotate-90 { 6 | margin-right: 0 !important; 7 | transform: rotate(90deg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/blocks/license.scss: -------------------------------------------------------------------------------- 1 | .license { 2 | counter-increment: dependency; 3 | 4 | h5 { 5 | &::before { 6 | content: counters(dependency, ".") ". "; 7 | } 8 | } 9 | 10 | pre { 11 | cursor: text; 12 | margin: var(--spacing-half) 0 var(--spacing-double); 13 | user-select: text; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/blocks/loading.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | align-items: center; 3 | display: flex; 4 | flex-direction: column; 5 | flex-grow: 1; 6 | padding-top: 15%; 7 | 8 | > .loading-group { 9 | animation: fade-in-top .4s forwards; 10 | animation-delay: .2s; // Reduce flicker when loading quickly. 11 | opacity: 0; 12 | 13 | .spinner, 14 | .spinner::after { 15 | border-radius: 50%; 16 | height: 30em; 17 | width: 30em; 18 | } 19 | 20 | .spinner { 21 | animation: rotate .6s infinite linear; 22 | border-bottom: 1em solid var(--color-accent-muted); 23 | border-left: 1em solid var(--color-accent-emphasis); 24 | border-right: 1em solid var(--color-accent-muted); 25 | border-top: 1em solid var(--color-accent-muted); 26 | display: block; 27 | font-size: 2px; 28 | left: 0; 29 | margin: 0 auto var(--spacing); 30 | position: relative; 31 | text-indent: -9999em; 32 | transform: translateZ(0); 33 | } 34 | 35 | h2 { 36 | color: var(--color-fg-subtle); 37 | font-weight: var(--font-weight-semibold); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/blocks/meta.scss: -------------------------------------------------------------------------------- 1 | .meta { 2 | margin-bottom: var(--spacing-double); 3 | 4 | table { 5 | border-left: 1px solid var(--color-border-default); 6 | border-right: 1px solid var(--color-border-default); 7 | font-family: var(--font-family-monospace); 8 | font-size: 1rem; 9 | line-height: 1.5; 10 | 11 | .heading { 12 | font-weight: var(--font-weight-semibold); 13 | max-width: 250px; 14 | overflow: hidden; 15 | text-align: right; 16 | text-overflow: ellipsis; 17 | white-space: nowrap; 18 | } 19 | 20 | td { 21 | border-left: 0; 22 | padding: 3px 7px; 23 | user-select: text; 24 | } 25 | 26 | td:not(.heading) { 27 | border-right: 0; 28 | word-break: break-word; 29 | } 30 | } 31 | } 32 | 33 | .meta-group { 34 | padding-top: var(--spacing); 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/blocks/object.scss: -------------------------------------------------------------------------------- 1 | .object { 2 | > .key-value { 3 | margin-bottom: var(--spacing); 4 | 5 | > .key { 6 | color: var(--color-fg-subtler); 7 | font-weight: var(--font-weight-semibold); 8 | text-transform: uppercase; 9 | } 10 | 11 | > .value { 12 | user-select: text; 13 | } 14 | } 15 | 16 | > :last-child { 17 | margin-bottom: 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/blocks/pre.scss: -------------------------------------------------------------------------------- 1 | pre { 2 | background-color: var(--secondary-background-color); 3 | border: 1px solid var(--primary-border-color); 4 | border-radius: 3px; 5 | font-family: var(--font-family-monospace); 6 | padding: var(--spacing); 7 | user-select: text; 8 | white-space: pre-wrap; 9 | word-break: break-word; 10 | 11 | &:empty { 12 | display: none; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/blocks/preferences.scss: -------------------------------------------------------------------------------- 1 | .preferences { 2 | .form-group { 3 | dl { 4 | dt { 5 | width: 180px; 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/blocks/project.scss: -------------------------------------------------------------------------------- 1 | .project { 2 | display: flex; 3 | flex-grow: 1; 4 | 5 | > .split { 6 | display: flex; 7 | flex-grow: 1; 8 | 9 | > .pane { 10 | display: flex; 11 | flex-direction: column; 12 | flex-grow: 1; 13 | height: 100%; 14 | overflow: hidden; 15 | position: relative; 16 | width: 33%; 17 | z-index: 1; 18 | 19 | + .gutter { 20 | border-left: 1px solid var(--color-sidebar-border); 21 | cursor: col-resize !important; 22 | margin: 0; 23 | margin-right: calc(calc(var(--pane-gutter-width) - 1px) * -1); 24 | z-index: 3; 25 | } 26 | 27 | &:not(.sidebar) { 28 | + .gutter { 29 | border-left-color: var(--pane-border-color); 30 | } 31 | } 32 | } 33 | 34 | &.empty { 35 | > .pane:not(.sidebar) { 36 | background-color: var(--secondary-background-color); 37 | overflow: visible; 38 | white-space: nowrap; 39 | z-index: 2; 40 | 41 | .loading, 42 | .cta { 43 | align-items: flex-start; 44 | margin-left: calc(var(--spacing-triple) - var(--pane-gutter-width)); 45 | padding-top: var(--spacing-double); 46 | 47 | .btn { 48 | margin-top: var(--spacing); 49 | } 50 | } 51 | 52 | + .gutter { 53 | visibility: hidden; 54 | } 55 | } 56 | 57 | > :last-child { 58 | border-left: 0; 59 | margin-left: -1px; 60 | z-index: 1 !important; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/styles/blocks/results.scss: -------------------------------------------------------------------------------- 1 | .results { 2 | background-color: var(--primary-background-color); 3 | flex-grow: 1; 4 | 5 | &.blankslate { 6 | background-color: var(--secondary-background-color); 7 | border: 0; 8 | border-radius: 0; 9 | box-shadow: none; 10 | color: var(--color-fg-subtle); 11 | opacity: .4; 12 | } 13 | 14 | > .has-status { 15 | display: flex; 16 | flex-direction: column; 17 | height: calc(100vh - var(--titlebar-height)); 18 | 19 | > .header { 20 | background-color: var(--secondary-background-color); 21 | padding-bottom: 15px; 22 | 23 | h2 { 24 | -webkit-app-region: no-drag; 25 | flex-grow: 0 !important; 26 | flex-shrink: 1 !important; 27 | font-size: 1.6rem; 28 | line-height: 1.9rem; 29 | user-select: text; 30 | } 31 | 32 | .breadcrumbs { 33 | padding: 0 15px; 34 | 35 | li { 36 | display: inline; 37 | user-select: text; 38 | white-space: normal; 39 | word-break: break-all; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/blocks/scrollable.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | .scrollable { 4 | .shadow { 5 | &::after { 6 | border-top: 1px solid var(--color-border-default); 7 | box-shadow: var(--color-shadow-small); 8 | content: ""; 9 | height: 0; 10 | position: fixed; 11 | width: 100%; 12 | z-index: 1; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/blocks/selective.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | .selective-toggle { 4 | height: 16px; 5 | margin-left: 3px; 6 | margin-right: 7px; 7 | position: relative; 8 | text-align: center; 9 | width: 14px; 10 | 11 | button { 12 | appearance: none; 13 | background-color: transparent; 14 | border: 0; 15 | border-color: var(--color-fg-subtler); 16 | border-radius: 100%; 17 | border-style: double; 18 | border-width: 4px; 19 | box-shadow: none; 20 | display: inline-block; 21 | height: 100%; 22 | height: 12px; 23 | opacity: 1; 24 | padding: 0; 25 | position: absolute; 26 | top: 2px; 27 | transition: transform 240ms cubic-bezier(.18, 1.4, .4, 1); 28 | width: 12px; 29 | z-index: 1; 30 | 31 | @include win32 { 32 | // Button and input positioning is tricky, so just override 33 | // as needed in Windows environments. 34 | top: 3px; 35 | } 36 | 37 | &:hover, 38 | &:focus, 39 | &:active { 40 | outline: 0; 41 | } 42 | 43 | &[disabled] { 44 | opacity: .5; 45 | } 46 | } 47 | 48 | &:hover, 49 | &:focus, 50 | &:active { 51 | button { 52 | &:not([disabled]) { 53 | background-color: var(--color-link); 54 | border-color: var(--primary-background-color); 55 | transform: scale(1.5); 56 | } 57 | } 58 | } 59 | 60 | input { 61 | opacity: 0; 62 | position: relative; 63 | 64 | @include win32 { 65 | // Button and input positioning is tricky, so just override 66 | // as needed in Windows environments. 67 | top: 2px; 68 | } 69 | } 70 | 71 | .selective & { 72 | button { 73 | opacity: 0; 74 | } 75 | 76 | input { 77 | opacity: 1; 78 | transition: none; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/styles/blocks/tables.scss: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | table { 3 | width: 100%; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/blocks/terms.scss: -------------------------------------------------------------------------------- 1 | .terms { 2 | pre { 3 | background-color: transparent; 4 | border: 0; 5 | font-family: var(--font-family-sans-serif); 6 | padding: 0; 7 | white-space: normal; 8 | 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6 { 15 | margin-bottom: var(--spacing-half); 16 | } 17 | 18 | ol, 19 | ul { 20 | margin-left: var(--spacing-double); 21 | 22 | li { 23 | margin-bottom: var(--spacing-half); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/blocks/test-information.scss: -------------------------------------------------------------------------------- 1 | .test-information { 2 | font-size: 1.1rem; 3 | 4 | .heading { 5 | font-weight: var(--font-weight-semibold); 6 | white-space: nowrap; 7 | width: 1%; 8 | } 9 | 10 | td:not(.heading) { 11 | user-select: text; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/blocks/test-result.scss: -------------------------------------------------------------------------------- 1 | .test-result { 2 | display: flex; 3 | flex: 1; 4 | flex-direction: column; 5 | flex-grow: 1; 6 | min-height: 0; 7 | 8 | .tabs { 9 | background-color: var(--secondary-background-color); 10 | border-bottom: 1px solid var(--primary-border-color); 11 | 12 | nav { 13 | padding: 0 var(--spacing-half); 14 | 15 | .tab { 16 | appearance: none; 17 | background: none; 18 | border: 0; 19 | box-shadow: none; 20 | color: var(--color-fg-default); 21 | padding: calc(var(--spacing) + -2px) var(--spacing); 22 | 23 | &:hover, 24 | &:focus, 25 | &:active { 26 | outline: none; 27 | text-decoration: none; 28 | } 29 | 30 | &.selected { 31 | color: var(--color-link); 32 | } 33 | } 34 | } 35 | } 36 | 37 | .test-result-general { 38 | user-select: text; 39 | } 40 | 41 | .test-result-breakdown { 42 | flex: 1; 43 | overflow-x: hidden; 44 | overflow-y: auto; 45 | padding: 15px; 46 | position: relative; 47 | 48 | // Avoid content inside results pane growing indefinitely, 49 | // especially when trace headers have very long filenames 50 | // which are set to not wrap. 51 | > div { 52 | position: absolute; 53 | width: calc(100% - 30px); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/styles/blocks/test.scss: -------------------------------------------------------------------------------- 1 | .test { 2 | .test-name { 3 | height: 100%; 4 | left: calc(var(--status-block-width) + 9px); 5 | line-height: 2.5em; 6 | overflow: hidden; 7 | position: absolute; 8 | text-overflow: ellipsis; 9 | top: 0; 10 | white-space: nowrap; 11 | 12 | // 100% - left positioning - padding 13 | width: calc(100% - (var(--status-block-width) + 9px) - var(--status-block-width)); 14 | } 15 | 16 | .selective-toggle + .test-name { 17 | margin-left: 24px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/blocks/trace.scss: -------------------------------------------------------------------------------- 1 | .trace { 2 | margin-bottom: var(--spacing-double); 3 | } 4 | 5 | .trace-group { 6 | .trace { 7 | margin-bottom: 0; 8 | } 9 | 10 | .trace-link { 11 | border-left: 2px solid var(--color-border-default); 12 | display: block; 13 | height: 35px; 14 | left: var(--spacing-double); 15 | position: relative; 16 | 17 | i { 18 | background-color: var(--color-canvas-default); 19 | border-radius: 22px; 20 | color: var(--color-fg-subtle); 21 | height: 20px; 22 | left: -11px; 23 | line-height: 21px; 24 | position: absolute; 25 | text-align: center; 26 | top: 7px; 27 | width: 20px; 28 | } 29 | 30 | span { 31 | color: var(--color-fg-muted); 32 | font-family: var(--font-family-monospace); 33 | left: 13px; 34 | position: relative; 35 | top: 7px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/styles/definitions.scss: -------------------------------------------------------------------------------- 1 | // ======================================================================== 2 | // Colors 3 | // ======================================================================== 4 | 5 | // 6 | // 7 | // -------- Grays -------- 8 | $gray-000: #f7f7f7 !default; 9 | $gray-100: #f0f0f0 !default; 10 | $gray-200: #ececec !default; 11 | $gray-300: #e3e3e3 !default; 12 | $gray-400: #ced4da !default; 13 | $gray-500: #adb5bd !default; 14 | $gray-600: #6c757d !default; 15 | $gray-700: #495057 !default; 16 | $gray-800: #343a40 !default; 17 | $gray-900: #212529 !default; 18 | 19 | // -------- Blue -------- 20 | $blue-000: #f1f8ff !default; 21 | $blue-100: #dfefff !default; 22 | $blue-200: #bbdaff !default; 23 | $blue-300: #79b8ff !default; 24 | $blue-400: #2188ff !default; 25 | $blue-500: #0366d6 !default; 26 | $blue-600: #005cc5 !default; 27 | $blue-700: #044289 !default; 28 | $blue-800: #032f62 !default; 29 | $blue-900: #05264c !default; 30 | 31 | // -------- Yellow -------- 32 | $yellow-000: #fffdef !default; 33 | $yellow-100: #fdf7ce !default; 34 | $yellow-200: #fff29c !default; 35 | $yellow-300: #ffea7f !default; 36 | $yellow-400: #ffdf5d !default; 37 | $yellow-500: #ffd33d !default; 38 | $yellow-600: #f9c513 !default; 39 | $yellow-700: #dbab09 !default; 40 | $yellow-800: #b08800 !default; 41 | $yellow-900: #735c0f !default; 42 | 43 | // -------- Red -------- 44 | $red-000: #ffeef0 !default; 45 | $red-100: #ffdce0 !default; 46 | $red-200: #fdaeb7 !default; 47 | $red-300: #f97583 !default; 48 | $red-400: #f8575f !default; 49 | $red-500: #f7434c !default; 50 | $red-600: #f6212b !default; 51 | $red-700: #e40a15 !default; 52 | $red-800: #c20912 !default; 53 | $red-900: #a4070f !default; 54 | 55 | // -------- Fades -------- 56 | $white: #fff !default; 57 | -------------------------------------------------------------------------------- /src/styles/functions.scss: -------------------------------------------------------------------------------- 1 | // ======================================================================== 2 | // Functions 3 | // ======================================================================== 4 | 5 | @use "sass:math"; 6 | @use "sass:map"; 7 | @use "sass:string"; 8 | 9 | /// 10 | /// Casts a number into a string 11 | /// 12 | /// @param {String | Number} $value - Value to be parsed 13 | /// 14 | /// @return {String} 15 | /// 16 | @function to-string($value) { 17 | @return inspect($value); 18 | } 19 | 20 | /// 21 | /// Casts a string into a number 22 | /// 23 | /// @param {String | Number} $value - Value to be parsed 24 | /// 25 | /// @return {Number} 26 | /// 27 | @function to-number($value) { 28 | @if type-of($value) == "number" { 29 | @return $value; 30 | } @else if type-of($value) != "string" { 31 | $log: log("Value for `to-number` should be a number or a string."); 32 | } 33 | 34 | $result: 0; 35 | $digits: 0; 36 | $minus: string.slice($value, 1, 1) == "-"; 37 | $numbers: ("0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9); 38 | 39 | @for $i from if($minus, 2, 1) through str-length($value) { 40 | $character: string.slice($value, $i, $i); 41 | 42 | @if not (index(map-keys($numbers), $character) or $character == ".") { 43 | @return to-length(if($minus, -$result, $result), string.slice($value, $i)); 44 | } 45 | 46 | @if $character == "." { 47 | $digits: 1; 48 | } @else if $digits == 0 { 49 | $result: $result * 10 + map.get($numbers, $character); 50 | } @else { 51 | $digits: $digits * 10; 52 | $result: $result + math.div(map.get($numbers, $character), $digits); 53 | } 54 | } 55 | 56 | @return if($minus, -$result, $result); 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background-color: var(--secondary-background-color); 4 | font-family: var(--font-family-sans-serif); 5 | font-size: 12px; 6 | height: 100%; 7 | margin: 0; 8 | overflow: hidden; 9 | padding: 0; 10 | user-select: none; 11 | width: 100%; 12 | } 13 | 14 | // stylelint-disable-next-line selector-max-id 15 | #app { 16 | display: flex; 17 | flex-direction: column; 18 | height: 100%; 19 | width: 100%; 20 | } 21 | 22 | a { 23 | color: var(--color-link); 24 | } 25 | 26 | * { 27 | cursor: default !important; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/highlight/dark.scss: -------------------------------------------------------------------------------- 1 | @include dark-context { 2 | /* stylelint-disable-next-line no-invalid-position-at-import-rule */ 3 | @import "node_modules/highlight.js/styles/atom-one-dark"; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/highlight/light.scss: -------------------------------------------------------------------------------- 1 | @include light-context { 2 | /* stylelint-disable-next-line no-invalid-position-at-import-rule */ 3 | @import "node_modules/highlight.js/styles/atom-one-light"; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/styles/images/error.png -------------------------------------------------------------------------------- /src/styles/images/error@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/styles/images/error@2x.png -------------------------------------------------------------------------------- /src/styles/images/octocat-spinner-16px.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/styles/images/octocat-spinner-16px.gif -------------------------------------------------------------------------------- /src/styles/images/octocat-spinner-32-EAF2F5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/styles/images/octocat-spinner-32-EAF2F5.gif -------------------------------------------------------------------------------- /src/styles/images/octocat-spinner-32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/styles/images/octocat-spinner-32.gif -------------------------------------------------------------------------------- /src/styles/images/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/styles/images/success.png -------------------------------------------------------------------------------- /src/styles/images/success@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/src/styles/images/success@2x.png -------------------------------------------------------------------------------- /src/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | // ======================================================================== 2 | // Mixins 3 | // ======================================================================== 4 | 5 | @mixin drop-shadow($opacity: .175) { 6 | box-shadow: 0 6px 12px rgba(0, 0, 0, $opacity); 7 | } 8 | 9 | @mixin no-shadow { 10 | background-clip: padding-box; 11 | box-shadow: none; 12 | } 13 | 14 | @mixin win32 { 15 | body.platform-win32 & { 16 | @content; 17 | } 18 | } 19 | 20 | @mixin win32-context { 21 | body.platform-win32 { 22 | @content; 23 | } 24 | } 25 | 26 | @mixin darwin { 27 | body.platform-darwin & { 28 | @content; 29 | } 30 | } 31 | 32 | @mixin darwin-context { 33 | body.platform-darwin { 34 | @content; 35 | } 36 | } 37 | 38 | @mixin linux { 39 | body.platform-linux & { 40 | @content; 41 | } 42 | } 43 | 44 | @mixin linux-context { 45 | body.platform-linux { 46 | @content; 47 | } 48 | } 49 | 50 | @mixin light { 51 | html[data-color-mode="light"] & { 52 | @content; 53 | } 54 | } 55 | 56 | @mixin light-context { 57 | html[data-color-mode="light"] { 58 | @content; 59 | } 60 | } 61 | 62 | @mixin dark { 63 | html[data-color-mode="dark"] & { 64 | @content; 65 | } 66 | } 67 | 68 | @mixin dark-context { 69 | html[data-color-mode="dark"] { 70 | @content; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/styles/typography.scss: -------------------------------------------------------------------------------- 1 | h1, 2 | h2, 3 | h3, 4 | h4, 5 | h5, 6 | h6 { 7 | &.text-muted { 8 | color: var(--color-fg-subtle); 9 | margin-bottom: var(--spacing-half); 10 | text-transform: uppercase; 11 | } 12 | } 13 | 14 | code { 15 | font-family: var(--font-family-monospace) !important; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/vendor.scss: -------------------------------------------------------------------------------- 1 | // ======================================================================== 2 | // Vendor SCSS 3 | // ======================================================================== 4 | 5 | @import "node_modules/@primer/css/color-modes/index"; 6 | @import "node_modules/@primer/css/core/index"; 7 | @import "node_modules/@primer/css/product/index"; 8 | 9 | // ======================================================================== 10 | // Vendor CSS 11 | // ======================================================================== 12 | 13 | @import "node_modules/overlayscrollbars/css/OverlayScrollbars"; 14 | @import "node_modules/@xterm/xterm/css/xterm" 15 | -------------------------------------------------------------------------------- /src/types/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'default-shell' 2 | declare module 'fuzzysearch' 3 | declare module 'sha1-es' 4 | declare module 'shell-env' 5 | declare module 'strip-ansi' 6 | declare module 'xterm-theme' 7 | -------------------------------------------------------------------------------- /src/types/vuex-shim.d.ts: -------------------------------------------------------------------------------- 1 | import { ComponentCustomProperties } from 'vue' 2 | import { Store } from 'vuex' 3 | 4 | declare module '@vue/runtime-core' { 5 | // Declare your own store states. 6 | interface State { 7 | count: number 8 | } 9 | 10 | interface ComponentCustomProperties { 11 | $store: Store 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/static/icons/1024x1024.png -------------------------------------------------------------------------------- /static/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/static/icons/128x128.png -------------------------------------------------------------------------------- /static/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/static/icons/256x256.png -------------------------------------------------------------------------------- /static/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/static/icons/32x32.png -------------------------------------------------------------------------------- /static/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/static/icons/512x512.png -------------------------------------------------------------------------------- /static/icons/gem.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /support/assets/dmg-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/support/assets/dmg-bg.png -------------------------------------------------------------------------------- /support/assets/dmg-bg.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/support/assets/dmg-bg.tiff -------------------------------------------------------------------------------- /support/assets/dmg-bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lodeapp/lode/013c999d5450d116dd6177277bfe28b217d3add3/support/assets/dmg-bg@2x.png -------------------------------------------------------------------------------- /support/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /support/release-notes.example.md: -------------------------------------------------------------------------------- 1 | New: 2 | - Group names with array items will be used as badges. 3 | - This is another array item (prefixed by a dash, parsed as YAML) 4 | Changed: 5 | - This is another group. Colons will not show. 6 | Fixed: 7 | - Also supports **markdown** 8 | Removed: 9 | - New, Changed, Fixed and Removed are categories that have custom styling 10 | Empty groups will be rendered as stand-alone notes (i.e. no badge prefix). 11 | -------------------------------------------------------------------------------- /support/release-notes.md: -------------------------------------------------------------------------------- 1 | New: 2 | - Preliminary support for PHPUnit 10+ 3 | -------------------------------------------------------------------------------- /tests/cypress/config.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'node:path' 2 | import { defineConfig } from 'cypress' 3 | import webpackPreprocessor from '@cypress/webpack-preprocessor' 4 | 5 | export default defineConfig({ 6 | e2e: { 7 | projectId: '6ki1wz', 8 | baseUrl: 'http://localhost:9080', 9 | specPattern: 'tests/cypress/e2e/**/*.{js,ts}', 10 | fixturesFolder: Path.resolve('../fixtures'), 11 | screenshotsFolder: Path.resolve('screenshots'), 12 | supportFile: Path.resolve('support/index.js'), 13 | videosFolder: Path.resolve('videos'), 14 | videoCompression: 0, 15 | video: true, 16 | retries: { 17 | runMode: 2, 18 | openMode: 0 19 | }, 20 | scrollBehavior: false, 21 | waitForAnimations: false, 22 | setupNodeEvents (on, config) { 23 | on('file:preprocessor', webpackPreprocessor({ 24 | webpackOptions: { 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.ts?$/, 29 | loader: 'ts-loader', 30 | exclude: /node_modules/ 31 | }, 32 | { 33 | test: /\.js$/, 34 | use: 'babel-loader', 35 | exclude: /node_modules/ 36 | } 37 | ] 38 | }, 39 | resolve: { 40 | alias: { 41 | '@preload': Path.join(__dirname, '../../src/preload'), 42 | 'electron': Path.join(__dirname, '../mocks/electron.js') 43 | }, 44 | extensions: ['.js', '.ts'] 45 | } 46 | } 47 | })) 48 | } 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /tests/cypress/e2e/theme.js: -------------------------------------------------------------------------------- 1 | describe('Themes', () => { 2 | it('can use light theme on load and switch when notified', () => { 3 | cy 4 | .start() 5 | .get('html') 6 | .invoke('attr', 'data-color-mode') 7 | .should('equal', 'light') 8 | .get('html') 9 | .invoke('attr', 'data-light-theme') 10 | .should('equal', 'light') 11 | .get('html') 12 | .invoke('attr', 'data-dark-theme') 13 | .should('equal', 'dark_dimmed') 14 | .ipcEvent('theme-updated', 'dark') 15 | .get('html') 16 | .invoke('attr', 'data-color-mode') 17 | .should('equal', 'dark') 18 | .ipcEvent('theme-updated', 'light') 19 | .get('html') 20 | .invoke('attr', 'data-color-mode') 21 | .should('equal', 'light') 22 | }) 23 | 24 | it('can use dark theme on load', () => { 25 | cy 26 | .start({ theme: 'dark' }) 27 | .get('html') 28 | .invoke('attr', 'data-color-mode') 29 | .should('equal', 'dark') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/cypress/support/assertions.js: -------------------------------------------------------------------------------- 1 | import electron from '../../mocks/electron' 2 | 3 | Cypress.Commands.add('assertInvoked', (...args) => { 4 | expect(electron.ipcRenderer.invoke).to.be.calledWith(...args) 5 | }) 6 | 7 | Cypress.Commands.add('assertInvokedOnce', (...args) => { 8 | expect(electron.ipcRenderer.invoke).to.be.calledOnceWith(...args) 9 | }) 10 | 11 | Cypress.Commands.add('assertInvokedCount', times => { 12 | expect(electron.ipcRenderer.invoke).to.be.callCount(times) 13 | }) 14 | 15 | Cypress.Commands.add('assertEmitted', (...args) => { 16 | expect(electron.ipcRenderer.send).to.be.calledWith(...args) 17 | }) 18 | 19 | Cypress.Commands.add('assertEmittedOnce', (...args) => { 20 | expect(electron.ipcRenderer.send).to.be.calledOnceWith(...args) 21 | }) 22 | 23 | Cypress.Commands.add('assertEmittedCount', times => { 24 | expect(electron.ipcRenderer.send).to.be.calledOnceWith(times) 25 | }) 26 | 27 | Cypress.Commands.add('assertArgs', { prevSubject: true }, (subject, ...args) => { 28 | cy.then(() => { 29 | return new Promise((resolve) => { 30 | expect(subject.args).to.deep.equal(args) 31 | resolve() 32 | }) 33 | }) 34 | }) 35 | 36 | Cypress.Commands.add('assertArgEq', { prevSubject: true }, (subject, index, value) => { 37 | cy.then(() => { 38 | return new Promise((resolve) => { 39 | expect(subject.args[index]).to.deep.equal(value) 40 | resolve() 41 | }) 42 | }) 43 | }) 44 | 45 | Cypress.Commands.add('assertChannel', { prevSubject: true }, (subject, channel) => { 46 | cy.then(() => { 47 | return new Promise((resolve) => { 48 | expect(subject.args[0]).to.equal(channel) 49 | resolve() 50 | }) 51 | }) 52 | }) 53 | 54 | Cypress.Commands.add('assertNormalizedText', { prevSubject: true }, (subject, string) => { 55 | cy 56 | .wrap(subject) 57 | .should(el => { 58 | expect(el.get(0).innerText.trim()).to.eq(string) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /tests/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | import electron from '../../mocks/electron' 2 | 3 | import './ipc' 4 | import './process' 5 | import './assertions' 6 | 7 | window.electron = electron 8 | -------------------------------------------------------------------------------- /tests/cypress/support/ipc.js: -------------------------------------------------------------------------------- 1 | import electron from '../../mocks/electron' 2 | 3 | Cypress.Commands.add('ipcEvent', (...args) => { 4 | // Allow functions that resolve into arguments 5 | if (args.length === 1 && typeof args[0] === 'function') { 6 | args = args[0]() 7 | } 8 | 9 | cy.then(() => { 10 | electron.ipcRenderer.trigger(...args) 11 | }) 12 | }) 13 | 14 | Cypress.Commands.add('ipcResetMockHistory', () => { 15 | cy.then(() => { 16 | return new Promise((resolve) => { 17 | if (electron.ipcRenderer.send.resetHistory) { 18 | electron.ipcRenderer.send.resetHistory() 19 | } 20 | if (electron.ipcRenderer.invoke.resetHistory) { 21 | electron.ipcRenderer.invoke.resetHistory() 22 | } 23 | resolve() 24 | }) 25 | }) 26 | }) 27 | 28 | Cypress.Commands.add('ipcInvocation', index => { 29 | cy.wrap(electron.ipcRenderer.invoke.getCall(index)) 30 | }) 31 | 32 | Cypress.Commands.add('ipcEmission', index => { 33 | cy.wrap(electron.ipcRenderer.send.getCall(index)) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/cypress/support/process.js: -------------------------------------------------------------------------------- 1 | import { Lode } from '@preload/lode' 2 | import electron from '../../mocks/electron' 3 | 4 | Cypress.Commands.add('start', (options = {}) => { 5 | cy 6 | .visit('/', { 7 | onBeforeLoad (win) { 8 | win.Lode = Lode 9 | }, 10 | onLoad (win) { 11 | cy.spy(electron.ipcRenderer, 'send') 12 | electron.ipcRenderer.trigger('did-finish-load', { 13 | ...{ 14 | theme: 'light', 15 | version: '0.0.0', 16 | focus: true 17 | }, 18 | ...options 19 | }) 20 | } 21 | }) 22 | .nextTick() 23 | }) 24 | 25 | Cypress.Commands.add('startWithProject', (options = {}) => { 26 | cy 27 | .start(options) 28 | .fixture('framework/project.json') 29 | .then(project => { 30 | electron.ipcRenderer.trigger('project-ready', project) 31 | }) 32 | .wait(1) 33 | }) 34 | 35 | Cypress.Commands.add('nextTick', callback => { 36 | if (callback) { 37 | cy 38 | .wait(1) 39 | .then(callback) 40 | .wait(1) 41 | } else { 42 | cy 43 | .wait(1) 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /tests/fixtures/framework/jest/26/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "jest-1", 3 | "name": "Jest", 4 | "type": "jest", 5 | "command": "yarn test", 6 | "path": "", 7 | "runsInRemote": false, 8 | "remotePath": "", 9 | "sshHost": "", 10 | "sshUser": null, 11 | "sshPort": null, 12 | "sshIdentity": null, 13 | "active": false, 14 | "status": "idle", 15 | "proprietary": {}, 16 | "sort": "name", 17 | "selected": 0, 18 | "canToggleTests": false 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/framework/ledger.json: -------------------------------------------------------------------------------- 1 | { 2 | "queued": 0, 3 | "running": 0, 4 | "passed": 0, 5 | "failed": 0, 6 | "incomplete": 0, 7 | "skipped": 0, 8 | "warning": 0, 9 | "partial": 0, 10 | "empty": 0, 11 | "idle": 0, 12 | "error": 0 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/framework/phpunit/8.0/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "phpunit-1", 3 | "name": "PHPUnit", 4 | "type": "phpunit", 5 | "command": "./vendor/bin/phpunit", 6 | "path": "", 7 | "runsInRemote": false, 8 | "remotePath": "", 9 | "sshHost": "", 10 | "sshUser": null, 11 | "sshPort": null, 12 | "sshIdentity": null, 13 | "active": false, 14 | "status": "idle", 15 | "proprietary": { 16 | "autoloadPath": "" 17 | }, 18 | "sort": "framework", 19 | "selected": 0, 20 | "canToggleTests": true 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/framework/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "42", 3 | "name": "Biscuit", 4 | "active": null 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/framework/repositories.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "repository-1", 4 | "name": "hobnobs", 5 | "path": "/lodeapp/lode/hobnobs", 6 | "expanded": true 7 | }, 8 | { 9 | "id": "repository-2", 10 | "name": "digestives", 11 | "path": "/lodeapp/lode/digestives", 12 | "expanded": false 13 | }, 14 | { 15 | "id": "repository-3", 16 | "name": "rich-tea", 17 | "path": "/lodeapp/lode/rich-tea", 18 | "expanded": false 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /tests/fixtures/framework/types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Jest", 4 | "type": "jest", 5 | "command": "yarn test", 6 | "path": "", 7 | "proprietary": {} 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /tests/fixtures/process/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/process/10.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/process/11.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/process/12.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/process/13.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/process/14.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/process/15.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/process/16.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/process/17.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/process/18.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/process/19.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "Starting...", 5 | "\n<<>>", 8 | "Ended!" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/process/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/process/20.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "Starting...", 5 | "\n<<>>\r\n", 11 | "Ended!\r\n" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/process/24.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/process/26.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>", 10 | "\n>" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/process/27.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "<<19302746jkiqpaldfh<<>>" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/process/28.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>hdkowhsftfg" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/process/29.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/process/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/process/4.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/process/5.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/process/6.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/process/7.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/process/8.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/process/9.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": { 3 | "rawChunks": [ 4 | "\n<<>>" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/process/decoded.json: -------------------------------------------------------------------------------- 1 | { 2 | "biscuit": "Hobnob", 3 | "ingredients": [ 4 | "Rolled Oats", 5 | "Wholemeal Wheat Flour", 6 | "Sugar", 7 | "Vegetable Oil", 8 | "Partially Inverted Sugar Syrup", 9 | "Sodium Bicarbonate", 10 | "Ammonium Bicarbonate", 11 | "Salt" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/lib/frameworks/emitter.spec.js: -------------------------------------------------------------------------------- 1 | import { ApplicationWindow } from '@main/application-window' 2 | import { ProjectEventEmitter } from '@lib/frameworks/emitter' 3 | 4 | jest.mock('@lib/state') 5 | jest.mock('electron-store') 6 | jest.mock('@main/application-window') 7 | 8 | it('can return the associated application window', async () => { 9 | const window = new ApplicationWindow() 10 | const emitter = new ProjectEventEmitter(window) 11 | expect(emitter.getApplicationWindow()).toBe(window) 12 | }) 13 | 14 | it('can emit events to the application window', async () => { 15 | const window = new ApplicationWindow() 16 | window.canReceiveEvents = jest.fn(() => true) 17 | 18 | const emitter = new ProjectEventEmitter(window) 19 | 20 | // Can emit with no arguments 21 | emitter.emitToRenderer('biscuit') 22 | expect(window.send).toHaveBeenLastCalledWith('biscuit', []) 23 | 24 | // Can emit with arguments 25 | emitter.emitToRenderer('biscuit', 'Hobnobs', 'Digestives', 'Rich Tea') 26 | expect(window.send).toHaveBeenLastCalledWith('biscuit', ['Hobnobs', 'Digestives', 'Rich Tea']) 27 | 28 | expect(window.send).toHaveBeenCalledTimes(2) 29 | }) 30 | 31 | it('does not emit events if the application window is blocking them', async () => { 32 | const window = new ApplicationWindow() 33 | window.canReceiveEvents = jest.fn(() => false) 34 | 35 | const emitter = new ProjectEventEmitter(window) 36 | emitter.emitToRenderer('biscuit') 37 | expect(window.send).not.toHaveBeenCalled() 38 | }) 39 | -------------------------------------------------------------------------------- /tests/lib/frameworks/factory.spec.js: -------------------------------------------------------------------------------- 1 | import { ApplicationWindow } from '@main/application-window' 2 | import { FrameworkFactory } from '@lib/frameworks/factory' 3 | import { PHPUnit } from '@lib/frameworks/phpunit/framework' 4 | 5 | jest.mock('@lib/state') 6 | jest.mock('electron-store') 7 | jest.mock('@main/application-window') 8 | 9 | it('can make a new framework', async () => { 10 | const window = new ApplicationWindow() 11 | const options = { 12 | id: '42', 13 | type: 'phpunit' 14 | } 15 | 16 | const framework = FrameworkFactory.make(window, options) 17 | expect(framework).toBeInstanceOf(PHPUnit) 18 | // Persists id 19 | expect(framework.id).toBe('42') 20 | // Hydrates with default options, including proprietary and OS-specific 21 | expect(framework.name).toBe('PHPUnit (Legacy)') 22 | expect(framework.proprietary).toEqual({ 23 | autoloadPath: '' 24 | }) 25 | expect(framework.command).toBe( 26 | __WIN32__ 27 | ? 'php vendor/phpunit/phpunit/phpunit' 28 | : './vendor/bin/phpunit' 29 | ) 30 | }) 31 | 32 | it('fails if type does not exist', async () => { 33 | const window = new ApplicationWindow() 34 | const options = { 35 | type: 'biscuit' 36 | } 37 | 38 | expect(() => { 39 | FrameworkFactory.make(window, options) 40 | }).toThrow('Unknown framework type "biscuit"') 41 | }) 42 | -------------------------------------------------------------------------------- /tests/lib/helpers/durations.spec.js: -------------------------------------------------------------------------------- 1 | import Durations from '@lib/helpers/durations' 2 | 3 | const helper = new Durations() 4 | 5 | it('formats durations', () => { 6 | expect(helper.format(0)).toBe('0ms') 7 | expect(helper.format(1)).toBe('1ms') 8 | expect(helper.format(166)).toBe('166ms') 9 | expect(helper.format(3660)).toBe('3.66s') 10 | expect(helper.format(300000)).toBe('5 min') 11 | expect(helper.format(300003)).toBe('5 min 3ms') 12 | expect(helper.format(303303)).toBe('5 min 3s') 13 | expect(helper.format(34500000)).toBe('9 hours 35 min') 14 | expect(helper.format(34500003)).toBe('9 hours 35 min') 15 | expect(helper.format(34503303)).toBe('9 hours 35 min 3s') 16 | expect(helper.format(134500000)).toBe('1 day 13 hours 21 min 40s') 17 | expect(helper.format(134500003)).toBe('1 day 13 hours 21 min 40s') 18 | expect(helper.format(134503303)).toBe('1 day 13 hours 21 min 43s') 19 | expect(helper.format(2134503303)).toBe('24 days 16 hours 55 min 3s') 20 | expect(helper.format(52134503303)).toBe('603 days 9 hours 48 min 23s') 21 | expect(helper.format(995213400000)).toBe('11518 days 16 hours 10 min') 22 | }) 23 | -------------------------------------------------------------------------------- /tests/lib/process/pool.spec.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import pool from '@lib/process/pool' 3 | 4 | beforeEach(() => { 5 | pool.clear() 6 | }) 7 | 8 | it('can pool processes without specifying id', () => { 9 | const spawned = { 10 | getId: jest.fn().mockReturnValue(7), 11 | on: jest.fn() 12 | } 13 | pool.add(spawned) 14 | expect(pool.processes[7]).toBe(spawned) 15 | expect(spawned.getId).toHaveBeenCalledTimes(1) 16 | expect(spawned.on).toHaveBeenCalledTimes(1) 17 | }) 18 | 19 | it('does not pool processes if it cannot figure out the process id', () => { 20 | const spawned = { 21 | getId: jest.fn().mockReturnValue(null), 22 | on: jest.fn() 23 | } 24 | pool.add(spawned) 25 | expect(pool.processes).toEqual({}) 26 | expect(spawned.getId).toHaveBeenCalledTimes(1) 27 | expect(spawned.on).not.toHaveBeenCalled() 28 | }) 29 | 30 | it('pools processes with a given id', () => { 31 | const spawned = { 32 | getId: jest.fn(), 33 | on: jest.fn() 34 | } 35 | pool.add(spawned, 11) 36 | expect(pool.processes[11]).toBe(spawned) 37 | expect(spawned.getId).not.toHaveBeenCalled() 38 | expect(spawned.on).toHaveBeenCalledTimes(1) 39 | }) 40 | 41 | it('can find process in the current pool', () => { 42 | const spawned = { 43 | on: jest.fn() 44 | } 45 | pool.add(spawned, 11) 46 | expect(pool.findProcess(11)).toBe(spawned) 47 | }) 48 | 49 | it('removes processes from the pool when they close', () => { 50 | const spawned = new EventEmitter() 51 | 52 | pool.add(spawned, 11) 53 | expect(pool.processes[11]).toBe(spawned) 54 | 55 | spawned.emit('close') 56 | expect(pool.processes[11]).toBe(undefined) 57 | }) 58 | 59 | it('can handle removed processes on close', () => { 60 | const spawned = new EventEmitter() 61 | 62 | pool.add(spawned, 11) 63 | expect(pool.processes[11]).toBe(spawned) 64 | pool.clear() 65 | expect(pool.processes[11]).toBe(undefined) 66 | 67 | spawned.emit('close') 68 | }) 69 | -------------------------------------------------------------------------------- /tests/mocks/electron.js: -------------------------------------------------------------------------------- 1 | const electron = { 2 | ipcRenderer: { 3 | listeners: { 4 | on: {}, 5 | once: {} 6 | }, 7 | on (event, callback) { 8 | electron.ipcRenderer.listeners.on[event] = callback 9 | }, 10 | once (event, callback) { 11 | electron.ipcRenderer.listeners.once[event] = callback 12 | }, 13 | send (channel, ...args) {}, 14 | invoke (channel, ...args) {}, 15 | removeAllListeners (channel) {}, 16 | 17 | /** 18 | * Mimick a channel event coming from the main process. 19 | * 20 | * @param channel The channel in which the event is being triggered 21 | * @param args The arguments with which the event is being triggered 22 | */ 23 | trigger (channel, ...args) { 24 | if (typeof electron.ipcRenderer.listeners.on[channel] === 'undefined') { 25 | throw Error(`Attempted to trigger unregistered event "${channel}"`) 26 | } 27 | 28 | const event = {} 29 | electron.ipcRenderer.listeners.on[channel](event, ...args) 30 | if (typeof electron.ipcRenderer.listeners.once[channel] !== 'undefined') { 31 | electron.ipcRenderer.listeners.once[channel](event, ...args) 32 | delete electron.ipcRenderer.listeners.once[channel] 33 | } 34 | } 35 | } 36 | } 37 | 38 | module.exports = electron 39 | -------------------------------------------------------------------------------- /tests/mocks/setup.js: -------------------------------------------------------------------------------- 1 | global.log = { 2 | debug: () => {}, 3 | info: () => {}, 4 | warn: () => {}, 5 | error: () => {} 6 | } 7 | -------------------------------------------------------------------------------- /tests/renderer/components/TestInformation.spec.js: -------------------------------------------------------------------------------- 1 | import { config, shallowMount } from '@vue/test-utils' 2 | import TestInformation from '@/components/TestInformation' 3 | import Strings from '@/plugins/strings' 4 | 5 | config.global.plugins = [new Strings()] 6 | 7 | const RealDate = Date.now 8 | beforeAll(() => { 9 | global.Date.now = jest.fn(() => new Date('2020-12-01T14:49:00').getTime()) 10 | }) 11 | afterAll(() => { 12 | global.Date.now = RealDate 13 | }) 14 | 15 | test.each([ 16 | ['never run', { 17 | first: '1985-10-21T09:00:00' 18 | }], 19 | ['dates only', { 20 | first: '1985-10-21T09:00:00', 21 | last: '2015-10-21T13:00:00' 22 | }], 23 | ['with duration', { 24 | first: '2020-09-10T13:00:00', 25 | last: '2020-11-21T13:00:00', 26 | duration: 1000 27 | }], 28 | ['with long duration', { 29 | first: '2020-12-01T10:20:00', 30 | last: '2020-12-01T14:47:00', 31 | duration: 32198 32 | }], 33 | ['with assertions', { 34 | first: '2020-11-21T13:00:00', 35 | last: '2020-12-01T14:48:33', 36 | assertions: 42 37 | }] 38 | ])('matches snapshot for stats: "%s"', async (name, stats) => { 39 | const wrapper = shallowMount(TestInformation, { 40 | propsData: { 41 | stats 42 | } 43 | }) 44 | expect(wrapper.html()).toMatchSnapshot() 45 | }) 46 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | // Force all tests to run in UTC timezone (i.e. same as our pipelines). 3 | process.env.TZ = 'UTC' 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "target": "ES2020", 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noUnusedLocals": true, 11 | "strict": true, 12 | "sourceMap": true, 13 | "jsx": "preserve", 14 | "outDir": "./dist", 15 | "types" : [ "node", "lodash" ], 16 | "typeRoots": [ 17 | "./node_modules/@types", 18 | "./src/types" 19 | ], 20 | "baseUrl": "./src", 21 | "paths": { 22 | "@/*": ["./renderer/*"], 23 | "@main/*": ["./main/*"], 24 | "@lib/*": ["./lib/*"] 25 | }, 26 | "allowJs": true, 27 | "lib": [ 28 | "DOM", 29 | "ES2020" 30 | ] 31 | }, 32 | "exclude": [ 33 | "node_modules", 34 | "build" 35 | ], 36 | "include": [ 37 | "src/**/*.ts" 38 | ], 39 | "compileOnSave": false 40 | } 41 | -------------------------------------------------------------------------------- /workflow/app-info.js: -------------------------------------------------------------------------------- 1 | const s = JSON.stringify 2 | 3 | module.exports.getReplacements = function () { 4 | return { 5 | __DARWIN__: process.platform === 'darwin', 6 | __WIN32__: process.platform === 'win32', 7 | __LINUX__: process.platform === 'linux', 8 | __DEV__: process.env.IS_DEV || false, 9 | __LOGGER__: process.env.LOGGER !== 'false', 10 | 'process.platform': s(process.platform), 11 | 'process.env.NODE_ENV': s(process.env.NODE_ENV || 'development'), 12 | 'process.env.TEST_ENV': s(process.env.TEST_ENV), 13 | __CRASH_URL__: s('https://71e593620d21420fb864d3fe667d8ce6@sentry.io/1476972'), 14 | __ANALYTICS_ID__: s('UA-103701546-4') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /workflow/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const del = require('del') 6 | const webpack = require('webpack') 7 | 8 | const mainConfig = require('./webpack.main.config') 9 | const preloadConfig = require('./webpack.preload.config') 10 | const rendererConfig = require('./webpack.renderer.config') 11 | 12 | build() 13 | 14 | function build () { 15 | del.sync(['dist/*', '!.gitkeep']) 16 | pack(mainConfig, 'main') 17 | pack(preloadConfig, 'preload') 18 | pack(rendererConfig, 'renderer') 19 | } 20 | 21 | function pack (config, input) { 22 | return new Promise((resolve, reject) => { 23 | config.mode = process.env.NODE_ENV === 'development' ? 'development' : 'production' 24 | config.plugins = [...config.plugins, new webpack.ProgressPlugin({ 25 | handler (percentage, msg) { 26 | console.log(`${input}/${msg}: ${(percentage * 100).toFixed()}%`) 27 | } 28 | })] 29 | webpack(config, (err, stats) => { 30 | if (err) reject(err.stack || err) 31 | else if (stats.hasErrors()) { 32 | let err = '' 33 | 34 | stats.toString({ 35 | chunks: false, 36 | colors: true 37 | }) 38 | .split(/\r?\n/) 39 | .forEach(line => { 40 | err += ` ${line}\n` 41 | }) 42 | 43 | reject(err) 44 | } else { 45 | resolve(stats.toString({ 46 | chunks: false, 47 | colors: true 48 | })) 49 | } 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /workflow/clean-build.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const Fs = require('fs-extra') 3 | 4 | const buildPath = Path.join(__dirname, '../build') 5 | Fs.removeSync(buildPath) 6 | Fs.mkdirsSync(buildPath) 7 | -------------------------------------------------------------------------------- /workflow/clean.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const Fs = require('fs-extra') 3 | 4 | const distPath = Path.join(__dirname, '../dist') 5 | const electronPath = Path.join(__dirname, '../dist') 6 | Fs.removeSync(Path.join(__dirname, '../static/reporters')) 7 | Fs.removeSync(Path.join(__dirname, '../support/icons')) 8 | Fs.removeSync(Path.join(__dirname, '../src/lib/reporters/phpunit/bootstrap')) 9 | Fs.removeSync(Path.join(__dirname, '../src/lib/reporters/phpunit-10/bootstrap')) 10 | Fs.removeSync(Path.join(__dirname, '../build/mac')) 11 | Fs.removeSync(Path.join(__dirname, '../build/win-unpacked')) 12 | Fs.removeSync(distPath) 13 | Fs.mkdirsSync(distPath) 14 | Fs.mkdirsSync(electronPath) 15 | Fs.closeSync(Fs.openSync(Path.join(electronPath, '.gitkeep'), 'w')) 16 | -------------------------------------------------------------------------------- /workflow/cypress.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Path = require('node:path') 4 | const { exec } = require('child_process') 5 | let callbackId = null 6 | 7 | const teardown = (code = 0) => { 8 | if (callbackId) { 9 | try { 10 | // Try to kill child process, but don't 11 | // throw if it no longer exists 12 | process.kill(-callbackId) 13 | } catch (_) {} 14 | } 15 | process.exit(code) 16 | } 17 | 18 | if (process.platform === 'win32') { 19 | const rl = require('readline').createInterface({ 20 | input: process.stdin, 21 | output: process.stdout 22 | }) 23 | 24 | rl.on('SIGINT', () => { 25 | process.emit('SIGINT') 26 | }) 27 | } 28 | 29 | process.on('SIGINT', () => { 30 | teardown() 31 | }) 32 | 33 | const startRenderer = require('./runners').startRenderer 34 | startRenderer().then(() => { 35 | const callback = exec( 36 | process.argv[2] === 'open' 37 | ? `cypress open ${process.argv.slice(3).join(' ')} --config-file ${Path.resolve(__dirname, '../tests/cypress/config.ts')}` 38 | : `cypress run -b electron ${process.argv.slice(3).join(' ')} --config-file ${Path.resolve(__dirname, '../tests/cypress/config.ts')}`, 39 | { 40 | env: { 41 | ...process.env, 42 | FORCE_COLOR: 3 43 | } 44 | } 45 | ) 46 | callbackId = callback.pid 47 | callback.stdout.setEncoding('utf8') 48 | callback.stderr.setEncoding('utf8') 49 | callback.stdout.pipe(process.stdout) 50 | callback.stderr.pipe(process.stderr) 51 | callback.on('error', (...args) => { 52 | teardown(...args) 53 | }) 54 | callback.on('close', (...args) => { 55 | teardown(...args) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /workflow/decipher.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs-extra') 2 | const Crypto = require('crypto') 3 | 4 | if (process.argv.length < 3) { 5 | throw Error('Missing project.db file path.') 6 | } 7 | 8 | const encryptionAlgorithm = 'aes-256-cbc' 9 | const encryptionKey = 'v1' 10 | const path = process.argv[2] 11 | 12 | // Decipher encrypted project.db files 13 | let data = Fs.readFileSync(path, null) 14 | const initializationVector = data.slice(0, 16) 15 | const password = Crypto.pbkdf2Sync(encryptionKey, initializationVector.toString(), 10000, 32, 'sha512') 16 | const decipher = Crypto.createDecipheriv(encryptionAlgorithm, password, initializationVector) 17 | data = Buffer.concat([decipher.update(data.slice(17)), decipher.final()]) 18 | 19 | console.log(JSON.stringify(JSON.parse(data))) 20 | -------------------------------------------------------------------------------- /workflow/icons.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const Fs = require('fs-extra') 3 | 4 | Fs.copySync(Path.join(__dirname, '../static/icons'), Path.join(__dirname, '../build/icons')) 5 | Fs.copySync(Path.join(__dirname, '../static/icons'), Path.join(__dirname, '../support/icons')) 6 | -------------------------------------------------------------------------------- /workflow/release-after.js: -------------------------------------------------------------------------------- 1 | // Nothing to do. 2 | -------------------------------------------------------------------------------- /workflow/release-before.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const Fs = require('fs-extra') 3 | const builder = require('../electron-builder.js') 4 | const chalk = require('chalk') 5 | 6 | const releaseNotesPath = Path.join(__dirname, `../${builder.directories.buildResources}/release-notes.md`) 7 | if (!Fs.existsSync(releaseNotesPath)) { 8 | console.log(`\n${chalk.bgRed.white(' NO RELEASE NOTES ')} No release notes file found inside the buildResources directory. Please add it and try again.\n`) 9 | process.exit(1) 10 | } 11 | -------------------------------------------------------------------------------- /workflow/reporters/phpunit.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const Fs = require('fs-extra') 3 | 4 | Fs.mkdirpSync(Path.join(__dirname, '../../static/reporters')) 5 | Fs.copySync(Path.join(__dirname, '../../src/lib/reporters/phpunit'), Path.join(__dirname, '../../static/reporters/phpunit')) 6 | Fs.copySync(Path.join(__dirname, '../../src/lib/reporters/phpunit-10'), Path.join(__dirname, '../../static/reporters/phpunit-10')) 7 | -------------------------------------------------------------------------------- /workflow/reporters/webpack.jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'reporters' 4 | 5 | const { getReplacements } = require('../app-info') 6 | const replacements = getReplacements() 7 | 8 | const path = require('path') 9 | const webpack = require('webpack') 10 | 11 | const config = { 12 | target: 'node', 13 | entry: { 14 | main: path.join(__dirname, '../../src/lib/reporters/jest/index.js') 15 | }, 16 | output: { 17 | filename: 'index.js', 18 | library: { 19 | type: 'commonjs2' 20 | }, 21 | path: path.join(__dirname, '../../static/reporters/jest') 22 | }, 23 | resolve: { 24 | extensions: ['.js'] 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js$/, 30 | use: 'babel-loader', 31 | exclude: /node_modules/ 32 | } 33 | ] 34 | }, 35 | node: { 36 | __dirname: process.env.NODE_ENV !== 'production', 37 | __filename: process.env.NODE_ENV !== 'production' 38 | }, 39 | plugins: [ 40 | new webpack.NoEmitOnErrorsPlugin(), 41 | new webpack.DefinePlugin(replacements) 42 | ], 43 | optimization: { 44 | minimize: process.env.NODE_ENV === 'production' 45 | }, 46 | } 47 | 48 | if (process.env.NODE_ENV === 'production') { 49 | config.plugins.push( 50 | new webpack.DefinePlugin({ 51 | 'process.env.NODE_ENV': '"production"' 52 | }) 53 | ) 54 | } 55 | 56 | module.exports = config 57 | -------------------------------------------------------------------------------- /workflow/run-dev.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const init = require('./runners').init 4 | init() 5 | -------------------------------------------------------------------------------- /workflow/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const ESLintPlugin = require('eslint-webpack-plugin') 6 | const StyleLintPlugin = require('stylelint-webpack-plugin') 7 | 8 | module.exports = { 9 | output: { 10 | filename: '[name].js', 11 | library: { 12 | type: 'umd' 13 | }, 14 | path: path.join(__dirname, '../dist') 15 | }, 16 | resolve: { 17 | alias: { 18 | '@': path.join(__dirname, '../src/renderer'), 19 | '@lib': path.join(__dirname, '../src/lib'), 20 | '@main': path.join(__dirname, '../src/main'), 21 | 'vue$': 'vue/dist/vue.esm-bundler.js' 22 | }, 23 | extensions: ['.js', '.ts'] 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.ts?$/, 29 | loader: 'ts-loader', 30 | options: { 31 | appendTsSuffixTo: [/\.vue$/] 32 | }, 33 | exclude: /node_modules/ 34 | }, 35 | { 36 | test: /\.js$/, 37 | use: 'babel-loader', 38 | exclude: /node_modules/ 39 | }, 40 | { 41 | test: /\.js$/, 42 | use: 'source-map-loader', 43 | enforce: 'pre' 44 | } 45 | ] 46 | }, 47 | optimization: { 48 | minimize: false, 49 | removeEmptyChunks: true 50 | }, 51 | devtool: process.env.NODE_ENV !== 'production' ? 'source-map' : false, 52 | plugins: [ 53 | new ESLintPlugin({ 54 | quiet: true 55 | }), 56 | new StyleLintPlugin({ 57 | files: ['src/**/*.{vue,scss}'] 58 | }), 59 | new webpack.DefinePlugin({ 60 | __VUE_OPTIONS_API__: true, 61 | __VUE_PROD_DEVTOOLS__: false 62 | }) 63 | ], 64 | stats: 'errors-only' 65 | } 66 | -------------------------------------------------------------------------------- /workflow/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const { getReplacements } = require('./app-info') 6 | 7 | const base = require('./webpack.base.config.js') 8 | const _ = require('lodash') 9 | const path = require('path') 10 | const { dependencies } = require('../package.json') 11 | const webpack = require('webpack') 12 | 13 | const mainConfig = { 14 | ...base, 15 | target: 'electron-main', 16 | entry: { 17 | main: path.join(__dirname, '../src/main/index.ts') 18 | }, 19 | externals: [ 20 | ...Object.keys(dependencies || {}) 21 | ], 22 | node: { 23 | __dirname: process.env.NODE_ENV !== 'production', 24 | __filename: process.env.NODE_ENV !== 'production' 25 | }, 26 | plugins: [ 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | new webpack.DefinePlugin(Object.assign({}, getReplacements(), { 29 | __PROCESS_KIND__: JSON.stringify('main') 30 | })) 31 | ] 32 | } 33 | 34 | if (process.env.NODE_ENV !== 'production' || process.env.IS_DEV) { 35 | mainConfig.plugins.push( 36 | new webpack.DefinePlugin({ 37 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 38 | }) 39 | ) 40 | } 41 | 42 | if (process.env.NODE_ENV === 'production') { 43 | Array.prototype.push.apply(mainConfig.plugins, _.compact([ 44 | new webpack.DefinePlugin({ 45 | 'process.env.NODE_ENV': '"production"' 46 | }) 47 | ])) 48 | } 49 | 50 | module.exports = mainConfig 51 | -------------------------------------------------------------------------------- /workflow/webpack.preload.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'preload' 4 | 5 | const base = require('./webpack.base.config.js') 6 | const { merge } = require('webpack-merge') 7 | const path = require('path') 8 | 9 | const preloadConfig = merge(base, { 10 | target: 'electron-preload', 11 | entry: { 12 | preload: path.join(__dirname, '../src/preload/index.ts') 13 | } 14 | }) 15 | 16 | module.exports = preloadConfig 17 | --------------------------------------------------------------------------------