Initial commit

cordova
Valerie 2022-07-19 18:07:59 -04:00
commit d1de401ce4
68 changed files with 13457 additions and 0 deletions

18
.editorconfig 100644
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,23 @@
if (process.env.VITE_APP_VERSION === undefined) {
const now = new Date;
process.env.VITE_APP_VERSION = `${now.getUTCFullYear() - 2000}.${now.getUTCMonth() + 1}.${now.getUTCDate()}-${now.getUTCHours() * 60 + now.getUTCMinutes()}`;
}
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
const config = {
directories: {
output: 'dist',
buildResources: 'buildResources',
},
files: [
'packages/**/dist/**',
],
extraMetadata: {
version: process.env.VITE_APP_VERSION,
},
};
module.exports = config;

53
.eslintrc.json 100644
View File

@ -0,0 +1,53 @@
{
"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",
"@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"
]
}
}

3
.gitattributes vendored 100644
View File

@ -0,0 +1,3 @@
.github/actions/**/*.js linguist-detectable=false
scripts/*.js linguist-detectable=false
*.config.js linguist-detectable=false

4
.github/FUNDING.yml vendored 100644
View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
patreon: Kozack
open_collective: vite-electron-builder

View File

@ -0,0 +1,27 @@
---
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.

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.

View File

@ -0,0 +1,23 @@
name: 'Release Notes'
description: 'Return release notes based on Git Commits'
inputs:
from:
description: 'Commit from which start log'
required: true
to:
description: 'Commit to which end log'
required: true
include-commit-body:
description: 'Should the commit body be in notes'
required: false
default: 'false'
include-abbreviated-commit:
description: 'Should the commit sha be in notes'
required: false
default: 'true'
outputs:
release-note: # id of output
description: 'Release notes'
runs:
using: 'node12'
main: 'main.js'

View File

@ -0,0 +1,346 @@
// TODO: Refactor this action
const {execSync} = require('child_process');
/**
* Gets the value of an input. The value is also trimmed.
*
* @param name name of the input to get
* @param options optional. See InputOptions.
* @returns string
*/
function getInput(name, options) {
const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`);
}
return val.trim();
}
const START_FROM = getInput('from');
const END_TO = getInput('to');
const INCLUDE_COMMIT_BODY = getInput('include-commit-body') === 'true';
const INCLUDE_ABBREVIATED_COMMIT = getInput('include-abbreviated-commit') === 'true';
/**
* @typedef {Object} ICommit
* @property {string | undefined} abbreviated_commit
* @property {string | undefined} subject
* @property {string | undefined} body
*/
/**
* @typedef {ICommit & {type: string | undefined, scope: string | undefined}} ICommitExtended
*/
/**
* Any unique string that is guaranteed not to be used in committee text.
* Used to split data in the commit line
* @type {string}
*/
const commitInnerSeparator = '~~~~';
/**
* Any unique string that is guaranteed not to be used in committee text.
* Used to split each commit line
* @type {string}
*/
const commitOuterSeparator = '₴₴₴₴';
/**
* Commit data to be obtained.
* @type {Map<string, string>}
*
* @see https://git-scm.com/docs/git-log#Documentation/git-log.txt-emnem
*/
const commitDataMap = new Map([
['subject', '%s'], // Required
]);
if (INCLUDE_COMMIT_BODY) {
commitDataMap.set('body', '%b');
}
if (INCLUDE_ABBREVIATED_COMMIT) {
commitDataMap.set('abbreviated_commit', '%h');
}
/**
* The type used to group commits that do not comply with the convention
* @type {string}
*/
const fallbackType = 'other';
/**
* List of all desired commit groups and in what order to display them.
* @type {string[]}
*/
const supportedTypes = [
'feat',
'fix',
'perf',
'refactor',
'style',
'docs',
'test',
'build',
'ci',
'chore',
'revert',
'deps',
fallbackType,
];
/**
* @param {string} commitString
* @returns {ICommit}
*/
function parseCommit(commitString) {
/** @type {ICommit} */
const commitDataObj = {};
const commitDataArray =
commitString
.split(commitInnerSeparator)
.map(s => s.trim());
for (const [key] of commitDataMap) {
commitDataObj[key] = commitDataArray.shift();
}
return commitDataObj;
}
/**
* Returns an array of commits since the last git tag
* @return {ICommit[]}
*/
function getCommits() {
const format = Array.from(commitDataMap.values()).join(commitInnerSeparator) + commitOuterSeparator;
const logs = String(execSync(`git --no-pager log ${START_FROM}..${END_TO} --pretty=format:"${format}" --reverse`));
return logs
.trim()
.split(commitOuterSeparator)
.filter(r => !!r.trim()) // Skip empty lines
.map(parseCommit);
}
/**
*
* @param {ICommit} commit
* @return {ICommitExtended}
*/
function setCommitTypeAndScope(commit) {
const matchRE = new RegExp(`^(?:(${supportedTypes.join('|')})(?:\\((\\S+)\\))?:)?(.*)`, 'i');
let [, type, scope, clearSubject] = commit.subject.match(matchRE);
/**
* Additional rules for checking committees that do not comply with the convention, but for which it is possible to determine the type.
*/
// Commits like `revert something`
if (type === undefined && commit.subject.startsWith('revert')) {
type = 'revert';
}
return {
...commit,
type: (type || fallbackType).toLowerCase().trim(),
scope: (scope || '').toLowerCase().trim(),
subject: (clearSubject || commit.subject).trim(),
};
}
class CommitGroup {
constructor() {
this.scopes = new Map;
this.commits = [];
}
/**
*
* @param {ICommitExtended[]} array
* @param {ICommitExtended} commit
*/
static _pushOrMerge(array, commit) {
const similarCommit = array.find(c => c.subject === commit.subject);
if (similarCommit) {
if (commit.abbreviated_commit !== undefined) {
similarCommit.abbreviated_commit += `, ${commit.abbreviated_commit}`;
}
} else {
array.push(commit);
}
}
/**
* @param {ICommitExtended} commit
*/
push(commit) {
if (!commit.scope) {
CommitGroup._pushOrMerge(this.commits, commit);
return;
}
const scope = this.scopes.get(commit.scope) || {commits: []};
CommitGroup._pushOrMerge(scope.commits, commit);
this.scopes.set(commit.scope, scope);
}
get isEmpty() {
return this.commits.length === 0 && this.scopes.size === 0;
}
}
/**
* Groups all commits by type and scopes
* @param {ICommit[]} commits
* @returns {Map<string, CommitGroup>}
*/
function getGroupedCommits(commits) {
const parsedCommits = commits.map(setCommitTypeAndScope);
const types = new Map(
supportedTypes.map(id => ([id, new CommitGroup()])),
);
for (const parsedCommit of parsedCommits) {
const typeId = parsedCommit.type;
const type = types.get(typeId);
type.push(parsedCommit);
}
return types;
}
/**
* Return markdown list with commits
* @param {ICommitExtended[]} commits
* @param {string} pad
* @returns {string}
*/
function getCommitsList(commits, pad = '') {
let changelog = '';
for (const commit of commits) {
changelog += `${pad}- ${commit.subject}.`;
if (commit.abbreviated_commit !== undefined) {
changelog += ` (${commit.abbreviated_commit})`;
}
changelog += '\r\n';
if (commit.body === undefined) {
continue;
}
const body = commit.body.replace('[skip ci]', '').trim();
if (body !== '') {
changelog += `${
body
.split(/\r*\n+/)
.filter(s => !!s.trim())
.map(s => `${pad} ${s}`)
.join('\r\n')
}${'\r\n'}`;
}
}
return changelog;
}
function replaceHeader(str) {
switch (str) {
case 'feat':
return 'New Features';
case 'fix':
return 'Bug Fixes';
case 'docs':
return 'Documentation Changes';
case 'build':
return 'Build System';
case 'chore':
return 'Chores';
case 'ci':
return 'Continuous Integration';
case 'refactor':
return 'Refactors';
case 'style':
return 'Code Style Changes';
case 'test':
return 'Tests';
case 'perf':
return 'Performance improvements';
case 'revert':
return 'Reverts';
case 'deps':
return 'Dependency updates';
case 'other':
return 'Other Changes';
default:
return str;
}
}
/**
* Return markdown string with changelog
* @param {Map<string, CommitGroup>} groups
*/
function getChangeLog(groups) {
let changelog = '';
for (const [typeId, group] of groups) {
if (group.isEmpty) {
continue;
}
changelog += `### ${replaceHeader(typeId)}${'\r\n'}`;
for (const [scopeId, scope] of group.scopes) {
if (scope.commits.length) {
changelog += `- #### ${replaceHeader(scopeId)}${'\r\n'}`;
changelog += getCommitsList(scope.commits, ' ');
}
}
if (group.commits.length) {
changelog += getCommitsList(group.commits);
}
changelog += ('\r\n' + '\r\n');
}
return changelog.trim();
}
function escapeData(s) {
return String(s)
.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A');
}
try {
const commits = getCommits();
const grouped = getGroupedCommits(commits);
const changelog = getChangeLog(grouped);
process.stdout.write('::set-output name=release-note::' + escapeData(changelog) + '\r\n');
// require('fs').writeFileSync('../CHANGELOG.md', changelog, {encoding: 'utf-8'})
} catch (e) {
console.error(e);
process.exit(1);
}

