Initial commit

This commit is contained in:
Dan Funk 2023-05-11 14:49:37 -04:00 committed by GitHub
commit 9d39fb0fb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 13376 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
# EditorConfig is awesome: http://EditorConfig.org
# https://github.com/jokeyrhyme/standard-editorconfig
# top-most EditorConfig file
root = true
# defaults
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 2
indent_style = space
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,29 @@
/**
* TODO: Rewrite this config to ESM
* But currently electron-builder doesn't support ESM configs
* @see https://github.com/develar/read-config-file/issues/10
*/
/**
* @type {() => import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
module.exports = async function () {
const {getVersion} = await import('./version/getVersion.mjs');
return {
directories: {
output: 'dist',
buildResources: 'buildResources',
},
files: ['packages/**/dist/**'],
extraMetadata: {
version: getVersion(),
},
// Specify linux target just for disabling snap compilation
linux: {
target: 'deb',
},
};
};

51
.eslintrc.json Normal file
View File

@ -0,0 +1,51 @@
{
"root": true,
"env": {
"es2021": true,
"node": true,
"browser": false
},
"extends": [
"eslint:recommended",
/** @see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#recommended-configs */
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["node_modules/**", "**/dist/**"],
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/consistent-type-imports": "error",
/**
* Having a semicolon helps the optimizer interpret your code correctly.
* This avoids rare errors in optimized code.
* @see https://twitter.com/alex_kozack/status/1364210394328408066
*/
"semi": ["error", "always"],
/**
* This will make the history of changes in the hit a little cleaner
*/
"comma-dangle": ["warn", "always-multiline"],
/**
* Just for beauty
*/
"quotes": [
"warn",
"single",
{
"avoidEscape": true
}
]
}
}

4
.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
.github/actions/**/*.js linguist-detectable=false
scripts/*.js linguist-detectable=false
*.config.js linguist-detectable=false
* text=auto eol=lf

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: ["https://www.buymeacoffee.com/kozack/", "https://send.monobank.ua/6SmojkkR9i"]

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: cawa-93
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions & Discussions
url: https://github.com/cawa-93/vite-electron-builder/discussions/categories/q-a
about: Use GitHub discussions for message-board style questions and discussions.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: cawa-93
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

27
.github/renovate.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
"extends": [
"config:base",
":semanticCommits",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled",
":automergeAll",
":automergeBranch",
":disableDependencyDashboard",
":pinVersions",
":onlyNpm",
":label(dependencies)"
],
"packageRules": [
{
"groupName": "Vite packages",
"matchUpdateTypes": "major",
"matchSourceUrlPrefixes": [
"https://github.com/vitejs/"
]
}
],
"gitNoVerify": [
"commit",
"push"
]
}

47
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,47 @@
# This workflow is the entry point for all CI processes.
# It is from here that all other workflows are launched.
on:
workflow_dispatch:
push:
branches:
- main
- 'renovate/**'
paths-ignore:
- '.github/**'
- '!.github/workflows/ci.yml'
- '!.github/workflows/typechecking.yml'
- '!.github/workflows/tests.yml'
- '!.github/workflows/release.yml'
- '**.md'
- .editorconfig
- .gitignore
- '.idea/**'
- '.vscode/**'
pull_request:
paths-ignore:
- '.github/**'
- '!.github/workflows/ci.yml'
- '!.github/workflows/typechecking.yml'
- '!.github/workflows/tests.yml'
- '!.github/workflows/release.yml'
- '**.md'
- .editorconfig
- .gitignore
- '.idea/**'
- '.vscode/**'
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
typechecking:
uses: ./.github/workflows/typechecking.yml
tests:
uses: ./.github/workflows/tests.yml
draft_release:
with:
dry-run: ${{ github.event_name != 'push' || github.ref_name != 'main' }}
needs: [ typechecking, tests ]
uses: ./.github/workflows/release.yml

65
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,65 @@
on:
workflow_dispatch:
push:
paths:
- '**.js'
- '**.mjs'
- '**.cjs'
- '**.jsx'
- '**.ts'
- '**.mts'
- '**.cts'
- '**.tsx'
- '**.vue'
- '**.json'
pull_request:
paths:
- '**.js'
- '**.mjs'
- '**.cjs'
- '**.jsx'
- '**.ts'
- '**.mts'
- '**.cts'
- '**.tsx'
- '**.vue'
- '**.json'
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16 # Need for npm >=7.7
cache: 'npm'
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run lint --if-present
# This job just check code style for in-template contributions.
code-style:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16 # Need for npm >=7.7
cache: 'npm'
- run: npm i prettier
- run: npx prettier --check "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,json}"

61
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: Release
on:
workflow_call:
inputs:
dry-run:
description: 'Compiles the app but not upload artifacts to distribution server'
default: false
required: false
type: boolean
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
draft_release:
strategy:
fail-fast: true
matrix:
os: [ macos-latest, ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16 # Need for npm >=7.7
cache: 'npm'
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build
- name: Compile artifacts ${{ inputs.dry-run && '' || 'and upload them to github release' }}
# I use this action because it is capable of retrying multiple times if there are any issues with the distribution server
uses: nick-fields/retry@v2
with:
timeout_minutes: 15
max_attempts: 6
retry_wait_seconds: 15
retry_on: error
shell: 'bash'
command: npx --no-install electron-builder --config .electron-builder.config.js --publish ${{ inputs.dry-run && 'never' || 'always' }}
env:
# Code Signing params
# See https://www.electron.build/code-signing
# CSC_LINK: ''
# CSC_KEY_PASSWORD: ''
# Publishing artifacts
GH_TOKEN: ${{ secrets.github_token }} # GitHub token, automatically provided (No need to define this secret in the repo settings)

38
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Tests
on: [ workflow_call ]
concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
tests:
strategy:
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run test:main --if-present
- run: npm run test:preload --if-present
- run: npm run test:renderer --if-present
# I ran into problems trying to run an electron window in ubuntu due to a missing graphics server.
# That's why this special command for Ubuntu is here
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e --if-present
if: matrix.os == 'ubuntu-latest'
- run: npm run test:e2e --if-present
if: matrix.os != 'ubuntu-latest'

27
.github/workflows/typechecking.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Typechecking
on: [ workflow_call ]
concurrency:
group: typechecking-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16 # Need for npm >=7.7
cache: 'npm'
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run typecheck --if-present

58
.gitignore vendored Normal file
View File

@ -0,0 +1,58 @@
node_modules
.DS_Store
dist
*.local
thumbs.db
.eslintcache
.browserslistrc
.electron-vendors.cache.json
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
.idea/artifacts
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/*.iml
.idea/modules
*.iml
*.ipr
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# Editor-based Rest Client
.idea/httpRequests
/.idea/csv-plugin.xml

View File

@ -0,0 +1,65 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<HTMLCodeStyleSettings>
<option name="HTML_ATTRIBUTE_WRAP" value="4" />
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_TYPE_BRACES" value="false" />
</JSCodeStyleSettings>
<JSON>
<option name="OBJECT_WRAPPING" value="5" />
<option name="ARRAY_WRAPPING" value="5" />
</JSON>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_TYPE_BRACES" value="false" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="UNIFORM_INDENT" value="false" />
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

28
.idea/deployment.xml Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="ihappymama-aliexpress">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="iosico.com">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="somespeed.com">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<work-dir-patterns value="src/**/*.{ts,vue} {bin,config}/**/*.js" />
<option name="fix-on-save" value="true" />
</component>
</project>

