This commit is contained in:
Philipp Dormann 2021-03-14 19:06:51 +01:00
parent de211eb1d3
commit 560b0f4c74
43 changed files with 5944 additions and 175 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

0
.env Normal file
View File

4
.env.development Normal file
View File

@ -0,0 +1,4 @@
# This is a stub.
# It is needed as a data sample for TypeScript & Typechecking.
# The real value of the variable is set in /bin/watch.js and depend on /config/renderer.vite.js
VITE_DEV_SERVER_URL=http://localhost:3000/

0
.env.production Normal file
View File

44
.eslintrc.js Normal file
View File

@ -0,0 +1,44 @@
module.exports = {
root: true,
env: {
es2021: true,
node: true,
browser: false,
},
extends: [
/** @see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#recommended-configs */
'plugin:@typescript-eslint/recommended',
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
plugins: [
'@typescript-eslint',
],
ignorePatterns: [
'types/env.d.ts',
'node_modules/**',
'dist/**',
],
rules: {
/**
* Having a semicolon helps the optimizer interpret your code correctly.
* This avoids rare errors in optimized code.
*/
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'],
},
};

221
.gitignore vendored
View File

@ -1,183 +1,56 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# ---> Windows
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# ---> macOS
# General
node_modules
.DS_Store
.AppleDouble
.LSOverride
dist
*.local
thumbs.db
types/env.d.ts
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# 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
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# ---> Vue
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# Generated files
.idea/**/contentModel.xml
# TODO: where does this rule come from?
docs/_book
# 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
# TODO: where does this rule come from?
test/
# 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

200
README.md
View File

@ -1,2 +1,200 @@
# scanclient
# Vite Electron Builder Template
> Vite+Electron = 🔥
This is a secure template for electron applications. Written following the latest safety requirements, recommendations and best practices.
Under the hood is used [Vite 2.0][vite] — super fast, nextgen bundler, and [electron-builder] for compilation.
By default, the **Vue framework** is used for the interface, but you can easily use any other frameworks such as **React**, **Preact**, **Angular**, **Svelte** or anything else.
> Vite is framework agnostic
## Support
This template maintained by [Alex Kozack][cawa-93-github]. You can [💖 sponsor him][cawa-93-sponsor] for continued development of this template.
If you have ideas, questions or suggestions - **Welcome to [discussions](https://github.com/cawa-93/vite-electron-builder/discussions)**. 😊
## Recommended requirements
- **Node**: >=14.16
- **npm**: >7.6
## Features
### Electron [![Electron version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/electron?label=%20)][electron]
- Template use the latest electron version with all the latest security patches.
- The architecture of the application is built according to the security [guids](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. My template has a separate command to generate `.d.ts` file with type definition your environment variables.
Vite provides you with 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 TypeScript is used for all source code.
- **Vite** supports TypeScript out of the box. However, it does not support type checking.
- Type checking is performed in both `.ts` and `.vue` files thanks to [@vuedx/typecheck].
- 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).
**Note**: If you do not need a TypeScript, you can easily abandon it. To do this, You do not need to make any bundler configuration changes, etc. Just replace all `.ts` files with `.js` files. Additionally, it will be useful to delete TS-specific files, plug-ins and dependencies like `tsconfig.json`, `@typescript-eslint/*`, etc.
### 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 the latest version of the [Vue]. However, there are no problems with using any other frameworks or technologies.
- Also, by default, the [vue-router] version [![Vue-router version](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/vue-router?label=%20)][vue-router] is included.
- 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.
### Continuous Integration
- The configured workflow for check the types for each push and PR.
- The configured workflow for check the code style for each push and PR.
- **Automatic tests** used [spectron]. Simple, automated test check:
- Does the main window open
- Is the main window not empty
- Is dev tools closed
### Continuous deployment
- An automatic update from GitHub releases is supported.
- Each time you push changes to the main branch, a workflow starts, which creates a new github release.
- The version number is automatically set based on the current date in the format "yy.mm.dd".
- Notes are automatically generated and added to the new release.
## Status
- ✅ Building main and renderer endpoints in production mode — works great.
- ✅ Development mode with hot reload for renderer endpoint — works great.
- ⚠ Development mode for main and preload endpoints — work fine, but it is possible to reboot the backend faster ([vite#1434](https://github.com/vitejs/vite/issues/1434))
- ✅ Compile the app with electron builder in CD — work.
- ✅ Auto update — work.
- ⚠ Typechecking in `.ts` and `.vue` files — work thanks [![@vuedx/typecheck](https://img.shields.io/github/package-json/dependency-version/cawa-93/vite-electron-builder/dev/@vuedx/typecheck)][@vuedx/typecheck]. Improvement needed.
- ⚠ Linting — work fine, but need review the configuration files and refactor its.
- ✅ Vue.js devtools beta.
- ⏳ Code signing — planned.
## How it works
The template required a minimum [dependencies](package.json). Only **Vite** is used for building, nothing more.
### Using electron API in renderer
As per the security requirements, context isolation is enabled in this template.
> Context Isolation is a feature that ensures that both your `preload` scripts and Electron's internal logic run in a separate context to the website you load in a [`webContents`](https://github.com/electron/electron/blob/master/docs/api/web-contents.md). This is important for security purposes as it helps prevent the website from accessing Electron internals or the powerful APIs your preload script has access to.
>
> This means that the `window` object that your preload script has access to is actually a **different** object than the website would have access to. For example, if you set `window.hello = 'wave'` in your preload script and context isolation is enabled `window.hello` will be undefined if the website tries to access it.
[Read more about Context Isolation](https://github.com/electron/electron/blob/master/docs/tutorial/context-isolation.md).
Exposing APIs from your `preload script` to the renderer is a common usecase and there is a dedicated module in Electron to help you do this in a painless way.
```ts
// /src/preload/index.ts
const api = {
data: ['foo', 'bar'],
doThing: () => ipcRenderer.send('do-a-thing')
}
contextBridge.exposeInMainWorld('electron', api)
```
To access this API use the `useElectron()` function:
```ts
// /src/renderer/App.vue
import {useElectron} from '/@/use/electron'
const {doThing, data} = useElectron()
```
**Note**: Context isolation disabled for `test` environment. See [#693](https://github.com/electron-userland/spectron/issues/693#issuecomment-747872160).
### Modes and Environment Variables
All environment variables set as part of the `import.meta`, so you can access them as follows: `import.meta.env`.
You can also build type definitions of your variables by running `bin/buildEnvTypes.js`. This command will create `types/env.d.ts` file with describing all environment variables for all modes.
The mode option is used to specify the value of `import.meta.env.MODE` and the corresponding environment variables files that needs to be loaded.
By default, there are two modes:
- `production` is used by default
- `development` is used by `npm run watch` script
- `test` is used by `npm test` script
When running building, 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
```
**Note:** only variables prefixed with `VITE_` are exposed to your code (e.g. `VITE_SOME_KEY=123`) and `SOME_KEY=123` will not. you can access `VITE_SOME_KEY` using `import.meta.env.VITE_SOME_KEY`. This is because the `.env` files may be used by some users for server-side or build scripts and may contain sensitive information that should not be exposed in code shipped to browsers.
### Project Structure
- [`src`](src)
Contains all source code.
- [`src/main`](src/main)
Contain entrypoint for Electron [**main script**](https://www.electronjs.org/docs/tutorial/quick-start#create-the-main-script-file).
- [`src/renderer`](src/renderer)
Contain entrypoint for Electron [**web page**](https://www.electronjs.org/docs/tutorial/quick-start#create-a-web-page). All files in this directory work as a regular Vue application.
- [`src/preload`](src/preload)
Contain entrypoint for custom script. It uses as `preload` script in `BrowserWindow.webPreferences.preload`. See [Checklist: Security Recommendations](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content).
- [`src/*`](src) It is assumed any entry points will be added here, for custom scripts, web workers, webassembly compilations, etc.
- [`dist`](dist)
- [`dist/source`](dist/source)
Contains all bundled code.
- [`dist/source/main`](dist/source/main) Bundled *main* entrypoint.
- [`dist/source/renderer`](dist/source/renderer) Bundled *renderer* entrypoint.
- [`dist/source/preload`](dist/source/preload) Bundled *preload* entrypoint.
- [`dist/source/*`](dist/source) Bundled any custom files.
- [`dist/app`](dist/app)
Contain packages and ready-to-distribute electron apps for any platform. Files in this directory created using [electron-builder].
- [`config`](config)
Contains various configuration files for Vite, TypeScript, electron builder, etc.
- [`bin`](bin)
It is believed any scripts for build the application will be located here.
- [`types`](types)
Contains all declaration files to be applied globally to the entire project
- [`tests`](tests)
Contains all tests
### Development Setup
This project was tested on Node 14.
1. Fork this repository
1. Run `npm install` to install all dependencies
1. Build compile app for production — `npm run compile`
1. Run development environment with file watching — `npm run watch`
1. Run tests — `npm test`
[vite]: https://vitejs.dev/
[electron]: https://electronjs.org/
[electron-builder]: https://www.electron.build/
[vue]: https://v3.vuejs.org/
[vue-router]: https://github.com/vuejs/vue-router-next/
[typescript]: https://www.typescriptlang.org/
[spectron]: https://www.electronjs.org/spectron/
[@vuedx/typecheck]: https://github.com/znck/vue-developer-experience/tree/master/packages/typecheck
[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/

8
bin/.eslintrc.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
env: {
node: true,
},
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
};

75
bin/build.js Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/node
console.time('Bundling time');
const {build} = require('vite');
const {join} = require('path');
/** @type 'production' | 'development' | 'test' */
const mode = process.env.MODE || 'production';
const configs = [
join(process.cwd(), 'config/main.vite.js'),
join(process.cwd(), 'config/preload.vite.js'),
join(process.cwd(), 'config/renderer.vite.js'),
];
/**
* Run `vite build` for config file
* @param {string} configFile
* @return {Promise<RollupOutput | RollupOutput[]>}
*/
const buildByConfig = (configFile) => build({configFile, mode});
/**
* Creates a separate package.json in which:
* - The version number is set based on the current date in the format yy.mm.dd
* - Removed all dependencies except those marked as "external".
* @see /config/external-packages.js
*
* @return {Promise<void>}
*/
const generatePackageJson = () => {
// Get project package.json
const packageJson = require(join(process.cwd(), 'package.json'));
// Cleanup
delete packageJson.scripts;
{
// Remove all bundled dependencies
// Keep only `external` dependencies
delete packageJson.devDependencies;
const {default: external} = require('../config/external-packages');
for (const type of ['dependencies', 'optionalDependencies']) {
if (packageJson[type] === undefined) {
continue;
}
for (const key of Object.keys(packageJson[type])) {
if (!external.includes(key)) {
delete packageJson[type][key];
}
}
}
}
{
// Set version based on current date in yy.mm.dd format
// The year is calculated on the principle of a `getFullYear() - 2000`, so that in 2120 the version was `120` and not `20` 😅
const now = new Date;
packageJson.version = `${now.getFullYear() - 2000}.${now.getMonth() + 1}.${now.getDate()}`;
}
// Create new package.json
const {writeFile} = require('fs/promises');
return writeFile(join(process.cwd(), 'dist/source/package.json'), JSON.stringify(packageJson));
};
Promise.all(configs.map(buildByConfig))
.then(generatePackageJson)
.then(() => console.timeEnd('Bundling time'))
.catch(e => {
console.error(e);
process.exit(1);
});

36
bin/buildEnvTypes.js Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env node
const {resolveConfig} = require('vite');
const {writeFileSync, mkdirSync, existsSync} = require('fs');
const {resolve, dirname} = require('path');
/**
* @param {string[]} modes
* @param {string} filePath
*/
async function buildMode(modes, filePath) {
const interfaces = await Promise.all(modes.map(async mode => {
const modeInterfaceName = `${mode}Env`;
const {env} = await resolveConfig({mode, configFile: resolve(process.cwd(), 'config/main.vite.js')}, 'build');
const interfaceDeclaration = `interface ${modeInterfaceName} ${JSON.stringify(env)}`;
return {modeInterfaceName, interfaceDeclaration};
}));
const interfacesDeclarations = interfaces.map(({interfaceDeclaration}) => interfaceDeclaration).join('\n');
const type = interfaces.map(({modeInterfaceName}) => modeInterfaceName).join(' | ');
const dir = dirname(filePath);
if (!existsSync(dir)) {
mkdirSync(dir);
}
writeFileSync(filePath, `${interfacesDeclarations}\ntype ImportMetaEnv = ${type}\n`, {encoding: 'utf-8', flag: 'w'});
}
buildMode(['production', 'development', 'test'], resolve(process.cwd(), './types/env.d.ts'))
.catch(err => {
console.error(err);
process.exit(1);
});

View File

@ -0,0 +1,88 @@
/**
* Temporally
* @deprecated
* @see https://github.com/electron/electron/issues/28006
*/
/**
* @typedef Vendors
* @type {{
* node: string,
* v8: string,
* uv: string,
* zlib: string,
* brotli: string,
* ares: string,
* modules: string,
* nghttp2: string,
* napi: string,
* llhttp: string,
* http_parser: string,
* openssl: string,
* cldr: string,
* icu: string,
* tz: string,
* unicode: string,
* electron: string,
* }}
*/
/**
*
* @type {null | Vendors}
*/
let runtimeCache = null;
/**
* Returns information about dependencies of the specified version of the electron
* @return {Vendors}
*
* @see https://electronjs.org/headers/index.json
*/
const loadDeps = () => {
const stringifiedDeps = require('child_process').execSync(
'electron -p JSON.stringify(process.versions)',
{
encoding: 'utf-8',
env: {
ELECTRON_RUN_AS_NODE: '1',
},
},
);
return JSON.parse(stringifiedDeps);
};
const saveToCache = (dist) => {
runtimeCache = dist;
};
/**
*
* @return {null|Vendors}
*/
const loadFromCache = () => runtimeCache;
/**
*
* @return {Vendors}
*/
const getElectronDist = () => {
let dist = loadFromCache();
if (dist) {
return dist;
}
dist = loadDeps();
saveToCache(dist);
return dist;
};
const {node, modules} = getElectronDist();
module.exports.node = node;
module.exports.chrome = modules;//.split('.')[0];

142
bin/watch.js Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/node
// TODO:
// - Disable dependency optimization during development.
// - Need more tests
// - Refactoring
const slash = require('slash');
const chokidar = require('chokidar');
const {createServer, build, normalizePath} = require('vite');
const electronPath = require('electron');
const {spawn} = require('child_process');
const {join, relative} = require('path');
const mode = process.env.MODE || 'development';
const TIMEOUT = 500;
function debounce(f, ms) {
let isCoolDown = false;
return function () {
if (isCoolDown) return;
f.apply(this, arguments);
isCoolDown = true;
setTimeout(() => isCoolDown = false, ms);
};
}
(async () => {
// Create Vite dev server
const viteDevServer = await createServer({
mode,
configFile: join(process.cwd(), 'config/renderer.vite.js'),
});
await viteDevServer.listen();
// Determining the current URL of the server. It depend on /config/renderer.vite.js
// Write a value to an environment variable to pass it to the main process.
{
const protocol = `http${viteDevServer.config.server.https ? 's' : ''}:`;
const host = viteDevServer.config.server.host || 'localhost';
const port = viteDevServer.config.server.port; // Vite searches for and occupies the first free port: 3000, 3001, 3002 and so on
const path = '/';
process.env.VITE_DEV_SERVER_URL = `${protocol}//${host}:${port}${path}`;
}
/** @type {ChildProcessWithoutNullStreams | null} */
let spawnProcess = null;
const runMain = debounce(() => {
if (spawnProcess !== null) {
spawnProcess.kill('SIGINT');
spawnProcess = null;
}
spawnProcess = spawn(electronPath, [join(process.cwd(), 'dist/source/main/index.cjs.js')]);
spawnProcess.stdout.on('data', d => console.log(d.toString()));
spawnProcess.stderr.on('data', d => console.error(d.toString()));
return spawnProcess;
}, TIMEOUT);
const buildMain = () => {
return build({mode, configFile: join(process.cwd(), 'config/main.vite.js')});
};
const buildMainDebounced = debounce(buildMain, TIMEOUT);
const runPreload = debounce((file) => {
viteDevServer.ws.send({
type: 'full-reload',
path: '/' + slash(relative(viteDevServer.config.root, file)),
});
}, TIMEOUT);
const buildPreload = () => {
return build({mode, configFile: join(process.cwd(), 'config/preload.vite.js')});
};
const buildPreloadDebounced = debounce(buildPreload, TIMEOUT);
await Promise.all([
buildMain(),
buildPreload(),
]);
const watcher = chokidar.watch([
join(process.cwd(), 'src/main/**'),
join(process.cwd(), 'src/preload/**'),
join(process.cwd(), 'dist/source/main/**'),
join(process.cwd(), 'dist/source/preload/**'),
], {ignoreInitial: true});
watcher
.on('unlink', path => {
const normalizedPath = normalizePath(path);
if (spawnProcess !== null && normalizedPath.includes('/dist/source/main/')) {
spawnProcess.kill('SIGINT');
spawnProcess = null;
}
})
.on('add', path => {
const normalizedPath = normalizePath(path);
if (normalizedPath.includes('/dist/source/main/')) {
return runMain();
}
if (spawnProcess !== undefined && normalizedPath.includes('/dist/source/preload/')) {
return runPreload(normalizedPath);
}
})
.on('change', (path) => {
const normalizedPath = normalizePath(path);
if (normalizedPath.includes('/src/main/')) {
return buildMainDebounced();
}
if (normalizedPath.includes('/dist/source/main/')) {
return runMain();
}
if (normalizedPath.includes('/src/preload/')) {
return buildPreloadDebounced();
}
if (normalizedPath.includes('/dist/source/preload/')) {
return runPreload(normalizedPath);
}
});
await runMain();
})();

8
config/.eslintrc.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
env: {
node: true,
},
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
};

View File

@ -0,0 +1,11 @@
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
*/
module.exports = {
directories: {
output: 'dist/app',
buildResources: 'build',
app: 'dist/source',
},
};

View File

@ -0,0 +1,4 @@
module.exports = {
chrome: 85,
node: 12,
};

View File

@ -0,0 +1,59 @@
/**
By default, vite optimizes and packs all the necessary dependencies into your bundle,
so there is no need to supply them in your application as a node module.
Unfortunately, vite cannot optimize any dependencies:
Some that are designed for a node environment may not work correctly after optimization.
Therefore, such dependencies should be marked as "external":
they will not be optimized, will not be included in your bundle, and will be delivered as a separate node module.
*/
module.exports.external = [
'electron',
'electron-updater',
];
module.exports.builtins = [
'assert',
'async_hooks',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'dns',
'domain',
'events',
'fs',
'http',
'http2',
'https',
'inspector',
'module',
'net',
'os',
'path',
'perf_hooks',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'timers',
'tls',
'trace_events',
'tty',
'url',
'util',
'v8',
'vm',
'zlib',
];
module.exports.default = [
...module.exports.builtins,
...module.exports.external,
];

37
config/main.vite.js Normal file
View File

@ -0,0 +1,37 @@
const {node} = require('./electron-vendors');
const {join} = require('path');
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
module.exports = () => {
return {
resolve: {
alias: {
'/@/': join(process.cwd(), './src/main') + '/',
},
},
build: {
sourcemap: 'inline',
target: `node${node}`,
outDir: 'dist/source/main',
assetsDir: '.',
minify: process.env.MODE === 'development' ? false : 'terser',
lib: {
entry: 'src/main/index.ts',
formats: ['cjs'],
},
rollupOptions: {
external: require('./external-packages').default,
output: {
entryFileNames: '[name].[format].js',
chunkFileNames: '[name].[format].js',
assetFileNames: '[name].[ext]',
},
},
emptyOutDir: true,
},
};
};

34
config/preload.vite.js Normal file
View File

@ -0,0 +1,34 @@
const {chrome} = require('./electron-vendors');
const {join} = require('path');
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
module.exports = {
resolve: {
alias: {
'/@/': join(process.cwd(), './src/preload') + '/',
},
},
build: {
sourcemap: 'inline',
target: `chrome${chrome}`,
outDir: 'dist/source/preload',
assetsDir: '.',
minify: process.env.MODE === 'development' ? false : 'terser',
lib: {
entry: 'src/preload/index.ts',
formats: ['cjs'],
},
rollupOptions: {
external: require('./external-packages').default,
output: {
entryFileNames: '[name].[format].js',
chunkFileNames: '[name].[format].js',
assetFileNames: '[name].[ext]',
},
},
emptyOutDir: true,
},
};

29
config/renderer.vite.js Normal file
View File

@ -0,0 +1,29 @@
const {join} = require('path');
const vue = require('@vitejs/plugin-vue');
const {chrome} = require('./electron-vendors');
/**
* @type {import('vite').UserConfig}
* @see https://vitejs.dev/config/
*/
module.exports = {
root: join(process.cwd(), './src/renderer'),
resolve: {
alias: {
'/@/': join(process.cwd(), './src/renderer') + '/',
},
},
plugins: [vue()],
base: '',
build: {
sourcemap: 'inline',
target: `chrome${chrome}`,
polyfillDynamicImport: false,
outDir: join(process.cwd(), 'dist/source/renderer'),
assetsDir: '.',
rollupOptions: {
external: require('./external-packages').default,
},
emptyOutDir: true,
},
};

18
config/tsconfig-base.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": true,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"types": [
"../types/env",
"vite/client"
]
},
"exclude": [
"../**/node_modules"
]
}

59
contributing.md Normal file
View File

@ -0,0 +1,59 @@
# 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.
## Important!
By contributing to this project, you:
* Agree that you have authored 100% of the content
* Agree that you have the necessary rights to the content
* Agree that you have received the necessary permissions from your employer to make the contributions (if applicable)
* Agree that the content you contribute may be provided under the Project license(s)
* Agree that, if you did not author 100% of the content, the appropriate licenses and copyrights have been added along with any other necessary attribution.
## Getting started
### 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._
### Before creating an issue
Please try to determine if the issue is caused by an underlying library, and if so, create the issue there. Sometimes this is difficult to know. We only ask that you attempt to give a reasonable attempt to find out. Oftentimes the readme will have advice about where to go to create issues.
Try to follow these guidelines:
- **Investigate the issue** - Search for exising issues (open or closed) that address the issue, and might have even resolved it already.
- **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue.
- Create the issue in the appropriate repository.
### Creating an issue
Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue:
- **description**: (required) What is the bug you're experiencing? How are you using this library/app?
- **OS**: (required) what operating system are you on?
- **version**: (required) please note the version of vite-electron-builder are you using
- **error messages**: (required) please paste any error messages into the issue, or a [gist](https://gist.github.com/)
- **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using
### Closing issues
The original poster or the maintainers of vite-electron-builder may close an issue at any time. Typically, but not exclusively, issues are closed when:
- The issue is resolved
- The project's maintainers have determined the issue is out of scope
- An issue is clearly a duplicate of another issue, in which case the duplicate issue will be linked.
- A discussion has clearly run its course

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "vite-electron-builder",
"private": true,
"engines": {
"node": ">=v14.15.0",
"npm": ">=7.6.1"
},
"main": "main/index.cjs.js",
"scripts": {
"buildEnvTypes": "node ./bin/buildEnvTypes.js",
"prebuild": "npm run buildEnvTypes",
"build": "node ./bin/build.js",
"precompile": "cross-env MODE=production npm run build",
"compile": "electron-builder build --config ./config/electron-builder.js",
"pretest": "cross-env MODE=test npm run build",
"test": "node ./tests/app.spec.js",
"prewatch": "npm run buildEnvTypes",
"watch": "node ./bin/watch.js",
"lint": "eslint . --ext js,ts,vue",
"pretypecheck": "npm run buildEnvTypes",
"typecheck-main": "tsc --noEmit -p ./src/main/tsconfig.json",
"typecheck-preload": "tsc --noEmit -p ./src/preload/tsconfig.json",
"typecheck-renderer": "vuedx-typecheck ./src/renderer --no-pretty",
"typecheck": "npm run typecheck-main && npm run typecheck-preload && npm run typecheck-renderer"
},
"devDependencies": {
"@types/electron-devtools-installer": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"@vitejs/plugin-vue": "^1.1.5",
"@vue/compiler-sfc": "^3.0.7",
"@vuedx/typecheck": "^0.6.3",
"chokidar": "^3.5.1",
"cross-env": "^7.0.3",
"electron": "^11.3.0",
"electron-builder": "^22.10.5",
"electron-devtools-installer": "^3.1.1",
"eslint": "^7.21.0",
"eslint-plugin-vue": "^7.7.0",
"slash": "^3.0.0",
"spectron": "^13.0.0",
"typescript": "^4.2.3",
"vite": "^2.0.5"
},
"dependencies": {
"electron-updater": "^4.3.8",
"vue": "^3.0.7",
"vue-router": "^4.0.4"
}
}

91
src/main/index.ts Normal file
View File

@ -0,0 +1,91 @@
import {app, BrowserWindow} from 'electron';
import {join} from 'path';
import {URL} from 'url';
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
/**
* Workaround for TypeScript bug
* @see https://github.com/microsoft/TypeScript/issues/41468#issuecomment-727543400
*/
const env = import.meta.env;
// Install "Vue.js devtools BETA"
if (env.MODE === 'development') {
app.whenReady()
.then(() => import('electron-devtools-installer'))
.then(({default: installExtension}) => {
/** @see https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg */
const VUE_DEVTOOLS_BETA = 'ljjemllljcmogpfapbkkighbhhppjdbg';
return installExtension(VUE_DEVTOOLS_BETA);
})
.catch(e => console.error('Failed install extension:', e));
}
let mainWindow: BrowserWindow | null = null;
async function createWindow() {
mainWindow = new BrowserWindow({
show: false,
webPreferences: {
preload: join(__dirname, '../preload/index.cjs.js'),
contextIsolation: env.MODE !== 'test', // Spectron tests can't work with contextIsolation: true
enableRemoteModule: env.MODE === 'test', // Spectron tests can't work with enableRemoteModule: false
},
});
/**
* URL for main window.
* Vite dev server for development.
* `file://../renderer/index.html` for production and test
*/
const pageUrl = env.MODE === 'development'
? env.VITE_DEV_SERVER_URL
: new URL('renderer/index.html', 'file://' + __dirname).toString();
await mainWindow.loadURL(pageUrl);
mainWindow.maximize();
mainWindow.show();
if (env.MODE === 'development') {
mainWindow.webContents.openDevTools();
}
}
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.whenReady()
.then(createWindow)
.catch((e) => console.error('Failed create window:', e));
// Auto-updates
if (env.PROD) {
app.whenReady()
.then(() => import('electron-updater'))
.then(({autoUpdater}) => autoUpdater.checkForUpdatesAndNotify())
.catch((e) => console.error('Failed check updates:', e));
}
}

19
src/main/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "../../config/tsconfig-base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"/@/*": [
"./*"
]
},
"types": [
"../../types/env",
"vite/client",
"electron-devtools-installer"
]
},
"files": [
"./index.ts"
]
}

55
src/preload/index.ts Normal file
View File

@ -0,0 +1,55 @@
import {ContextBridge, contextBridge} from 'electron';
const apiKey = 'electron';
/**
* @see https://github.com/electron/electron/issues/21437#issuecomment-573522360
*/
const api = {
versions: process.versions,
} as const;
export type ExposedInMainWorld = Readonly<typeof api>;
if (import.meta.env.MODE !== 'test') {
/**
* The "Main World" is the JavaScript context that your main renderer code runs in.
* By default, the page you load in your renderer executes code in this world.
*
* @see https://www.electronjs.org/docs/api/context-bridge
*/
contextBridge.exposeInMainWorld(apiKey, api);
} else {
type API = Parameters<ContextBridge['exposeInMainWorld']>[1]
/**
* Recursively Object.freeze() on objects and functions
* @see https://github.com/substack/deep-freeze
* @param obj Object on which to lock the attributes
*/
function deepFreeze<T extends API>(obj: T): Readonly<T> {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj.hasOwnProperty(prop)
&& obj[prop] !== null
&& (typeof obj[prop] === 'object' || typeof obj[prop] === 'function')
&& !Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop]);
}
});
return obj;
}
deepFreeze(api);
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-var-requires
(window as any).electronRequire = require;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any)[apiKey] = api;
}