40
.github/renovate.json vendored 100644
View File

@ -0,0 +1,40 @@
{
"extends": [
"config:base",
"group:allNonMajor",
":semanticCommits",
":automergeTypes",
":disableDependencyDashboard",
":pinVersions",
":semanticCommitType(deps)",
":onlyNpm",
":label(dependencies)",
"schedule:weekly"
],
"baseBranches": [
"main"
],
"bumpVersion": "patch",
"packageRules": [
{
"packageNames": [
"node",
"npm"
],
"enabled": false
},
{
"depTypeList": [
"devDependencies"
],
"semanticCommitType": "build"
},
{
"matchPackageNames": [
"electron"
],
"separateMajorMinor": false
}
]
}

29
.github/workflows/ci.yml vendored 100644
View File

@ -0,0 +1,29 @@
# This workflow is the entry point for all CI processes.
# It is from here that all other workflows are launched.
on:
push:
branches:
- main
- master
paths-ignore:
- '**.md'
- .editorconfig
- .gitignore
pull_request:
paths-ignore:
- '**.md'
- .editorconfig
- .gitignore
jobs:
lint:
uses: ./.github/workflows/lint.yml
typechecking:
uses: ./.github/workflows/typechecking.yml
tests:
uses: ./.github/workflows/tests.yml
draft_release:
if: github.event_name == 'push' && github.ref_name == 'main'
needs: [ typechecking, tests ]
uses: ./.github/workflows/release.yml