28
.idea/jsonSchemas.xml Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JsonSchemaMappingsProjectConfiguration">
<state>
<map>
<entry key="GitHub Workflow">
<value>
<SchemaInfo>
<option name="name" value="GitHub Workflow" />
<option name="relativePathToSchema" value="https://json.schemastore.org/github-workflow.json" />
<option name="applicationDefined" value="true" />
<option name="patterns">
<list>
<Item>
<option name="path" value=".github/workflows/release.yml" />
</Item>
<Item>
<option name="path" value=".github/workflows/ci.yml" />
</Item>
</list>
</option>
</SchemaInfo>
</value>
</entry>
</map>
</state>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/vite-electron-builder.iml" filepath="$PROJECT_DIR$/.idea/vite-electron-builder.iml" />
</modules>
</component>
</project>

8
.idea/prettier.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myRunOnSave" value="true" />
<option name="myRunOnReformat" value="true" />
<option name="myFilesPattern" value="{**/*,*}.{js,mjs,cjs,ts,mts,cts,vue,json}" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/packages/main/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/packages/preload/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/renderer/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/packages/renderer/dist" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/main/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/preload/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/preload/node_modules" />
<excludeFolder url="file://$MODULE_DIR$/packages/main/node_modules" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

14
.idea/webResources.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/packages/renderer/assets" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

24
.nano-staged.mjs Normal file
View File

@ -0,0 +1,24 @@
import {resolve, sep} from 'path';
export default {
'*.{js,mjs,cjs,ts,mts,cts,vue}': 'eslint --cache --fix',
/**
* Run typechecking if any type-sensitive files or project dependencies was changed
* @param {string[]} filenames
* @return {string[]}
*/
'{package-lock.json,packages/**/{*.ts,*.vue,tsconfig.json}}': ({filenames}) => {
// if dependencies was changed run type checking for all packages
if (filenames.some(f => f.endsWith('package-lock.json'))) {
return ['npm run typecheck --if-present'];
}
// else run type checking for staged packages
const fileNameToPackageName = filename =>
filename.replace(resolve(process.cwd(), 'packages') + sep, '').split(sep)[0];
return [...new Set(filenames.map(fileNameToPackageName))].map(
p => `npm run typecheck:${p} --if-present`,
);
},
};

1
.npmrc Normal file
View File

@ -0,0 +1 @@
auto-install-peers=true

10
.prettierignore Normal file
View File

@ -0,0 +1,10 @@
**/node_modules
**/dist
**/*.svg
package.json
package-lock.json
.electron-vendors.cache.json
.github
.idea

21
.prettierrc Normal file
View File

@ -0,0 +1,21 @@
{
"printWidth": 100,
"semi": true,
"singleQuote": true,
"overrides": [
{
"files": ["**/*.css", "**/*.scss", "**/*.html"],
"options": {
"singleQuote": false
}
}
],
"trailingComma": "all",
"bracketSpacing": false,
"arrowParens": "avoid",
"proseWrap": "never",
"htmlWhitespaceSensitivity": "strict",
"vueIndentScriptAndStyle": false,
"endOfLine": "lf",
"singleAttributePerLine": true
}

3
.simple-git-hooks.json Normal file
View File

@ -0,0 +1,3 @@
{
"pre-commit": "npx nano-staged"
}

13
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Main Process",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}\\scripts\\watch.mjs",
"autoAttachChildProcesses": true
}
]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Alex Kozack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

301
README.md Normal file
View File