28
src/preload/tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"sourceMap": true,
"moduleResolution": "Node",
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"baseUrl": ".",
"paths": {
"/@/*": [
"./*"
]
},
"types": [
"../../types/env",
"vite/client",
]
},
"files": [
"./index.ts"
],
"exclude": [
"**/node_modules",
"**/.*"
]
}

15
src/renderer/.eslintrc.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
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',
},
};

31
src/renderer/App.vue Normal file
View File

@ -0,0 +1,31 @@
<template>
<img
alt="Vue logo"
src="./assets/logo.png"
>
<app-navigation />
<router-view />
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import AppNavigation from '/@/components/AppNavigation.vue';
export default defineComponent({
name: 'App',
components: {
AppNavigation,
},
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,37 @@
<template>
<h2 id="versions">
Lib versions
</h2>
<div>
<ul aria-labelledby="versions">
<li
v-for="(version, lib) in versions"
:key="lib"
>
<strong>{{ lib }}</strong>: v{{ version }}
</li>
</ul>
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import {useElectron} from '/@/use/electron';
export default defineComponent({
name: 'App',
setup() {
const {versions} = useElectron();
// It makes no sense to make "versions" reactive
return {versions};
},
});
</script>
<style scoped>
div {
text-align: left;
display: grid;
justify-content: center;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<nav>
<router-link to="/">
Home
</router-link>
<span> | </span>
<router-link to="/about">
About
</router-link>
</nav>
</template>
<script lang="ts">
import {defineComponent} from 'vue';
export default defineComponent({
name: 'AppNavigation',
});
</script>
<style scoped>
nav {
display: flex;
gap: 1em;
justify-content: center;
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<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"
rel="noopener"
target="_blank"
>vite-electron-builder documentation</a>.
</p>
<p>
<a
href="https://vitejs.dev/guide/features.html"
target="_blank"
>Vite Documentation</a> |
<a
href="https://v3.vuejs.org/"
target="_blank"
>Vue 3 Documentation</a>
</p>
<hr>
<button @click="count++">
count is: {{ count }}
</button>
<p>
Edit
<code>renderer/components/Home.vue</code> to test hot module replacement.
</p>
</template>
<script lang="ts">
import {defineComponent, ref} from 'vue';
export default defineComponent({
name: 'HelloWorld',
setup() {
const count = ref(0);
return {count};
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
a {
color: #42b983;
}
</style>

13
src/renderer/index.html Normal file
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="./index.ts" type="module"></script>
</body>
</html>

7
src/renderer/index.ts Normal file
View File

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

12
src/renderer/router.ts Normal file
View File

@ -0,0 +1,12 @@
import {createRouter, createWebHashHistory} from 'vue-router';
import Home from '/@/components/Home.vue';
const routes = [
{path: '/', name: 'Home', component: Home},
{path: '/about', name: 'About', component: () => import('/@/components/About.vue')}, // Lazy load route component
];
export default createRouter({
routes,
history: createWebHashHistory(),
});

View File

@ -0,0 +1,19 @@
{
"extends": "../../config/tsconfig-base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"/@/*": [
"./*"
]
},
"types": [
"../../types/env",
"vite/client",
"./types/shims-vue"
]
},
"files": [
"./index.ts"
]
}

5
src/renderer/types/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import type {DefineComponent} from 'vue';
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, never>;
export default component;
}

View File

@ -0,0 +1,6 @@
import type {ExposedInMainWorld} from '../../preload';
export function useElectron(): ExposedInMainWorld {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (window as any).electron as ExposedInMainWorld;
}

8
tests/.eslintrc.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
env: {
node: true,
},
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
};

43
tests/app.spec.js Normal file
View File

@ -0,0 +1,43 @@
const {Application} = require('spectron');
const {strict: assert} = require('assert');
const {join} = require('path');
const app = new Application({
path: require('electron'),
requireName: 'electronRequire',
args: [join(__dirname, '../dist/source')],
});
app.start()
.then(async () => {
const isVisible = await app.browserWindow.isVisible();
assert.ok(isVisible, 'Main window not visible');
})
.then(async () => {
const isDevtoolsOpen = await app.webContents.isDevToolsOpened();
assert.ok(!isDevtoolsOpen, 'DevTools opened');
})
.then(async function () {
// Get the window content
const content = await app.client.$('#app');
assert.notStrictEqual(await content.getHTML(), '<div id="app"></div>', 'Window content is empty');
})
.then(function () {
if (app && app.isRunning()) {
return app.stop();
}
})
.then(() => process.exit(0))
.catch(function (error) {
console.error(error);
if (app && app.isRunning()) {
app.stop();
}
process.exit(1);
});

16
tests/url.spec.js Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/node
const assert = require ('assert');
const {format} = require ('url');
const {join} = require ('path');
const expected = format({
protocol: 'file',
pathname: join(__dirname, '../renderer/index.html'),
slashes: true,
});
const actual = new URL('renderer/index.html', 'file://' + __dirname);
assert.strictEqual(actual.toString(), expected);

25
vetur.config.js Normal file
View File

@ -0,0 +1,25 @@
/** @type {import('vls').VeturConfig} */
module.exports = {
settings: {
'vetur.useWorkspaceDependencies': true,
'vetur.experimental.templateInterpolationService': true,
},
projects: [
{
root: './src/renderer',
tsconfig: './tsconfig.json',
snippetFolder: './.vscode/vetur/snippets',
globalComponents: [
'./components/**/*.vue',
],
},
{
root: './src/main',
tsconfig: './tsconfig.json',
},
{
root: './src/preload',
tsconfig: './tsconfig.json',
},
],
};

4466
yarn.lock Normal file

File diff suppressed because it is too large Load Diff