23
.github/workflows/lint.yml vendored 100644
View File

@ -0,0 +1,23 @@
name: Linters
on: [workflow_call]
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'
# TODO: Install not all dependencies, but only those required for this workflow
- name: Install dependencies
run: npm ci
- run: npm run lint --if-present

127
.github/workflows/release.yml vendored 100644
View File

@ -0,0 +1,127 @@
name: Release
on: [workflow_call, workflow_dispatch]
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
draft:
runs-on: ubuntu-latest
outputs:
release-note: ${{ steps.release-note.outputs.release-note }}
version: ${{ steps.version.outputs.build-version }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 14
- name: Get last git tag
id: tag
run: echo "::set-output name=last-tag::$(git describe --tags --abbrev=0 || git rev-list --max-parents=0 ${{github.ref}})"
- name: Generate release notes
uses: ./.github/actions/release-notes
id: release-note
with:
from: ${{ steps.tag.outputs.last-tag }}
to: ${{ github.ref }}
include-commit-body: true
include-abbreviated-commit: true
- name: Get version from current date
id: version
run: echo "::set-output name=build-version::$(node -e "try{console.log(require('./.electron-builder.config.js').extraMetadata.version)}catch(e){console.error(e);process.exit(1)}")"
- name: Delete outdated drafts
uses: hugo19941994/delete-draft-releases@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.github_token }}
with:
prerelease: true
draft: true
tag_name: v${{ steps.version.outputs.build-version }}
name: v${{ steps.version.outputs.build-version }}
body: ${{ steps.release-note.outputs.release-note }}
upload_artifacts:
needs: [ draft ]
strategy:
matrix:
os: [ windows-latest ]
# To compile the application for different platforms, use:
# 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'
- name: Install dependencies
run: npm ci
# The easiest way to transfer release notes to a compiled application is create `release-notes.md` in the build resources.
# See https://github.com/electron-userland/electron-builder/issues/1511#issuecomment-310160119
- name: Prepare release notes
env:
RELEASE_NOTE: ${{ needs.draft.outputs.release-note }}
run: echo "$RELEASE_NOTE" >> ./buildResources/release-notes.md
# Compile app and upload artifacts
- name: Compile & release Electron app
uses: samuelmeuli/action-electron-builder@v1
env:
VITE_APP_VERSION: ${{ needs.draft.outputs.version }}
with:
build_script_name: build
args: --config .electron-builder.config.js
# GitHub token, automatically provided to the action
# (No need to define this secret in the repo settings)
github_token: ${{ secrets.github_token }}
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: true
# Sometimes the build may fail due to a connection problem with Apple, GitHub, etc. servers.
# This option will restart the build as many attempts as possible
max_attempts: 3
# Code Signing params
# Base64-encoded code signing certificate for Windows
# windows_certs: ''
# Password for decrypting `windows_certs`
# windows_certs_password: ''
# Base64-encoded code signing certificate for macOS
# mac_certs: ''
# Password for decrypting `mac_certs`
# mac_certs_password: ''