@ -0,0 +1,301 @@
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua)
---
# Vite Electron Builder Boilerplate
This is a template for secure electron applications. Written following the latest safety requirements, recommendations
and best practices.
Under the hood is [Vite] — A next-generation blazing fast bundler, and [electron-builder] for packaging.
## Get started
Follow these steps to get started with the template:
1. Click the **[Use this template](https://github.com/cawa-93/vite-electron-builder/generate)** button (you must be
logged in) or just clone this repo.
2. If you want to use another package manager you may need to edit [`.github/workflows`](/.github/workflows) — [it
uses `npm` by default](https://github.com/search?q=npm+repo%3Acawa-93%2Fvite-electron-builder+path%3A.github%2Fworkflows&type=Code&ref=advsearch&l=&l=).
3. If you like this template, don't forget to give a github star or send support! ⭐♥
That's all you need. 😉
> **Note**:
> This template uses npm v7 feature — [**Installing Peer Dependencies
Automatically**](https://github.com/npm/rfcs/blob/latest/implemented/0025-install-peer-deps.md). If you are using a
different package manager, you may need to install some peerDependencies manually.
## Features
### Electron [![Electron version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/electron?label=%20)][electron]
- This template uses the latest electron version with all the latest security patches.
- The architecture of the application is built according to the
security [guides](https://www.electronjs.org/docs/tutorial/security) and best practices.
- The latest version of the [electron-builder] is used to package the application.
### Vite [![Vite version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vite?label=%20)][vite]
- [Vite] is used to bundle all source codes. It's an extremely fast bundler, that has a vast array of amazing features.
You can learn more about how it is arranged in [this](https://www.youtube.com/watch?v=xXrhg26VCSc) video.
- Vite [supports](https://vitejs.dev/guide/env-and-mode.html) reading `.env` files. You can also specify the types of
your environment variables in [`types/env.d.ts`](types/env.d.ts).
- Automatic hot-reloads for the `Main` and `Renderer` processes.
Vite provides many useful features, such as: `TypeScript`, `TSX/JSX`, `CSS/JSON Importing`, `CSS Modules`
, `Web Assembly` and much more.
> [See all Vite features](https://vitejs.dev/guide/features.html).
### TypeScript [![TypeScript version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/typescript?label=%20)][typescript] (optional)
- The latest version of TypeScript is used for all the source code.
- **Vite** supports TypeScript out of the box. However, it does not support type checking.
- Code formatting rules follow the latest TypeScript recommendations and best practices thanks
to [@typescript-eslint/eslint-plugin](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin).
> [Guide to disable typescript and remove dependencies](https://github.com/cawa-93/vite-electron-builder/discussions/339)
### Vue [![Vue version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vue?label=%20&)][vue] (optional)
- By default, web pages are built using [Vue]. However, you can easily change that. Or not use additional frameworks at
all.
- Code formatting rules follow the latest Vue recommendations and best practices thanks to [eslint-plugin-vue].
> [Find more forks 🔱 for others frameworks or setups](https://github.com/cawa-93/vite-electron-builder/discussions/categories/forks)
### Continuous Integration
- The configured workflow will check the types for each push and PR.
- The configured workflow will check the code style for each push and PR.
- **Automatic tests**
used [Vitest ![Vitest version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/vitest?label=%20&color=yellow)][vitest]
-- A blazing fast test framework powered by Vite.
- Unit tests are placed within each package and are ran separately.
- End-to-end tests are placed in the root [`tests`](tests) directory and use [playwright].
![Workflow graph](https://user-images.githubusercontent.com/1662812/213429323-ef4bcc87-c273-4f2f-b77f-c04cf6dbc36d.png)
### Publishing
- Each time you push changes to the `main` branch, the [`release`](.github/workflows/release.yml) workflow starts, which creates a new draft release. For each next commit will be created and replaced artifacts. That way you will always have draft with latest artifacts, and the release can be published once it is ready.
- Code signing supported. See [`release` workflow](.github/workflows/release.yml).
- **Auto-update is supported**. After the release is published, all client applications will download the new version
and install updates silently.
> **Note**:
> This template **configured only for GitHub public repository**, but electron-builder also supports other update distribution servers. Find more in [electron-builder docs](https://www.electron.build/configuration/publish).
## How it works
The template requires a minimum amount [dependencies](package.json). Only **Vite** is used for building, nothing more.
### Project Structure
The structure of this template is very similar to a monorepo. The entire source code of the project is divided into three modules (packages) that are each bundled independently:
- [`packages/renderer`](packages/renderer). Responsible for the contents of the application window. In fact, it is a
regular web application. In developer mode, you can even open it in a browser. The development and build process is
the same as for classic web applications. Access to low-level API electrons or Node.js is done through the _preload_
layer.
- [`packages/preload`](packages/preload). Contain Electron [**preload scripts**](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload). Acts as an intermediate bridge between the _renderer_ process and the API
exposed by electron and Node.js. Runs in an _isolated browser context_, but has direct access to the full Node.js
functionality.
- [`packages/main`](packages/main)
Contain Electron [**main script**](https://www.electronjs.org/docs/tutorial/quick-start#create-the-main-script-file). This is
the main process that powers the application. It manages creating and handling the spawned BrowserWindow, setting and
enforcing secure permissions and request handlers. You can also configure it to do much more as per your need, such
as: logging, reporting statistics and health status among others.
Schematically, the structure of the application and the method of communication between packages can be depicted as follows:
```mermaid
flowchart TB;
packages/preload <-. IPC Messages .-> packages/main
subgraph packages/main["packages/main (Shared beatween all windows)"]
M[index.ts] --> EM[Electron Main Process Modules]
M --> N2[Node.js API]
end
subgraph Window["Browser Window"]
subgraph packages/preload["packages/preload (Works in isolated context)"]
P[index.ts] --> N[Node.js API]
P --> ED[External dependencies]
P --> ER[Electron Renderer Process Modules]
end
subgraph packages/renderer
R[index.html] --> W[Web API]
R --> BD[Bundled dependencies]
R --> F[Web Frameforks]
end
end
packages/renderer -- Call Exposed API --> P
```
### Build web resources
The `main` and `preload` packages are built in [library mode](https://vitejs.dev/guide/build.html#library-mode) as it is
simple javascript.
The `renderer` package builds as a regular web app.
### Compile App
The next step is to package a ready to distribute Electron app for macOS, Windows and Linux with "auto update" support
out of the box.
To do this, use [electron-builder]:
- Using the npm script `compile`: This script is configured to compile the application as quickly as possible. It is not
ready for distribution, it is compiled only for the current platform and is used for debugging.
- Using GitHub Actions: The application is compiled for any platform and ready-to-distribute files are automatically
added as a draft to the GitHub releases page.
### Working with dependencies
Because the `renderer` works and builds like a _regular web application_, you can only use dependencies that support the
browser or compile to a browser-friendly format.
This means that in the `renderer` you are free to use any frontend dependencies such as Vue, React, lodash, axios and so
on. However, you _CANNOT_ use any native Node.js APIs, such as, `systeminformation`. These APIs are _only_ available in
a Node.js runtime environment and will cause your application to crash if used in the `renderer` layer. Instead, if you
need access to Node.js runtime APIs in your frontend, export a function form the `preload` package.
All dependencies that require Node.js api can be used in
the [`preload` script](https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts).
#### Expose in main world
Here is an example. Let's say you need to read some data from the file system or database in the renderer.
In the preload context, create a function that reads and returns data. To make the function announced in the preload
available in the render, you usually need to call
the [`electron.contextBridge.exposeInMainWorld`](https://www.electronjs.org/ru/docs/latest/api/context-bridge). However,
this template uses the [unplugin-auto-expose](https://github.com/cawa-93/unplugin-auto-expose) plugin, so you just need
to export the method from the preload. The `exposeInMainWorld` will be called automatically.
```ts
// preload/index.ts
import { readFile } from 'node:fs/promises';
// Encapsulate types if you use typescript
interface UserData {
prop: string
}
// Encapsulate all node.js api
// Everything you exported from preload/index.ts may be called in renderer
export function getUserData(): Promise<UserData> {
return readFile('/path/to/file/in/user/filesystem.json', {encoding:'utf8'}).then(JSON.parse);
}
```
Now you can import and call the method in renderer
```ts
// renderer/anywere/component.ts
import { getUserData } from '#preload'
const userData = await getUserData()
```
> Find more in [Context Isolation tutorial](https://www.electronjs.org/docs/tutorial/context-isolation#security-considerations).
### Working with Electron API
Although the preload has access to all of Node.js's API, it **still runs in the BrowserWindow context**, so a limited
electron modules are available in it. Check the [electron docs](https://www.electronjs.org/ru/docs/latest/api/clipboard)
for full list of available methods.
All other electron methods can be invoked in the `main`.
As a result, the architecture of interaction between all modules is as follows:
```mermaid
sequenceDiagram
renderer->>+preload: Read data from file system
preload->>-renderer: Data
renderer->>preload: Maximize window
activate preload
preload-->>main: Invoke IPC command
activate main
main-->>preload: IPC response
deactivate main
preload->>renderer: Window maximized
deactivate preload
```
> Find more in [Inter-Process Communication tutorial](https://www.electronjs.org/docs/latest/tutorial/ipc).
### Modes and Environment Variables
All environment variables are set as part of the `import.meta`, so you can access them vie the following
way: `import.meta.env`.
> **Note**:
> If you are using TypeScript and want to get code completion you must add all the environment variables to
the [`ImportMetaEnv` in `types/env.d.ts`](types/env.d.ts).
The mode option is used to specify the value of `import.meta.env.MODE` and the corresponding environment variables files
that need to be loaded.
By default, there are two modes:
- `production` is used by default
- `development` is used by `npm run watch` script
When running the build script, the environment variables are loaded from the following files in your project root:
```
.env # loaded in all cases
.env.local # loaded in all cases, ignored by git
.env.[mode] # only loaded in specified env mode
.env.[mode].local # only loaded in specified env mode, ignored by git
```
> **Warning**:
> To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your
Vite-processed code.
For example let's take the following `.env` file:
```
DB_PASSWORD=foobar
VITE_SOME_KEY=123
```
Only `VITE_SOME_KEY` will be exposed as `import.meta.env.VITE_SOME_KEY` to your client source code, but `DB_PASSWORD`
will not.
You can change that prefix or add another. See [`envPrefix`](https://vitejs.dev/config/shared-options.html#envprefix)
## Contribution
See [Contributing Guide](contributing.md).
[vite]: https://github.com/vitejs/vite/
[electron]: https://github.com/electron/electron
[electron-builder]: https://github.com/electron-userland/electron-builder
[vue]: https://github.com/vuejs/vue-next
[vue-router]: https://github.com/vuejs/vue-router-next/
[typescript]: https://github.com/microsoft/TypeScript/
[playwright]: https://playwright.dev
[vitest]: https://vitest.dev
[vue-tsc]: https://github.com/johnsoncodehk/vue-tsc
[eslint-plugin-vue]: https://github.com/vuejs/eslint-plugin-vue
[cawa-93-github]: https://github.com/cawa-93/
[cawa-93-sponsor]: https://www.patreon.com/Kozack/

0
buildResources/.gitkeep Normal file
View File

BIN
buildResources/icon.icns Normal file

Binary file not shown.

BIN
buildResources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

34
contributing.md Normal file
View File

@ -0,0 +1,34 @@
# Contributing
First and foremost, thank you! We appreciate that you want to contribute to vite-electron-builder, your time is
valuable, and your contributions mean a lot to us.
## Issues
Do not create issues about bumping dependencies unless a bug has been identified, and you can demonstrate that it
effects this library.
**Help us to help you**
Remember that were here to help, but not to make guesses about what you need help with:
- Whatever bug or issue you're experiencing, assume that it will not be as obvious to the maintainers as it is to you.
- Spell it out completely. Keep in mind that maintainers need to think about _all potential use cases_ of a library.
It's important that you explain how you're using a library so that maintainers can make that connection and solve the
issue.
_It can't be understated how frustrating and draining it can be to maintainers to have to ask clarifying questions on
the most basic things, before it's even possible to start debugging. Please try to make the best use of everyone's time
involved, including yourself, by providing this information up front._
## Repo Setup
The package manager used to install and link dependencies must be npm v7 or later.
1. Clone repo
1. `npm run watch` start electron app in watch mode.
1. `npm run compile` build app but for local debugging only.
1. `npm run lint` lint your code.
1. `npm run typecheck` Run typescript check.
1. `npm run test` Run app test.
1. `npm run format` Reformat all codebase to project code style.

11053
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
package.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "vite-electron-builder",
"description": "Secure boilerplate for Electron app based on Vite",
"version": "1.0.2",
"private": true,
"author": {
"email": "kozackunisoft@gmail.com",
"name": "Alex Kozack",
"url": "https://kozack.me"
},
"main": "packages/main/dist/index.cjs",
"scripts": {
"build": "npm run build:main && npm run build:preload && npm run build:renderer",
"build:main": "cd ./packages/main && vite build",
"build:preload": "cd ./packages/preload && vite build",
"build:renderer": "cd ./packages/renderer && vite build",
"compile": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.js --dir --config.asar=false",
"test": "npm run test:main && npm run test:preload && npm run test:renderer && npm run test:e2e",
"test:e2e": "npm run build && vitest run",
"test:main": "vitest run -r packages/main --passWithNoTests",
"test:preload": "vitest run -r packages/preload --passWithNoTests",
"test:renderer": "vitest run -r packages/renderer --passWithNoTests",
"watch": "node scripts/watch.mjs",
"lint": "eslint . --ext js,mjs,cjs,ts,mts,cts,vue",
"typecheck:main": "tsc --noEmit -p packages/main/tsconfig.json",
"typecheck:preload": "tsc --noEmit -p packages/preload/tsconfig.json",
"typecheck:renderer": "vue-tsc --noEmit -p packages/renderer/tsconfig.json",
"typecheck": "npm run typecheck:main && npm run typecheck:preload && npm run typecheck:renderer",
"postinstall": "cross-env ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs",
"format": "npx prettier --write \"**/*.{js,mjs,cjs,ts,mts,cts,vue,json}\""
},
"devDependencies": {
"@types/node": "18.16.7",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@vitejs/plugin-vue": "4.2.2",
"@vue/test-utils": "2.3.2",
"cross-env": "7.0.3",
"electron": "24.3.0",
"electron-builder": "23.6.0",
"eslint": "8.40.0",
"eslint-plugin-vue": "9.11.1",
"happy-dom": "9.10.9",
"nano-staged": "0.8.0",
"playwright": "1.33.0",
"simple-git-hooks": "2.8.1",
"typescript": "5.0.4",
"unplugin-auto-expose": "0.0.4",
"vite": "4.3.5",
"vitest": "0.31.0",
"vue": "3.2.47",
"vue-tsc": "1.6.4"
},
"dependencies": {
"electron-updater": "5.3.0"
}
}

View File

@ -0,0 +1,86 @@
import {app} from 'electron';
import './security-restrictions';
import {restoreOrCreateWindow} from '/@/mainWindow';
import {platform} from 'node:process';
/**
* Prevent electron from running multiple instances.
*/
const isSingleInstance = app.requestSingleInstanceLock();
if (!isSingleInstance) {
app.quit();
process.exit(0);
}
app.on('second-instance', restoreOrCreateWindow);
/**
* Disable Hardware Acceleration to save more system resources.
*/
app.disableHardwareAcceleration();
/**
* Shout down background process if all windows was closed
*/
app.on('window-all-closed', () => {
if (platform !== 'darwin') {
app.quit();
}
});
/**
* @see https://www.electronjs.org/docs/latest/api/app#event-activate-macos Event: 'activate'.
*/
app.on('activate', restoreOrCreateWindow);
/**
* Create the application window when the background process is ready.
*/
app
.whenReady()
.then(restoreOrCreateWindow)
.catch(e => console.error('Failed create window:', e));
/**
* Install Vue.js or any other extension in development mode only.
* Note: You must install `electron-devtools-installer` manually
*/
// if (import.meta.env.DEV) {
// app
// .whenReady()
// .then(() => import('electron-devtools-installer'))
// .then(module => {
// const {default: installExtension, VUEJS3_DEVTOOLS} =
// // @ts-expect-error Hotfix for https://github.com/cawa-93/vite-electron-builder/issues/915
// typeof module.default === 'function' ? module : (module.default as typeof module);
//
// return installExtension(VUEJS3_DEVTOOLS, {
// loadExtensionOptions: {
// allowFileAccess: true,
// },
// });
// })
// .catch(e => console.error('Failed install extension:', e));
// }
/**
* Check for app updates, install it in background and notify user that new version was installed.
* No reason run this in non-production build.
* @see https://www.electron.build/auto-update.html#quick-setup-guide
*
* Note: It may throw "ENOENT: no such file app-update.yml"
* if you compile production app without publishing it to distribution server.
* Like `npm run compile` does. It's ok 😅
*/
if (import.meta.env.PROD) {
app
.whenReady()
.then(() => import('electron-updater'))
.then(module => {
const autoUpdater =
module.autoUpdater ||
// @ts-expect-error Hotfix for https://github.com/electron-userland/electron-builder/issues/7338
(module.default.autoUpdater as (typeof module)['autoUpdater']);
return autoUpdater.checkForUpdatesAndNotify();
})
.catch(e => console.error('Failed check and install updates:', e));
}

View File

@ -0,0 +1,71 @@
import {app, BrowserWindow} from 'electron';
import {join, resolve} from 'node:path';
async function createWindow() {
const browserWindow = new BrowserWindow({
show: false, // Use the 'ready-to-show' event to show the instantiated BrowserWindow.
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: false, // Sandbox disabled because the demo of preload script depend on the Node.js api
webviewTag: false, // The webview tag is not recommended. Consider alternatives like an iframe or Electron's BrowserView. @see https://www.electronjs.org/docs/latest/api/webview-tag#warning
preload: join(app.getAppPath(), 'packages/preload/dist/index.cjs'),
},
});
/**
* If the 'show' property of the BrowserWindow's constructor is omitted from the initialization options,
* it then defaults to 'true'. This can cause flickering as the window loads the html content,
* and it also has show problematic behaviour with the closing of the window.
* Use `show: false` and listen to the `ready-to-show` event to show the window.
*
* @see https://github.com/electron/electron/issues/25012 for the afford mentioned issue.
*/
browserWindow.on('ready-to-show', () => {
browserWindow?.show();
if (import.meta.env.DEV) {
browserWindow?.webContents.openDevTools();
}
});
/**
* Load the main page of the main window.
*/
if (import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined) {
/**
* Load from the Vite dev server for development.
*/
await browserWindow.loadURL(import.meta.env.VITE_DEV_SERVER_URL);
} else {
/**
* Load from the local file system for production and test.
*
* Use BrowserWindow.loadFile() instead of BrowserWindow.loadURL() for WhatWG URL API limitations
* when path contains special characters like `#`.
* Let electron handle the path quirks.
* @see https://github.com/nodejs/node/issues/12682
* @see https://github.com/electron/electron/issues/6869
*/
await browserWindow.loadFile(resolve(__dirname, '../../renderer/dist/index.html'));
}
return browserWindow;
}
/**
* Restore an existing BrowserWindow or Create a new BrowserWindow.
*/
export async function restoreOrCreateWindow() {
let window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed());
if (window === undefined) {
window = await createWindow();
}
if (window.isMinimized()) {
window.restore();
}
window.focus();
}

View File

@ -0,0 +1,128 @@
import type {Session} from 'electron';
import {app, shell} from 'electron';
import {URL} from 'node:url';
/**
* Union for all existing permissions in electron
*/
type Permission = Parameters<
Exclude<Parameters<Session['setPermissionRequestHandler']>[0], null>
>[1];
/**
* A list of origins that you allow open INSIDE the application and permissions for them.
*
* In development mode you need allow open `VITE_DEV_SERVER_URL`.
*/
const ALLOWED_ORIGINS_AND_PERMISSIONS = new Map<string, Set<Permission>>(
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL
? [[new URL(import.meta.env.VITE_DEV_SERVER_URL).origin, new Set()]]
: [],
);
/**
* A list of origins that you allow open IN BROWSER.
* Navigation to the origins below is only possible if the link opens in a new window.
*
* @example
* <a
* target="_blank"
* href="https://github.com/"
* >
*/
const ALLOWED_EXTERNAL_ORIGINS = new Set<`https://${string}`>(['https://github.com']);
app.on('web-contents-created', (_, contents) => {
/**
* Block navigation to origins not on the allowlist.
*
* Navigation exploits are quite common. If an attacker can convince the app to navigate away from its current page,
* they can possibly force the app to open arbitrary web resources/websites on the web.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
*/
contents.on('will-navigate', (event, url) => {
const {origin} = new URL(url);
if (ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
return;
}
// Prevent navigation
event.preventDefault();
if (import.meta.env.DEV) {
console.warn(`Blocked navigating to disallowed origin: ${origin}`);
}
});
/**
* Block requests for disallowed permissions.
* By default, Electron will automatically approve all permission requests.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
*/
contents.session.setPermissionRequestHandler((webContents, permission, callback) => {
const {origin} = new URL(webContents.getURL());
const permissionGranted = !!ALLOWED_ORIGINS_AND_PERMISSIONS.get(origin)?.has(permission);
callback(permissionGranted);
if (!permissionGranted && import.meta.env.DEV) {
console.warn(`${origin} requested permission for '${permission}', but was rejected.`);
}
});
/**
* Hyperlinks leading to allowed sites are opened in the default browser.
*
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
* You should deny any unexpected window creation.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
*/
contents.setWindowOpenHandler(({url}) => {
const {origin} = new URL(url);
if (ALLOWED_EXTERNAL_ORIGINS.has(origin as `https://${string}`)) {
// Open url in default browser.
shell.openExternal(url).catch(console.error);
} else if (import.meta.env.DEV) {
console.warn(`Blocked the opening of a disallowed origin: ${origin}`);
}
// Prevent creating a new window.
return {action: 'deny'};
});
/**
* Verify webview options before creation.
*
* Strip away preload scripts, disable Node.js integration, and ensure origins are on the allowlist.
*
* @see https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
*/
contents.on('will-attach-webview', (event, webPreferences, params) => {
const {origin} = new URL(params.src);
if (!ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) {
if (import.meta.env.DEV) {
console.warn(`A webview tried to attach ${params.src}, but was blocked.`);
}
event.preventDefault();
return;
}
// Strip away preload scripts if unused or verify their location is legitimate.
delete webPreferences.preload;
// @ts-expect-error `preloadURL` exists. - @see https://www.electronjs.org/docs/latest/api/web-contents#event-will-attach-webview
delete webPreferences.preloadURL;
// Disable Node.js integration
webPreferences.nodeIntegration = false;
// Enable contextIsolation
webPreferences.contextIsolation = true;
});
});

View File

@ -0,0 +1,80 @@
import type {MockedClass, MockedObject} from 'vitest';
import {beforeEach, expect, test, vi} from 'vitest';
import {restoreOrCreateWindow} from '../src/mainWindow';
import {BrowserWindow} from 'electron';
/**
* Mock real electron BrowserWindow API
*/
vi.mock('electron', () => {
// Use "as unknown as" because vi.fn() does not have static methods
const bw = vi.fn() as unknown as MockedClass<typeof BrowserWindow>;
bw.getAllWindows = vi.fn(() => bw.mock.instances);
bw.prototype.loadURL = vi.fn((_: string, __?: Electron.LoadURLOptions) => Promise.resolve());
bw.prototype.loadFile = vi.fn((_: string, __?: Electron.LoadFileOptions) => Promise.resolve());
// Use "any" because the on function is overloaded
// eslint-disable-next-line @typescript-eslint/no-explicit-any
bw.prototype.on = vi.fn<any>();
bw.prototype.destroy = vi.fn();
bw.prototype.isDestroyed = vi.fn();
bw.prototype.isMinimized = vi.fn();
bw.prototype.focus = vi.fn();
bw.prototype.restore = vi.fn();
const app: Pick<Electron.App, 'getAppPath'> = {
getAppPath(): string {
return '';
},
};
return {BrowserWindow: bw, app};
});
beforeEach(() => {
vi.clearAllMocks();
});
test('Should create a new window', async () => {
const {mock} = vi.mocked(BrowserWindow);
expect(mock.instances).toHaveLength(0);
await restoreOrCreateWindow();
expect(mock.instances).toHaveLength(1);
const instance = mock.instances[0] as MockedObject<BrowserWindow>;
const loadURLCalls = instance.loadURL.mock.calls.length;
const loadFileCalls = instance.loadFile.mock.calls.length;
expect(loadURLCalls + loadFileCalls).toBe(1);
if (loadURLCalls === 1) {
expect(instance.loadURL).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/));
} else {
expect(instance.loadFile).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/));
}
});
test('Should restore an existing window', async () => {
const {mock} = vi.mocked(BrowserWindow);
// Create a window and minimize it.
await restoreOrCreateWindow();
expect(mock.instances).toHaveLength(1);
const appWindow = vi.mocked(mock.instances[0]);
appWindow.isMinimized.mockReturnValueOnce(true);
await restoreOrCreateWindow();
expect(mock.instances).toHaveLength(1);
expect(appWindow.restore).toHaveBeenCalledOnce();
});
test('Should create a new window if the previous one was destroyed', async () => {
const {mock} = vi.mocked(BrowserWindow);
// Create a window and destroy it.
await restoreOrCreateWindow();
expect(mock.instances).toHaveLength(1);
const appWindow = vi.mocked(mock.instances[0]);
appWindow.isDestroyed.mockReturnValueOnce(true);
await restoreOrCreateWindow();
expect(mock.instances).toHaveLength(2);
});

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": false,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"types": ["node"],
"baseUrl": ".",
"paths": {
"/@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "../../types/**/*.d.ts"],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
}

View File

@ -0,0 +1,43 @@
import {node} from '../../.electron-vendors.cache.json';
import {join} from 'node:path';
import {injectAppVersion} from '../../version/inject-app-version-plugin.mjs';
const PACKAGE_ROOT = __dirname;
const PROJECT_ROOT = join(PACKAGE_ROOT, '../..');
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: PROJECT_ROOT,
resolve: {
alias: {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
build: {
ssr: true,
sourcemap: 'inline',
target: `node${node}`,
outDir: 'dist',
assetsDir: '.',
minify: process.env.MODE !== 'development',
lib: {
entry: 'src/index.ts',
formats: ['cjs'],
},
rollupOptions: {
output: {
entryFileNames: '[name].cjs',
},
},
emptyOutDir: true,
reportCompressedSize: false,
},
plugins: [injectAppVersion()],
};
export default config;

View File

@ -0,0 +1,6 @@
/**
* @module preload
*/
export {sha256sum} from './nodeCrypto';
export {versions} from './versions';

View File

@ -0,0 +1,5 @@
import {type BinaryLike, createHash} from 'node:crypto';
export function sha256sum(data: BinaryLike) {
return createHash('sha256').update(data).digest('hex');
}

View File

@ -0,0 +1 @@
export {versions} from 'node:process';

View File

@ -0,0 +1,15 @@
import {createHash} from 'crypto';
import {expect, test} from 'vitest';
import {sha256sum, versions} from '../src';
test('versions', async () => {
expect(versions).toBe(process.versions);
});
test('nodeCrypto', async () => {
// Test hashing a random string.
const testString = Math.random().toString(36).slice(2, 7);
const expectedHash = createHash('sha256').update(testString).digest('hex');
expect(sha256sum(testString)).toBe(expectedHash);
});

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": false,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"types": ["node"],
"baseUrl": "."
},
"include": ["src/**/*.ts", "../../types/**/*.d.ts"],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
}

View File

@ -0,0 +1,39 @@
import {chrome} from '../../.electron-vendors.cache.json';
import {preload} from 'unplugin-auto-expose';
import {join} from 'node:path';
import {injectAppVersion} from '../../version/inject-app-version-plugin.mjs';
const PACKAGE_ROOT = __dirname;
const PROJECT_ROOT = join(PACKAGE_ROOT, '../..');
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: PROJECT_ROOT,
build: {
ssr: true,
sourcemap: 'inline',
target: `chrome${chrome}`,
outDir: 'dist',
assetsDir: '.',
minify: process.env.MODE !== 'development',
lib: {
entry: 'src/index.ts',
formats: ['cjs'],
},
rollupOptions: {
output: {
entryFileNames: '[name].cjs',
},
},
emptyOutDir: true,
reportCompressedSize: false,
},
plugins: [preload.vite(), injectAppVersion()],
};
export default config;

View File

@ -0,0 +1,20 @@
{
"env": {
"browser": true,
"node": false
},
"extends": [
/** @see https://eslint.vuejs.org/rules/ */
"plugin:vue/vue3-recommended"
],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
/** These rules are disabled because they are incompatible with prettier */
"vue/html-self-closing": "off",
"vue/singleline-html-element-content-newline": "off"
}
}

View File

@ -0,0 +1,20 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z"
fill="url(#paint0_linear)"/>
<path
d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z"
fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="script-src 'self' blob:" http-equiv="Content-Security-Policy">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script src="./src/index.ts" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,69 @@
<script lang="ts" setup>
import ReactiveCounter from '/@/components/ReactiveCounter.vue';
import ReactiveHash from '/@/components/ReactiveHash.vue';
import ElectronVersions from '/@/components/ElectronVersions.vue';
const APP_VERSION = import.meta.env.VITE_APP_VERSION;
</script>
<template>
<img
alt="Vue logo"
src="../assets/logo.svg"
width="150"
/>
<p>
<!-- Example how to inject current app version to UI -->
App version: {{ APP_VERSION }}
</p>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a
href="https://github.com/cawa-93/vite-electron-builder"
target="_blank"
>
vite-electron-builder documentation
</a>
.
</p>
<fieldset>
<legend>Test Vue Reactivity</legend>
<reactive-counter />
</fieldset>
<fieldset>
<legend>Test Node.js API</legend>
<reactive-hash />
</fieldset>
<fieldset>
<legend>Environment</legend>
<electron-versions />
</fieldset>
<p>
Edit
<code>packages/renderer/src/App.vue</code> to test hot module replacement.
</p>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin: 60px auto;
max-width: 700px;
}
fieldset {
margin: 2rem;
padding: 1rem;
}
</style>

View File

@ -0,0 +1,30 @@
<script lang="ts" setup>
import {versions} from '#preload';
</script>
<template>
<table id="process-versions">
<tr
v-for="(version, lib) in versions"
:key="lib"
>
<th>{{ lib }} :</th>
<td>v{{ version }}</td>
</tr>
</table>
<code>packages/renderer/src/components/ElectronVersions.vue</code>
</template>
<style scoped>
table {
margin: auto;
}
th {
text-align: right;
}
td {
text-align: left;
}
</style>

View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
import {ref} from 'vue';
const count = ref(0);
</script>
<template>
<button @click="count++"> count is: {{ count }}</button>
<br /><br />
<code>packages/renderer/src/components/ReactiveCounter.vue</code>
</template>

View File

@ -0,0 +1,53 @@
<script lang="ts" setup>
import {computed, ref} from 'vue';
import {sha256sum} from '#preload';
const rawString = ref('Hello World');
const hashedString = computed(() => sha256sum(rawString.value));
</script>
<template>
<table>
<tr>
<th>
<label for="reactive-hash-raw-value">Raw value :</label>
</th>
<td>
<input
id="reactive-hash-raw-value"
v-model="rawString"
type="text"
/>
</td>
</tr>
<tr>
<th>
<label for="reactive-hash-hashed-value">Hashed by node:crypto :</label>
</th>
<td>
<input
id="reactive-hash-hashed-value"
v-model="hashedString"
readonly
type="text"
/>
</td>
</tr>
</table>
<code>packages/renderer/src/components/ReactiveHash.vue</code>
</template>
<style scoped>
table {
margin: auto;
}
th {
text-align: right;
}
td {
text-align: left;
}
</style>

View File

@ -0,0 +1,4 @@
import {createApp} from 'vue';
import App from '/@/App.vue';
createApp(App).mount('#app');

View File

@ -0,0 +1,23 @@
import {mount} from '@vue/test-utils';
import {expect, test, vi} from 'vitest';
import ElectronVersions from '../src/components/ElectronVersions.vue';
vi.mock('#preload', () => {
return {
versions: {lib1: 1, lib2: 2},
};
});
test('ElectronVersions component', async () => {
expect(ElectronVersions).toBeTruthy();
const wrapper = mount(ElectronVersions);
const rows = wrapper.findAll<HTMLTableRowElement>('tr');
expect(rows.length).toBe(2);
expect(rows[0].find('th').text()).toBe('lib1 :');
expect(rows[0].find('td').text()).toBe('v1');
expect(rows[1].find('th').text()).toBe('lib2 :');
expect(rows[1].find('td').text()).toBe('v2');
});

View File

@ -0,0 +1,14 @@
import {mount} from '@vue/test-utils';
import {expect, test} from 'vitest';
import ReactiveCounter from '../src/components/ReactiveCounter.vue';
test('ReactiveHash component', async () => {
expect(ReactiveCounter).toBeTruthy();
const wrapper = mount(ReactiveCounter);
const button = wrapper.get('button');
expect(button.text()).toBe('count is: 0');
await button.trigger('click');
expect(button.text()).toBe('count is: 1');
});

View File

@ -0,0 +1,21 @@
import {mount} from '@vue/test-utils';
import {expect, test, vi} from 'vitest';
import ReactiveHash from '../src/components/ReactiveHash.vue';
vi.mock('#preload', () => {
return {
sha256sum: vi.fn((s: string) => `${s}:HASHED`),
};
});
test('ReactiveHash component', async () => {
expect(ReactiveHash).toBeTruthy();
const wrapper = mount(ReactiveHash);
const dataInput = wrapper.get<HTMLInputElement>('input:not([readonly])');
const hashInput = wrapper.get<HTMLInputElement>('input[readonly]');
const dataToHashed = Math.random().toString(36).slice(2, 7);
await dataInput.setValue(dataToHashed);
expect(hashInput.element.value).toBe(`${dataToHashed}:HASHED`);
});

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": false,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"jsx": "preserve",
"types": ["node"],
"baseUrl": ".",
"paths": {
"#preload": ["../preload/src/index"],
"/@/*": ["./src/*"]
},
"lib": ["ESNext", "dom", "dom.iterable"]
},
"include": [
"src/**/*.vue",
"src/**/*.ts",
"src/**/*.tsx",
"types/**/*.d.ts",
"../../types/**/*.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
}

View File

@ -0,0 +1,6 @@
declare module '*.vue' {
import type {DefineComponent} from 'vue';
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -0,0 +1,54 @@
/* eslint-env node */
import {chrome} from '../../.electron-vendors.cache.json';
import vue from '@vitejs/plugin-vue';
import {renderer} from 'unplugin-auto-expose';
import {join} from 'node:path';
import {injectAppVersion} from '../../version/inject-app-version-plugin.mjs';
const PACKAGE_ROOT = __dirname;
const PROJECT_ROOT = join(PACKAGE_ROOT, '../..');
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: PROJECT_ROOT,
resolve: {
alias: {
'/@/': join(PACKAGE_ROOT, 'src') + '/',
},
},
base: '',
server: {
fs: {
strict: true,
},
},
build: {
sourcemap: true,
target: `chrome${chrome}`,
outDir: 'dist',
assetsDir: '.',
rollupOptions: {
input: join(PACKAGE_ROOT, 'index.html'),
},
emptyOutDir: true,
reportCompressedSize: false,
},
test: {
environment: 'happy-dom',
},
plugins: [
vue(),
renderer.vite({
preloadEntry: join(PACKAGE_ROOT, '../preload/src/index.ts'),
}),
injectAppVersion(),
],
};
export default config;

View File

@ -0,0 +1,18 @@
/**
* This script should be run in electron context
* @example
* ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs
*/
import {writeFileSync} from 'fs';
import path from 'path';
const electronRelease = process.versions;
const node = electronRelease.node.split('.')[0];
const chrome = electronRelease.v8.split('.').splice(0, 2).join('');
const browserslistrcPath = path.resolve(process.cwd(), '.browserslistrc');
writeFileSync('./.electron-vendors.cache.json', JSON.stringify({chrome, node}));
writeFileSync(browserslistrcPath, `Chrome ${chrome}`, 'utf8');

104
scripts/watch.mjs Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env node
import {build, createServer} from 'vite';
import electronPath from 'electron';
import {spawn} from 'child_process';
/** @type 'production' | 'development'' */
const mode = (process.env.MODE = process.env.MODE || 'development');
/** @type {import('vite').LogLevel} */
const logLevel = 'warn';
/**
* Setup watcher for `main` package
* On file changed it totally re-launch electron app.
* @param {import('vite').ViteDevServer} watchServer Renderer watch server instance.
* Needs to set up `VITE_DEV_SERVER_URL` environment variable from {@link import('vite').ViteDevServer.resolvedUrls}
*/
function setupMainPackageWatcher({resolvedUrls}) {
process.env.VITE_DEV_SERVER_URL = resolvedUrls.local[0];
/** @type {ChildProcess | null} */
let electronApp = null;
return build({
mode,
logLevel,
configFile: 'packages/main/vite.config.js',
build: {
/**
* Set to {} to enable rollup watcher
* @see https://vitejs.dev/config/build-options.html#build-watch
*/
watch: {},
},
plugins: [
{
name: 'reload-app-on-main-package-change',
writeBundle() {
/** Kill electron if process already exist */
if (electronApp !== null) {
electronApp.removeListener('exit', process.exit);
electronApp.kill('SIGINT');
electronApp = null;
}
/** Spawn new electron process */
electronApp = spawn(String(electronPath), ['--inspect', '.'], {
stdio: 'inherit',
});
/** Stops the watch script when the application has been quit */
electronApp.addListener('exit', process.exit);
},
},
],
});
}
/**
* Setup watcher for `preload` package
* On file changed it reload web page.
* @param {import('vite').ViteDevServer} watchServer Renderer watch server instance.
* Required to access the web socket of the page. By sending the `full-reload` command to the socket, it reloads the web page.
*/
function setupPreloadPackageWatcher({ws}) {
return build({
mode,
logLevel,
configFile: 'packages/preload/vite.config.js',
build: {
/**
* Set to {} to enable rollup watcher
* @see https://vitejs.dev/config/build-options.html#build-watch
*/
watch: {},
},
plugins: [
{
name: 'reload-page-on-preload-package-change',
writeBundle() {
ws.send({
type: 'full-reload',
});
},
},
],
});
}
/**
* Dev server for Renderer package
* This must be the first,
* because the {@link setupMainPackageWatcher} and {@link setupPreloadPackageWatcher}
* depend on the dev server properties
*/
const rendererWatchServer = await createServer({
mode,
logLevel,
configFile: 'packages/renderer/vite.config.js',
}).then(s => s.listen());
await setupPreloadPackageWatcher(rendererWatchServer);
await setupMainPackageWatcher(rendererWatchServer);

94
tests/e2e.spec.ts Normal file
View File

@ -0,0 +1,94 @@
import type {ElectronApplication, JSHandle} from 'playwright';
import {_electron as electron} from 'playwright';
import {afterAll, beforeAll, expect, test} from 'vitest';
import {createHash} from 'crypto';
import type {BrowserWindow} from 'electron';
let electronApp: ElectronApplication;
beforeAll(async () => {
electronApp = await electron.launch({args: ['.']});
});
afterAll(async () => {
await electronApp.close();
});
test('Main window state', async () => {
const page = await electronApp.firstWindow();
const window: JSHandle<BrowserWindow> = await electronApp.browserWindow(page);
const windowState = await window.evaluate(
(mainWindow): Promise<{isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean}> => {
const getState = () => ({
isVisible: mainWindow.isVisible(),
isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
isCrashed: mainWindow.webContents.isCrashed(),
});
return new Promise(resolve => {
/**
* The main window is created hidden, and is shown only when it is ready.
* See {@link ../packages/main/src/mainWindow.ts} function
*/
if (mainWindow.isVisible()) {
resolve(getState());
} else mainWindow.once('ready-to-show', () => resolve(getState()));
});
},
);
expect(windowState.isCrashed, 'The app has crashed').toBeFalsy();
expect(windowState.isVisible, 'The main window was not visible').toBeTruthy();
expect(windowState.isDevToolsOpened, 'The DevTools panel was open').toBeFalsy();
});
test('Main window web content', async () => {
const page = await electronApp.firstWindow();
const element = await page.$('#app', {strict: true});
expect(element, 'Was unable to find the root element').toBeDefined();
expect((await element.innerHTML()).trim(), 'Window content was empty').not.equal('');
});
test('Preload versions', async () => {
const page = await electronApp.firstWindow();
const versionsElement = page.locator('#process-versions');
expect(await versionsElement.count(), 'expect find one element #process-versions').toStrictEqual(
1,
);
/**
* In this test we check only text value and don't care about formatting. That's why here we remove any space symbols
*/
const renderedVersions = (await versionsElement.innerText()).replace(/\s/g, '');
const expectedVersions = await electronApp.evaluate(() => process.versions);
for (const expectedVersionsKey in expectedVersions) {
expect(renderedVersions).include(
`${expectedVersionsKey}:v${expectedVersions[expectedVersionsKey]}`,
);
}
});
test('Preload nodeCrypto', async () => {
const page = await electronApp.firstWindow();
// Test hashing a random string
const testString = Math.random().toString(36).slice(2, 7);
const rawInput = page.locator('input#reactive-hash-raw-value');
expect(
await rawInput.count(),
'expect find one element input#reactive-hash-raw-value',
).toStrictEqual(1);
const hashedInput = page.locator('input#reactive-hash-hashed-value');
expect(
await hashedInput.count(),
'expect find one element input#reactive-hash-hashed-value',
).toStrictEqual(1);
await rawInput.fill(testString);
const renderedHash = await hashedInput.inputValue();
const expectedHash = createHash('sha256').update(testString).digest('hex');
expect(renderedHash).toEqual(expectedHash);
});

25
types/env.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
/// <reference types="vite/client" />
/**
* Describes all existing environment variables and their types.
* Required for Code completion/intellisense and type checking.
*
* Note: To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code.
*
* @see https://github.com/vitejs/vite/blob/0a699856b248116632c1ac18515c0a5c7cf3d1db/packages/vite/types/importMeta.d.ts#L7-L14 Base Interface.
* @see https://vitejs.dev/guide/env-and-mode.html#env-files Vite Env Variables Doc.
*/
interface ImportMetaEnv {
/**
* URL where `renderer` web page is running.
* This variable is initialized in scripts/watch.ts
*/
readonly VITE_DEV_SERVER_URL: undefined | string;
/** Current app version */
readonly VITE_APP_VERSION: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

9
version/getVersion.mjs Normal file
View File

@ -0,0 +1,9 @@
/**
* Entry function for get app version.
* In current implementation, it returns `version` from `package.json`, but you can implement any logic here.
* Runs several times for each vite configs and electron-builder config.
* @return {string}
*/
export function getVersion() {
return process.env.npm_package_version;
}

View File

@ -0,0 +1,13 @@
import {getVersion} from './getVersion.mjs';
/**
* Somehow inject app version to vite build context
* @return {import('vite').Plugin}
*/
export const injectAppVersion = () => ({
name: 'inject-version',
config: () => {
// TODO: Find better way to inject app version
process.env.VITE_APP_VERSION = getVersion();
},
});

23
vitest.config.js Normal file
View File

@ -0,0 +1,23 @@
/**
* Configuration for the global end-to-end testing,
* placed in the project's root 'tests' folder.
* @type {import('vite').UserConfig}
* @see https://vitest.dev/config/
*/
const config = {
test: {
/**
* By default, vitest searches for the test files in all packages.
* For e2e tests, have vitest search only in the project root 'tests' folder.
*/
include: ['./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
/**
* The default timeout of 5000ms is sometimes not enough for playwright.
*/
testTimeout: 30_000,
hookTimeout: 30_000,
},
};
export default config;