44
.github/workflows/tests.yml vendored 100644
View File

@ -0,0 +1,44 @@
name: Tests
on: [workflow_call]
defaults:
run:
shell: 'bash'
jobs:
unit:
strategy:
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
package: [ main, preload, renderer ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
- run: npm ci
- run: npm run test:${{ matrix.package }} --if-present
e2e:
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
- run: npx playwright install --with-deps
- 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'

View File

@ -0,0 +1,24 @@
name: Typechecking
on: [workflow_call]
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'
# TODO: Install not all dependencies, but only those required for this workflow
- name: Install dependencies
run: npm ci
- run: npm run typecheck --if-present

58
.gitignore vendored 100644
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,16 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
</TypeScriptCodeStyleSettings>
</code_scheme>
</component>

View File

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

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>

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>

6
.idea/vcs.xml 100644
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,17 @@
<?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" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

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>

21
.nano-staged.mjs 100644
View File

@ -0,0 +1,21 @@
import {resolve, sep} from 'path';
export default {
'*.{js,ts,vue}': 'eslint --cache --fix',
/**
* Run typechecking if any type-sensitive files was staged
* @param {string[]} filenames
* @return {string[]}
*/
'packages/**/{*.ts,*.vue,tsconfig.json}': ({filenames}) => {
const pathToPackages = resolve(process.cwd(), 'packages') + sep;
return Array.from(
filenames.reduce((set, filename) => {
const pack = filename.replace(pathToPackages, '').split(sep)[0];
set.add(`npm run typecheck:${pack} --if-present`);
return set;
}, new Set),
);
},
};

View File

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

21
LICENSE 100644
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.

233
README.md 100644
View File

@ -0,0 +1,233 @@
# Vite Electron Builder Boilerplate
----
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua)
----
[![GitHub issues by-label](https://img.shields.io/github/issues/cawa-93/vite-electron-builder/help%20wanted?label=issues%20need%20help&logo=github)](https://github.com/cawa-93/vite-electron-builder/issues?q=label%3A%22help+wanted%22+is%3Aopen+is%3Aissue)
[![Required Node.JS >= v16.13](https://img.shields.io/static/v1?label=node&message=%3E=16.13&logo=node.js&color)](https://nodejs.org/about/releases/)
[![Required npm >= v8.1](https://img.shields.io/static/v1?label=npm&message=%3E=8.1&logo=npm&color)](https://github.com/npm/cli/releases)
> Vite+Electron = 🔥
This is template for secure electron applications. Written following the latest safety requirements, recommendations and best practices.
Under the hood is used [Vite] — superfast, nextgen bundler, and [electron-builder] for compilation.
## Get started
Follow these steps to get started with this 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 don't forget to edit [`.github/workflows`](/.github/workflows) -- it uses `npm` by default.
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.
**Note**: Find more useful forks [here](https://github.com/cawa-93/vite-electron-builder/discussions/categories/forks).
## 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 compile 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. This is an extremely fast packer that has a bunch of great features. You can learn more about how it is arranged in [this](https://youtu.be/xXrhg26VCSc) video.
- Vite [supports](https://vitejs.dev/guide/env-and-mode.html) reading `.env` files. You can also specify types of your environment variables in [`types/env.d.ts`](types/env.d.ts).
- Hot reloads for `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).
**[See this discussion](https://github.com/cawa-93/vite-electron-builder/discussions/339)** if you want completely remove TypeScript.
### Vue [![Vue version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/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].
- Installed [Vue.js devtools beta](https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg) with Vue 3 support.
See [examples of web pages for different frameworks](https://github.com/vitejs/vite/tree/main/packages/create-vite).
### 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 run separately.
- End-to-end tests are placed in the root [`tests`](tests) directory and use [playwright].
### Continuous delivery
- Each time you push changes to the `main` branch, the [`release`](.github/workflows/release.yml) workflow starts, which creates a release draft.
- The version is automatically set based on the current date in the format `yy.mm.dd-minutes`.
- Notes are automatically generated and added to the release draft.
- Code signing supported. See [`compile` job in the `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.
## 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 the structure of a monorepo.
```mermaid
flowchart TB;
packages/preload <-. IPC Messages .-> packages/main
subgraph packages/main
M[index.ts] --> EM[Electron Main Process Modules]
M --> N2[Node.js API]
end
subgraph packages/preload
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
packages/renderer -- Call Exposed API --> P
```
The entire source code of the program 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 through the _preload_ layer.
- [`packages/preload`](packages/preload). Acts as an intermediate link between the _renderer_ layer and the low-level API electrons or Node.js. Runs in an _isolated browser context_, but has direct access to Node.js api. See [Checklist: Security Recommendations](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content).
- [`packages/main`](packages/main)
Electron [**main script**](https://www.electronjs.org/docs/tutorial/quick-start#create-the-main-script-file).
### 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 and compile a ready to distribute Electron app for macOS, Windows and Linux with "auto update" support out of the box.
To do this using the [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 state.
This means that in the `renderer` you are free to use dependencies such as Vue, React, lodash, axios and so on. But you can't use, say, systeminformation or pg because these dependencies need access to a node api to work, which is not available in the `renderer` context.
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).
Here is an example. Let's say you need to read some data from the file system or database in the renderer.
In preload context create a method that reads and return data. To make the method 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 {writeFile} from 'fs'
// Everything you exported from preload/index.ts may be called in renderer
export function getData() {
return /* ... */
}
```
Now you can import and call the method in renderer
```ts
// renderer/somewere.component.ts
import {getData} from '#preload'
const dataFromFS = getData()
```
[Read more about Security Considerations](https://www.electronjs.org/docs/tutorial/context-isolation#security-considerations).
### Working with Electron API
Although the preload has access to Node.js 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
```
[Read more aboud Inter-Process Communication](https://www.electronjs.org/docs/latest/tutorial/ipc)
### Modes and Environment Variables
All environment variables set as part of the `import.meta`, so you can access them as follows: `import.meta.env`.
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
```
To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code. e.g. the following 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.
## 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/

View File

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

27
contributing.md 100644
View File

@ -0,0 +1,27 @@
# 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.

11020
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

58
package.json 100644
View File

@ -0,0 +1,58 @@
{
"name": "vite-electron-builder",
"description": "Secure boilerplate for Electron app based on Vite",
"private": true,
"engines": {
"node": ">=v16.13",
"npm": ">=8.1"
},
"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": "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.js",
"lint": "eslint . --ext js,ts,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 npx --no-install electron ./scripts/update-electron-vendors.js "
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.30.6",
"@vitejs/plugin-vue": "3.0.0",
"@vue/test-utils": "2.0.2",
"cross-env": "7.0.3",
"electron": "19.0.8",
"electron-builder": "23.1.0",
"electron-devtools-installer": "3.2.0",
"eslint": "8.20.0",
"eslint-plugin-vue": "9.2.0",
"happy-dom": "6.0.4",
"nano-staged": "0.8.0",
"playwright": "1.23.4",
"simple-git-hooks": "2.8.0",
"typescript": "4.7.4",
"unplugin-auto-expose": "0.0.1",
"vite": "3.0.0",
"vitest": "0.18.1",
"vue-tsc": "0.38.8"
},
"dependencies": {
"electron-updater": "5.0.5",
"vue": "3.2.37"
}
}

View File

@ -0,0 +1,68 @@
import {app} from 'electron';
import './security-restrictions';
import {restoreOrCreateWindow} from '/@/mainWindow';
/**
* Prevent multiple instances
*/
const isSingleInstance = app.requestSingleInstanceLock();
if (!isSingleInstance) {
app.quit();
process.exit(0);
}
app.on('second-instance', restoreOrCreateWindow);
/**
* Disable Hardware Acceleration for more power-save
*/
app.disableHardwareAcceleration();
/**
* Shout down background process if all windows was closed
*/
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
/**
* @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate'
*/
app.on('activate', restoreOrCreateWindow);
/**
* Create app window when background process will be ready
*/
app.whenReady()
.then(restoreOrCreateWindow)
.catch((e) => console.error('Failed create window:', e));
/**
* Install Vue.js or some other devtools in development mode only
*/
if (import.meta.env.DEV) {
app.whenReady()
.then(() => import('electron-devtools-installer'))
.then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, {
loadExtensionOptions: {
allowFileAccess: true,
},
}))
.catch(e => console.error('Failed install extension:', e));
}
/**
* Check new app version in production mode only
*/
if (import.meta.env.PROD) {
app.whenReady()
.then(() => import('electron-updater'))
.then(({autoUpdater}) => autoUpdater.checkForUpdatesAndNotify())
.catch((e) => console.error('Failed check updates:', e));
}

View File

@ -0,0 +1,58 @@
import {BrowserWindow} from 'electron';
import {join} from 'path';
import {URL} from 'url';
async function createWindow() {
const browserWindow = new BrowserWindow({
show: false, // Use 'ready-to-show' event to show window
webPreferences: {
webviewTag: false, // The webview tag is not recommended. Consider alternatives like iframe or Electron's BrowserView. https://www.electronjs.org/docs/latest/api/webview-tag#warning
preload: join(__dirname, '../../preload/dist/index.cjs'),
},
});
/**
* If you install `show: true` then it can cause issues when trying to close the window.
* Use `show: false` and listener events `ready-to-show` to fix these issues.
*
* @see https://github.com/electron/electron/issues/25012
*/
browserWindow.on('ready-to-show', () => {
browserWindow?.show();
if (import.meta.env.DEV) {
browserWindow?.webContents.openDevTools();
}
});
/**
* URL for main window.
* Vite dev server for development.
* `file://../renderer/index.html` for production and test
*/
const pageUrl = import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined
? import.meta.env.VITE_DEV_SERVER_URL
: new URL('../renderer/dist/index.html', 'file://' + __dirname).toString();
await browserWindow.loadURL(pageUrl);
return browserWindow;
}
/**
* Restore existing BrowserWindow or Create 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,127 @@
import {app, shell} from 'electron';
import {URL} from 'url';
/**
* List of origins that you allow open INSIDE the application and permissions for each of them.
*
* In development mode you need allow open `VITE_DEV_SERVER_URL`
*/
const ALLOWED_ORIGINS_AND_PERMISSIONS = new Map<string, Set<'clipboard-read' | 'media' | 'display-capture' | 'mediaKeySystem' | 'geolocation' | 'notifications' | 'midi' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' | 'unknown'>>(
import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL
? [[new URL(import.meta.env.VITE_DEV_SERVER_URL).origin, new Set]]
: [],
);
/**
* List of origins that you allow open IN BROWSER.
* Navigation to origins below is possible only 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 is a common attack vector. If an attacker can convince the app to navigate away
* from its current page, they can possibly force the app to open web sites on the Internet.
*
* @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 an unallowed origin:', origin);
}
});
/**
* Block requested unallowed 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 blocked.`);
}
});
/**
* Hyperlinks to allowed sites open 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);
// @ts-expect-error Type checking is performed in runtime
if (ALLOWED_EXTERNAL_ORIGINS.has(origin)) {
// Open default browser
shell.openExternal(url).catch(console.error);
} else if (import.meta.env.DEV) {
console.warn('Blocked the opening of an unallowed origin:', origin);
}
// Prevent creating new window in application
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;
});
});

View File

@ -0,0 +1,69 @@
import type {MaybeMocked} 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', () => {
const bw = vi.fn() as MaybeMocked<typeof BrowserWindow>;
// @ts-expect-error It's work in runtime, but I Haven't idea how to fix this type error
bw.getAllWindows = vi.fn(() => bw.mock.instances);
bw.prototype.loadURL = vi.fn();
bw.prototype.on = vi.fn();
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();
return {BrowserWindow: bw};
});
beforeEach(() => {
vi.clearAllMocks();
});
test('Should create new window', async () => {
const {mock} = vi.mocked(BrowserWindow);
expect(mock.instances).toHaveLength(0);
await restoreOrCreateWindow();
expect(mock.instances).toHaveLength(1);
expect(mock.instances[0].loadURL).toHaveBeenCalledOnce();
expect(mock.instances[0].loadURL).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/));
});
test('Should restore existing window', async () => {
const {mock} = vi.mocked(BrowserWindow);
// Create 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 new window if previous was destroyed', async () => {
const {mock} = vi.mocked(BrowserWindow);
// Create 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,28 @@
{
"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,41 @@
import {node} from '../../.electron-vendors.cache.json';
import {join} from 'path';
const PACKAGE_ROOT = __dirname;
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: process.cwd(),
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,
brotliSize: false,
},
};
export default config;

View File

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

View File

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

View File

@ -0,0 +1 @@
export const versions = process.versions;

View File

@ -0,0 +1,20 @@
import {createHash} from 'crypto';
import {expect, test} from 'vitest';
import {versions, sha256sum} from '../src';
test('versions', async () => {
expect(versions).toBe(process.versions);
});
test('nodeCrypto', async () => {
/**
* Random string to test hashing
*/
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,23 @@
{
"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,38 @@
import {chrome} from '../../.electron-vendors.cache.json';
import {preload} from 'unplugin-auto-expose';
const PACKAGE_ROOT = __dirname;
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_ROOT,
envDir: process.cwd(),
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,
brotliSize: false,
},
plugins: [
preload.vite(),
],
};
export default config;

View File

@ -0,0 +1,15 @@
{
"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"
}
}

View File

@ -0,0 +1,15 @@
<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.5 KiB

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' blob:">
<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,58 @@
<script lang="ts" setup>
import ReactiveCounter from '/@/components/ReactiveCounter.vue';
import ReactiveHash from '/@/components/ReactiveHash.vue';
import ElectronVersions from '/@/components/ElectronVersions.vue';
</script>
<template>
<img
alt="Vue logo"
src="../assets/logo.svg"
width="150"
>
<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,21 @@
<script lang="ts" setup>
import {versions} from '#preload';
</script>
<template>
<ul id="process-versions">
<li
v-for="(version, lib) in versions"
:key="lib"
>
<strong>{{ lib }}</strong>: v{{ version }}
</li>
</ul>
<code>packages/renderer/src/components/ElectronVersions.vue</code>
</template>
<style scoped>
ul {
list-style: none;
}
</style>

View File

@ -0,0 +1,13 @@
<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,31 @@
<script lang="ts" setup>
import {computed, ref} from 'vue';
import {sha256sum} from '#preload';
const rawString = ref('Hello World');
/**
* window.nodeCrypto was exposed from {@link module:preload}
*/
const hashedString = computed(() => sha256sum(rawString.value));
</script>
<template>
<label>
Raw value
<input
v-model="rawString"
type="text"
>
</label>
<br>
<label>
Hashed by node:crypto
<input
v-model="hashedString"
type="text"
readonly
>
</label>
<br><br>
<code>packages/renderer/src/components/ReactiveHash.vue</code>
</template>

View File

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

View File

@ -0,0 +1,21 @@
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 lis = wrapper.findAll<HTMLElement>('li');
expect(lis.length).toBe(2);
expect(lis[0].text()).toBe('lib1: v1');
expect(lis[1].text()).toBe('lib2: 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,23 @@
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,36 @@
{
"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,50 @@
/* eslint-env node */
import {chrome} from '../../.electron-vendors.cache.json';
import {join} from 'path';
import vue from '@vitejs/plugin-vue';
import {renderer} from 'unplugin-auto-expose';
const PACKAGE_ROOT = __dirname;
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
const config = {
mode: process.env.MODE,
root: PACKAGE_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,
brotliSize: false,
},
test: {
environment: 'happy-dom',
},
plugins: [
vue(),
renderer.vite({
preloadEntry: join(PACKAGE_ROOT, '../preload/src/index.ts'),
}),
],
};
export default config;

View File

@ -0,0 +1,15 @@
const {writeFileSync} = require('fs');
const path = require('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');

149
scripts/watch.js 100644
View File

@ -0,0 +1,149 @@
#!/usr/bin/env node
const {createServer, build, createLogger} = require('vite');
const electronPath = require('electron');
const {spawn} = require('child_process');
/** @type 'production' | 'development'' */
const mode = process.env.MODE = process.env.MODE || 'development';
/** @type {import('vite').LogLevel} */
const logLevel = 'info';
/** Messages on stderr that match any of the contained patterns will be stripped from output */
const stderrFilterPatterns = [
/**
* warning about devtools extension
* @see https://github.com/cawa-93/vite-electron-builder/issues/492
* @see https://github.com/MarshallOfSound/electron-devtools-installer/issues/143
*/
/ExtensionLoadWarning/,
];
/**
* 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}
*/
const setupMainPackageWatcher = ({resolvedUrls}) => {
process.env.VITE_DEV_SERVER_URL = resolvedUrls.local[0];
const logger = createLogger(logLevel, {
prefix: '[main]',
});
/** @type {ChildProcessWithoutNullStreams | null} */
let spawnProcess = null;
return build({
mode,
logLevel,
build: {
/**
* Set to {} to enable rollup watcher
* @see https://vitejs.dev/config/build-options.html#build-watch
*/
watch: {},
},
configFile: 'packages/main/vite.config.js',
plugins: [{
name: 'reload-app-on-main-package-change',
writeBundle() {
/** Kill electron ff process already exist */
if (spawnProcess !== null) {
spawnProcess.off('exit', process.exit);
spawnProcess.kill('SIGINT');
spawnProcess = null;
}
/** Spawn new electron process */
spawnProcess = spawn(String(electronPath), ['.']);
/** Proxy all logs */
spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: true}));
/** Proxy error logs but stripe some noisy messages. See {@link stderrFilterPatterns} */
spawnProcess.stderr.on('data', d => {
const data = d.toString().trim();
if (!data) return;
const mayIgnore = stderrFilterPatterns.some((r) => r.test(data));
if (mayIgnore) return;
logger.error(data, {timestamp: true});
});
/** Stops the watch script when the application has been quit */
spawnProcess.on('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.
*/
const setupPreloadPackageWatcher = ({ws}) =>
build({
mode,
logLevel,
build: {
/**
* Set to {} to enable rollup watcher
* @see https://vitejs.dev/config/build-options.html#build-watch
*/
watch: {},
},
configFile: 'packages/preload/vite.config.js',
plugins: [{
name: 'reload-page-on-preload-package-change',
writeBundle() {
ws.send({
type: 'full-reload',
});
},
}],
});
(async () => {
try {
/**
* Renderer watcher
* This must be the first,
* because the {@link setupMainPackageWatcher} and {@link setupPreloadPackageWatcher} depend on the renderer params
*/
const rendererWatchServer = await createServer({
mode,
logLevel,
configFile: 'packages/renderer/vite.config.js',
});
/**
* Should launch watch server before create other watchers
*/
await rendererWatchServer.listen();
/**
* See {@link setupPreloadPackageWatcher} JSDoc
*/
await setupPreloadPackageWatcher(rendererWatchServer);
/**
* See {@link setupMainPackageWatcher} JSDoc
*/
await setupMainPackageWatcher(rendererWatchServer);
} catch (e) {
console.error(e);
process.exit(1);
}
})();

77
tests/e2e.spec.ts 100644
View File

@ -0,0 +1,77 @@
import type {ElectronApplication} from 'playwright';
import {_electron as electron} from 'playwright';
import {afterAll, beforeAll, expect, test} from 'vitest';
import {createHash} from 'crypto';
let electronApp: ElectronApplication;
beforeAll(async () => {
electronApp = await electron.launch({args: ['.'], timeout: 60000 * 5});
});
afterAll(async () => {
await electronApp.close();
});
test('Main window state', async () => {
const windowState: { isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean }
= await electronApp.evaluate(({BrowserWindow}) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
const getState = () => ({
isVisible: mainWindow.isVisible(),
isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
isCrashed: mainWindow.webContents.isCrashed(),
});
return new Promise((resolve) => {
if (mainWindow.isVisible()) {
resolve(getState());
} else
mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0));
});
});
expect(windowState.isCrashed, 'App was crashed').toBeFalsy();
expect(windowState.isVisible, 'Main window was not visible').toBeTruthy();
expect(windowState.isDevToolsOpened, 'DevTools was opened').toBeFalsy();
});
test('Main window web content', async () => {
const page = await electronApp.firstWindow();
const element = await page.$('#app', {strict: true});
expect(element, 'Can\'t find root element').toBeDefined();
expect((await element.innerHTML()).trim(), 'Window content was empty').not.equal('');
});
test('Preload versions', async () => {
const page = await electronApp.firstWindow();
const renderedVersions = await page.locator('#process-versions').innerText();
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();
/**
* Random string to test hashing
*/
const testString = Math.random().toString(36).slice(2, 7);
await page.fill('input', testString);
const renderedHash = await page.inputValue('input[readonly]');
const expectedHash = createHash('sha256').update(testString).digest('hex');
expect(renderedHash).toEqual(expectedHash);
});

23
types/env.d.ts vendored 100644
View File

@ -0,0 +1,23 @@
/// <reference types="vite/client" />
/**
* Describes all existing environment variables and their types.
* Required for Code completion 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/cab55b32de62e0de7d7789e8c2a1f04a8eae3a3f/packages/vite/types/importMeta.d.ts#L62-L69 Base Interface
* @see https://vitejs.dev/guide/env-and-mode.html#env-files Vite Env Variables Doc
*/
interface ImportMetaEnv {
/**
* URL where `renderer` web page located.
* Variable initialized in scripts/watch.ts
*/
readonly VITE_DEV_SERVER_URL: undefined | string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

23
vitest.config.js 100644
View File

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