├── .nvmrc ├── apps ├── electron │ ├── buildResources │ │ ├── .gitkeep │ │ ├── icon.png │ │ └── icon.icns │ ├── .browserslistrc │ ├── .electron-vendors.cache.json │ ├── layers │ │ ├── preload │ │ │ ├── src │ │ │ │ ├── sha256sum.ts │ │ │ │ └── index.ts │ │ │ ├── exposedInMainWorld.d.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ └── main │ │ │ ├── tsconfig.json │ │ │ ├── vite.config.js │ │ │ └── src │ │ │ ├── index.ts │ │ │ ├── mainWindow.ts │ │ │ └── security-restrictions.ts │ ├── .editorconfig │ ├── .electron-builder.config.js │ ├── types │ │ └── env.d.ts │ ├── LICENSE │ ├── scripts │ │ ├── update-electron-vendors.js │ │ └── watch.js │ ├── contributing.md │ └── package.json └── web │ ├── .eslintrc.js │ ├── src │ ├── styles │ │ └── globals.css │ └── pages │ │ ├── _app.tsx │ │ └── index.tsx │ ├── postcss.config.js │ ├── tailwind.config.js │ ├── next-env.d.ts │ ├── tsconfig.json │ └── package.json ├── assets └── example.png ├── packages ├── ui │ ├── index.tsx │ ├── Button.tsx │ ├── tsconfig.json │ └── package.json ├── tsconfig │ ├── README.md │ ├── package.json │ ├── react-library.json │ ├── base.json │ └── nextjs.json └── config │ ├── eslint-preset.js │ └── package.json ├── turbo.json ├── .gitignore ├── README.md ├── package.json └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.13.0 -------------------------------------------------------------------------------- /apps/electron/buildResources/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/electron/.browserslistrc: -------------------------------------------------------------------------------- 1 | Chrome 98 2 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("config/eslint-preset"); 2 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/yerba/HEAD/assets/example.png -------------------------------------------------------------------------------- /packages/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | export * from "./Button"; 3 | -------------------------------------------------------------------------------- /apps/electron/.electron-vendors.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrome": "98", 3 | "node": "16" 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /apps/electron/buildResources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/yerba/HEAD/apps/electron/buildResources/icon.png -------------------------------------------------------------------------------- /apps/electron/buildResources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/yerba/HEAD/apps/electron/buildResources/icon.icns -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | export const Button = () => { 3 | return ; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "files": [ 7 | "base.json", 8 | "nextjs.json", 9 | "react-library.json" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /apps/electron/layers/preload/src/sha256sum.ts: -------------------------------------------------------------------------------- 1 | import type {BinaryLike} from "crypto"; 2 | import {createHash} from "crypto"; 3 | 4 | export function sha256sum(data: BinaryLike) { 5 | return createHash("sha256") 6 | .update(data) 7 | .digest("hex"); 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": [ 4 | "next-env.d.ts", 5 | "**/*.ts", 6 | "**/*.tsx", 7 | "../electron/layers/preload/exposedInMainWorld.d.ts" 8 | ], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/config/eslint-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "prettier"], 3 | settings: { 4 | next: { 5 | rootDir: ["apps/*/", "packages/*/"], 6 | }, 7 | }, 8 | rules: { 9 | "@next/next/no-html-link-for-pages": "off", 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/electron/layers/preload/exposedInMainWorld.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | readonly yerba: { version: number; }; 3 | /** 4 | * Safe expose node.js API 5 | * @example 6 | * window.nodeCrypto('data') 7 | */ 8 | readonly nodeCrypto: { sha256sum: any; }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "files": [ 7 | "eslint-preset.js" 8 | ], 9 | "dependencies": { 10 | "eslint-config-next": "^12.0.8", 11 | "eslint-config-prettier": "^8.3.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "lib": ["ES2015"], 7 | "module": "ESNext", 8 | "target": "ES6", 9 | "jsx": "react-jsx" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@types/react": "^17.0.37", 9 | "@types/react-dom": "^17.0.11", 10 | "tsconfig": "*", 11 | "config": "*", 12 | "typescript": "^4.5.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "npm", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**", ".next/**"] 7 | }, 8 | "compile": { 9 | "dependsOn": ["^build", "^compile"], 10 | "outputs": ["dist/**", ".next/**"] 11 | }, 12 | "lint": { 13 | "outputs": [] 14 | }, 15 | "dev": { 16 | "cache": false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/electron/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # https://github.com/jokeyrhyme/standard-editorconfig 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # defaults 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | dist/ 8 | 9 | # testing 10 | coverage 11 | 12 | # next.js 13 | .next/ 14 | out/ 15 | build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .pnpm-debug.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yerba 2 | 3 | ![assets/example.png](assets/example.png) 4 | 5 | An Electron Monorepo Demo 6 | 7 | Uses: 8 | 9 | - Next.js 10 | - Typescript 11 | - Tailwind 12 | - Turborepo 13 | - Vite (for Electron builds) 14 | 15 | ## Getting Started 16 | 17 | ```bash 18 | npm install 19 | npm run dev 20 | ``` 21 | 22 | ## Prior work 23 | 24 | Most of this code is generously borrowed from the following 25 | 26 | - [vite-electron-builder](https://github.com/cawa-93/vite-electron-builder) 27 | - [Turborepo basic example](https://github.com/vercel/turborepo/tree/main/examples/basic) 28 | -------------------------------------------------------------------------------- /apps/electron/layers/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": false, 6 | "moduleResolution": "Node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "isolatedModules": true, 10 | 11 | "types" : ["node"], 12 | 13 | "baseUrl": ".", 14 | "paths": { 15 | "/@/*": [ 16 | "./src/*" 17 | ] 18 | }, 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "../../types/**/*.d.ts" 23 | ], 24 | "exclude": [ 25 | "**/*.spec.ts", 26 | "**/*.test.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /apps/electron/layers/preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": false, 6 | "moduleResolution": "Node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "isolatedModules": true, 10 | 11 | "types" : ["node"], 12 | 13 | "baseUrl": ".", 14 | "paths": { 15 | "/@/*": [ 16 | "./src/*" 17 | ] 18 | } 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "../../types/**/*.d.ts" 23 | ], 24 | "exclude": [ 25 | "**/*.spec.ts", 26 | "**/*.test.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turborepo-basic-shared", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev --parallel", 12 | "compile": "turbo run compile", 13 | "lint": "turbo run lint", 14 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 15 | }, 16 | "devDependencies": { 17 | "prettier": "^2.5.1", 18 | "turbo": "latest" 19 | }, 20 | "engines": { 21 | "npm": ">=7.0.0", 22 | "node": ">=14.0.0" 23 | }, 24 | "packageManager": "npm@7.5.3" 25 | } 26 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "12.0.8", 13 | "react": "17.0.2", 14 | "react-dom": "17.0.2" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^17.0.12", 18 | "@types/react": "17.0.37", 19 | "autoprefixer": "^10.4.2", 20 | "config": "*", 21 | "eslint": "7.32.0", 22 | "next-transpile-modules": "9.0.0", 23 | "postcss": "^8.4.6", 24 | "tailwindcss": "^3.0.18", 25 | "tsconfig": "*", 26 | "typescript": "^4.5.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/electron/.electron-builder.config.js: -------------------------------------------------------------------------------- 1 | if (process.env.VITE_APP_VERSION === undefined) { 2 | const now = new Date(); 3 | process.env.VITE_APP_VERSION = `${now.getUTCFullYear() - 2000}.${ 4 | now.getUTCMonth() + 1 5 | }.${now.getUTCDate()}-${now.getUTCHours() * 60 + now.getUTCMinutes()}`; 6 | } 7 | 8 | /** 9 | * @type {import('electron-builder').Configuration} 10 | * @see https://www.electron.build/configuration/configuration 11 | */ 12 | const config = { 13 | directories: { 14 | output: "dist", 15 | buildResources: "buildResources", 16 | }, 17 | files: ["layers/**/dist/**"], 18 | extraMetadata: { 19 | version: process.env.VITE_APP_VERSION, 20 | }, 21 | }; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /apps/electron/types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * Describes all existing environment variables and their types. 5 | * Required for Code completion and type checking 6 | * 7 | * Note: To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code 8 | * 9 | * @see https://github.com/vitejs/vite/blob/cab55b32de62e0de7d7789e8c2a1f04a8eae3a3f/packages/vite/types/importMeta.d.ts#L62-L69 Base Interface 10 | * @see https://vitejs.dev/guide/env-and-mode.html#env-files Vite Env Variables Doc 11 | */ 12 | interface ImportMetaEnv { 13 | /** 14 | * The value of the variable is set in scripts/watch.js and depend on layers/main/vite.config.js 15 | */ 16 | readonly VITE_DEV_SERVER_URL: undefined | string; 17 | } 18 | 19 | interface ImportMeta { 20 | readonly env: ImportMetaEnv; 21 | } 22 | -------------------------------------------------------------------------------- /apps/electron/layers/preload/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module preload 3 | */ 4 | 5 | import { contextBridge } from "electron"; 6 | import { sha256sum } from "/@/sha256sum"; 7 | import * as fs from "fs"; 8 | 9 | // Expose version number to renderer 10 | contextBridge.exposeInMainWorld("yerba", { version: 0.1 }); 11 | 12 | /** 13 | * The "Main World" is the JavaScript context that your main renderer code runs in. 14 | * By default, the page you load in your renderer executes code in this world. 15 | * 16 | * @see https://www.electronjs.org/docs/api/context-bridge 17 | */ 18 | 19 | /** 20 | * After analyzing the `exposeInMainWorld` calls, 21 | * `packages/preload/exposedInMainWorld.d.ts` file will be generated. 22 | * It contains all interfaces. 23 | * `packages/preload/exposedInMainWorld.d.ts` file is required for TS is `renderer` 24 | * 25 | * @see https://github.com/cawa-93/dts-for-context-bridge 26 | */ 27 | 28 | /** 29 | * Safe expose node.js API 30 | * @example 31 | * window.nodeCrypto('data') 32 | */ 33 | contextBridge.exposeInMainWorld("nodeCrypto", { sha256sum }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Theo Browne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /apps/electron/layers/preload/vite.config.js: -------------------------------------------------------------------------------- 1 | import {chrome} from "../../.electron-vendors.cache.json"; 2 | import {join} from "path"; 3 | import {builtinModules} from "module"; 4 | 5 | const PACKAGE_ROOT = __dirname; 6 | 7 | /** 8 | * @type {import('vite').UserConfig} 9 | * @see https://vitejs.dev/config/ 10 | */ 11 | const config = { 12 | mode: process.env.MODE, 13 | root: PACKAGE_ROOT, 14 | envDir: process.cwd(), 15 | resolve: { 16 | alias: { 17 | "/@/": join(PACKAGE_ROOT, "src") + "/", 18 | }, 19 | }, 20 | build: { 21 | sourcemap: "inline", 22 | target: `chrome${chrome}`, 23 | outDir: "dist", 24 | assetsDir: ".", 25 | minify: process.env.MODE !== "development", 26 | lib: { 27 | entry: "src/index.ts", 28 | formats: ["cjs"], 29 | }, 30 | rollupOptions: { 31 | external: [ 32 | "electron", 33 | ...builtinModules.flatMap(p => [p, `node:${p}`]), 34 | ], 35 | output: { 36 | entryFileNames: "[name].cjs", 37 | }, 38 | }, 39 | emptyOutDir: true, 40 | brotliSize: false, 41 | }, 42 | }; 43 | 44 | export default config; 45 | -------------------------------------------------------------------------------- /apps/web/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps /*, AppContext */ } from "next/app"; 2 | import Head from "next/head"; 3 | import React from "react"; 4 | import "../styles/globals.css"; 5 | 6 | const SafeAppContents = ({ Component, pageProps }: AppProps) => { 7 | const [mounted, setMounted] = React.useState(false); 8 | React.useEffect(() => { 9 | setMounted(true); 10 | }, []); 11 | 12 | if (!mounted) { 13 | return
Loading...
; 14 | } 15 | 16 | // Lock out users on old versions 17 | if (window?.yerba?.version < 0.1) { 18 | return
Please update your app
; 19 | } 20 | 21 | // Lock out SSR and browser users 22 | if (typeof window === "undefined" || !window?.yerba?.version) { 23 | return
Please use the app
; 24 | } 25 | 26 | // Only render if top two conditions pass 27 | return ; 28 | }; 29 | 30 | function AppWrapper(props: AppProps) { 31 | return ( 32 | <> 33 | 34 | Yerba 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default AppWrapper; 42 | -------------------------------------------------------------------------------- /apps/electron/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alex Kozack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/electron/layers/main/vite.config.js: -------------------------------------------------------------------------------- 1 | import { node } from "../../.electron-vendors.cache.json"; 2 | import { join } from "path"; 3 | import { builtinModules } from "module"; 4 | 5 | const PACKAGE_ROOT = __dirname; 6 | 7 | /** 8 | * @type {import('vite').UserConfig} 9 | * @see https://vitejs.dev/config/ 10 | */ 11 | const config = { 12 | mode: process.env.MODE, 13 | root: PACKAGE_ROOT, 14 | envDir: process.cwd(), 15 | resolve: { 16 | alias: { 17 | "/@/": join(PACKAGE_ROOT, "src") + "/", 18 | }, 19 | }, 20 | build: { 21 | sourcemap: "inline", 22 | target: `node${node}`, 23 | outDir: "dist", 24 | assetsDir: ".", 25 | minify: process.env.MODE !== "development", 26 | lib: { 27 | entry: "src/index.ts", 28 | formats: ["cjs"], 29 | }, 30 | rollupOptions: { 31 | external: [ 32 | "electron", 33 | "electron-devtools-installer", 34 | ...builtinModules.flatMap((p) => [p, `node:${p}`]), 35 | ], 36 | output: { 37 | entryFileNames: "[name].cjs", 38 | }, 39 | }, 40 | emptyOutDir: true, 41 | brotliSize: false, 42 | }, 43 | }; 44 | 45 | export default config; 46 | -------------------------------------------------------------------------------- /apps/electron/scripts/update-electron-vendors.js: -------------------------------------------------------------------------------- 1 | const {writeFile} = require("fs/promises"); 2 | const {execSync} = require("child_process"); 3 | const electron = require("electron"); 4 | const path = require("path"); 5 | 6 | /** 7 | * Returns versions of electron vendors 8 | * The performance of this feature is very poor and can be improved 9 | * @see https://github.com/electron/electron/issues/28006 10 | * 11 | * @returns {NodeJS.ProcessVersions} 12 | */ 13 | function getVendors() { 14 | const output = execSync(`${electron} -p "JSON.stringify(process.versions)"`, { 15 | env: {"ELECTRON_RUN_AS_NODE": "1"}, 16 | encoding: "utf-8", 17 | }); 18 | 19 | return JSON.parse(output); 20 | } 21 | 22 | function updateVendors() { 23 | const electronRelease = getVendors(); 24 | 25 | const nodeMajorVersion = electronRelease.node.split(".")[0]; 26 | const chromeMajorVersion = electronRelease.v8.split(".")[0] + electronRelease.v8.split(".")[1]; 27 | 28 | const browserslistrcPath = path.resolve(process.cwd(), ".browserslistrc"); 29 | 30 | return Promise.all([ 31 | writeFile("./.electron-vendors.cache.json", 32 | JSON.stringify({ 33 | chrome: chromeMajorVersion, 34 | node: nodeMajorVersion, 35 | }, null, 2) + "\n", 36 | ), 37 | 38 | writeFile(browserslistrcPath, `Chrome ${chromeMajorVersion}\n`, "utf8"), 39 | ]); 40 | } 41 | 42 | updateVendors().catch(err => { 43 | console.error(err); 44 | process.exit(1); 45 | }); 46 | -------------------------------------------------------------------------------- /apps/web/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | export default function Web() { 4 | return ( 5 |
6 |
7 |

8 | Yerba: An Electron Monorepo Demo 9 |

10 |
11 |
Using...
12 |
    13 |
  • Electron
  • 14 |
  • Vite
  • 15 |
  • TurboRepo
  • 16 |
  • Next.js
  • 17 |
  • Typescript
  • 18 |
  • Tailwind Monorepo
  • 19 |
20 |
21 |
22 |
...yeah this kinda sucked to figure out
23 |
24 |
Wanna see some typesafe data?
25 |
26 | {"Yerba version: "} 27 | {window.yerba.version} 28 |
29 |
30 | 31 | {"Hashed Yerba version using node's builtin crypto: "} 32 | 33 | {window.nodeCrypto.sha256sum(window.yerba.version.toString())} 34 |
35 |
36 | Quickly hacked together by Theo 37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/electron/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 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. 4 | 5 | ## Issues 6 | 7 | Do not create issues about bumping dependencies unless a bug has been identified, and you can demonstrate that it effects this library. 8 | 9 | **Help us to help you** 10 | 11 | Remember that we’re here to help, but not to make guesses about what you need help with: 12 | 13 | - Whatever bug or issue you're experiencing, assume that it will not be as obvious to the maintainers as it is to you. 14 | - 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. 15 | 16 | _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._ 17 | 18 | 19 | ## Repo Setup 20 | The package manager used to install and link dependencies must be npm v7 or later. 21 | 22 | 1. Clone repo 23 | 1. `npm run watch` start electron app in watch mode. 24 | 1. `npm run compile` build app but for local debugging only. 25 | 1. `npm run lint` lint your code. 26 | 1. `npm run typecheck` Run typescript check. 27 | 1. `npm run test` Run app test. 28 | -------------------------------------------------------------------------------- /apps/electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-wrapper", 3 | "version": "0.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=v16.13", 7 | "npm": ">=8.1" 8 | }, 9 | "main": "layers/main/dist/index.cjs", 10 | "scripts": { 11 | "dev": "node scripts/watch.js", 12 | "build": "npm run build:main && npm run build:preload", 13 | "build:main": "cd ./layers/main && vite build", 14 | "build:preload": "cd ./layers/preload && vite build", 15 | "build:preload:types": "dts-cb -i \"layers/preload/tsconfig.json\" -o \"layers/preload/exposedInMainWorld.d.ts\"", 16 | "compile": "cross-env MODE=production npm run build && electron-builder build --config .electron-builder.config.js --dir --config.asar=false", 17 | "watch": "node scripts/watch.js", 18 | "lint": "eslint . --ext js,ts", 19 | "typecheck:main": "tsc --noEmit -p layers/main/tsconfig.json", 20 | "typecheck:preload": "tsc --noEmit -p layers/preload/tsconfig.json", 21 | "typecheck": "npm run typecheck:main && npm run typecheck:preload" 22 | }, 23 | "devDependencies": { 24 | "@typescript-eslint/eslint-plugin": "5.10.2", 25 | "cross-env": "7.0.3", 26 | "dts-for-context-bridge": "0.7.1", 27 | "electron": "17.0.0", 28 | "electron-builder": "22.14.5", 29 | "electron-devtools-installer": "3.2.0", 30 | "eslint": "8.8.0", 31 | "eslint-plugin-vue": "8.4.0", 32 | "typescript": "4.5.5", 33 | "vite": "2.7.13" 34 | }, 35 | "dependencies": { 36 | "electron-updater": "4.6.5", 37 | "react": "^17.0.2", 38 | "react-dom": "^17.0.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/electron/layers/main/src/index.ts: -------------------------------------------------------------------------------- 1 | import {app} from "electron"; 2 | import "./security-restrictions"; 3 | import {restoreOrCreateWindow} from "/@/mainWindow"; 4 | 5 | 6 | /** 7 | * Prevent multiple instances 8 | */ 9 | const isSingleInstance = app.requestSingleInstanceLock(); 10 | if (!isSingleInstance) { 11 | app.quit(); 12 | process.exit(0); 13 | } 14 | app.on("second-instance", restoreOrCreateWindow); 15 | 16 | 17 | /** 18 | * Disable Hardware Acceleration for more power-save 19 | */ 20 | app.disableHardwareAcceleration(); 21 | 22 | /** 23 | * Shout down background process if all windows was closed 24 | */ 25 | app.on("window-all-closed", () => { 26 | if (process.platform !== "darwin") { 27 | app.quit(); 28 | } 29 | }); 30 | 31 | /** 32 | * @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate' 33 | */ 34 | app.on("activate", restoreOrCreateWindow); 35 | 36 | 37 | /** 38 | * Create app window when background process will be ready 39 | */ 40 | app.whenReady() 41 | .then(restoreOrCreateWindow) 42 | .catch((e) => console.error("Failed create window:", e)); 43 | 44 | 45 | /** 46 | * Install Vue.js or some other devtools in development mode only 47 | */ 48 | if (import.meta.env.DEV) { 49 | app.whenReady() 50 | .then(() => import("electron-devtools-installer")) 51 | .then(({default: installExtension, VUEJS3_DEVTOOLS}) => installExtension(VUEJS3_DEVTOOLS, { 52 | loadExtensionOptions: { 53 | allowFileAccess: true, 54 | }, 55 | })) 56 | .catch(e => console.error("Failed install extension:", e)); 57 | } 58 | 59 | /** 60 | * Check new app version in production mode only 61 | */ 62 | if (import.meta.env.PROD) { 63 | app.whenReady() 64 | .then(() => import("electron-updater")) 65 | .then(({autoUpdater}) => autoUpdater.checkForUpdatesAndNotify()) 66 | .catch((e) => console.error("Failed check updates:", e)); 67 | } 68 | 69 | -------------------------------------------------------------------------------- /apps/electron/layers/main/src/mainWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from "electron"; 2 | import { join } from "path"; 3 | 4 | async function createWindow() { 5 | const browserWindow = new BrowserWindow({ 6 | show: false, // Use 'ready-to-show' event to show window 7 | webPreferences: { 8 | nativeWindowOpen: true, 9 | 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 10 | preload: join(__dirname, "../../preload/dist/index.cjs"), 11 | }, 12 | }); 13 | 14 | /** 15 | * If you install `show: true` then it can cause issues when trying to close the window. 16 | * Use `show: false` and listener events `ready-to-show` to fix these issues. 17 | * 18 | * @see https://github.com/electron/electron/issues/25012 19 | */ 20 | browserWindow.on("ready-to-show", () => { 21 | browserWindow?.show(); 22 | 23 | if (import.meta.env.DEV) { 24 | browserWindow?.webContents.openDevTools(); 25 | } 26 | }); 27 | 28 | /** 29 | * URL for main window. 30 | * Vite dev server for development. 31 | * `file://../renderer/index.html` for production and test 32 | */ 33 | const pageUrl = 34 | import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined 35 | ? import.meta.env.VITE_DEV_SERVER_URL 36 | : "https://yerba.ping.gg"; 37 | 38 | await browserWindow.loadURL(pageUrl); 39 | 40 | return browserWindow; 41 | } 42 | 43 | /** 44 | * Restore existing BrowserWindow or Create new BrowserWindow 45 | */ 46 | export async function restoreOrCreateWindow() { 47 | let window = BrowserWindow.getAllWindows().find((w) => !w.isDestroyed()); 48 | 49 | if (window === undefined) { 50 | window = await createWindow(); 51 | } 52 | 53 | if (window.isMinimized()) { 54 | window.restore(); 55 | } 56 | 57 | window.focus(); 58 | } 59 | -------------------------------------------------------------------------------- /apps/electron/scripts/watch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { createServer, build, createLogger } = require("vite"); 4 | const electronPath = require("electron"); 5 | const { spawn } = require("child_process"); 6 | const { generateAsync } = require("dts-for-context-bridge"); 7 | 8 | /** @type 'production' | 'development'' */ 9 | const mode = (process.env.MODE = process.env.MODE || "development"); 10 | 11 | /** @type {import('vite').LogLevel} */ 12 | const LOG_LEVEL = "info"; 13 | 14 | /** @type {import('vite').InlineConfig} */ 15 | const sharedConfig = { 16 | mode, 17 | build: { 18 | watch: {}, 19 | }, 20 | logLevel: LOG_LEVEL, 21 | }; 22 | 23 | /** Messages on stderr that match any of the contained patterns will be stripped from output */ 24 | const stderrFilterPatterns = [ 25 | // warning about devtools extension 26 | // https://github.com/cawa-93/vite-electron-builder/issues/492 27 | // https://github.com/MarshallOfSound/electron-devtools-installer/issues/143 28 | /ExtensionLoadWarning/, 29 | ]; 30 | 31 | /** 32 | * @param {{name: string; configFile: string; writeBundle: import('rollup').OutputPlugin['writeBundle'] }} param0 33 | */ 34 | const getWatcher = ({ name, configFile, writeBundle }) => { 35 | return build({ 36 | ...sharedConfig, 37 | configFile, 38 | plugins: [{ name, writeBundle }], 39 | }); 40 | }; 41 | 42 | /** 43 | * Start or restart App when source files are changed 44 | * @param {{config: {server: import('vite').ResolvedServerOptions}}} ResolvedServerOptions 45 | */ 46 | const setupMainPackageWatcher = ({ config: { server } }) => { 47 | // Create VITE_DEV_SERVER_URL environment variable to pass it to the main process. 48 | { 49 | const protocol = server.https ? "https:" : "http:"; 50 | const host = server.host || "localhost"; 51 | const port = server.port; // Vite searches for and occupies the first free port: 3000, 3001, 3002 and so on 52 | const path = "/"; 53 | process.env.VITE_DEV_SERVER_URL = `${protocol}//${host}:${port}${path}`; 54 | } 55 | 56 | const logger = createLogger(LOG_LEVEL, { 57 | prefix: "[main]", 58 | }); 59 | 60 | /** @type {ChildProcessWithoutNullStreams | null} */ 61 | let spawnProcess = null; 62 | 63 | return getWatcher({ 64 | name: "reload-app-on-main-package-change", 65 | configFile: "layers/main/vite.config.js", 66 | writeBundle() { 67 | if (spawnProcess !== null) { 68 | spawnProcess.off("exit", process.exit); 69 | spawnProcess.kill("SIGINT"); 70 | spawnProcess = null; 71 | } 72 | 73 | spawnProcess = spawn(String(electronPath), ["."]); 74 | 75 | spawnProcess.stdout.on( 76 | "data", 77 | (d) => 78 | d.toString().trim() && logger.warn(d.toString(), { timestamp: true }) 79 | ); 80 | spawnProcess.stderr.on("data", (d) => { 81 | const data = d.toString().trim(); 82 | if (!data) return; 83 | const mayIgnore = stderrFilterPatterns.some((r) => r.test(data)); 84 | if (mayIgnore) return; 85 | logger.error(data, { timestamp: true }); 86 | }); 87 | 88 | // Stops the watch script when the application has been quit 89 | spawnProcess.on("exit", process.exit); 90 | }, 91 | }); 92 | }; 93 | 94 | /** 95 | * Start or restart App when source files are changed 96 | * @param {{ws: import('vite').WebSocketServer}} WebSocketServer 97 | */ 98 | const setupPreloadPackageWatcher = ({ ws }) => 99 | getWatcher({ 100 | name: "reload-page-on-preload-package-change", 101 | configFile: "layers/preload/vite.config.js", 102 | writeBundle() { 103 | // Generating exposedInMainWorld.d.ts when preload package is changed. 104 | generateAsync({ 105 | input: "layers/preload/src/**/*.ts", 106 | output: "layers/preload/exposedInMainWorld.d.ts", 107 | }); 108 | 109 | ws.send({ 110 | type: "full-reload", 111 | }); 112 | }, 113 | }); 114 | 115 | (async () => { 116 | try { 117 | const viteDevServer = await createServer({ 118 | ...sharedConfig, 119 | }); 120 | 121 | await viteDevServer.listen(); 122 | 123 | await setupPreloadPackageWatcher(viteDevServer); 124 | await setupMainPackageWatcher(viteDevServer); 125 | } catch (e) { 126 | console.error(e); 127 | process.exit(1); 128 | } 129 | })(); 130 | -------------------------------------------------------------------------------- /apps/electron/layers/main/src/security-restrictions.ts: -------------------------------------------------------------------------------- 1 | import { app, shell } from "electron"; 2 | import { URL } from "url"; 3 | 4 | /** 5 | * List of origins that you allow open INSIDE the application and permissions for each of them. 6 | * 7 | * In development mode you need allow open `VITE_DEV_SERVER_URL` 8 | */ 9 | const ALLOWED_ORIGINS_AND_PERMISSIONS = new Map< 10 | string, 11 | Set< 12 | | "clipboard-read" 13 | | "media" 14 | | "display-capture" 15 | | "mediaKeySystem" 16 | | "geolocation" 17 | | "notifications" 18 | | "midi" 19 | | "midiSysex" 20 | | "pointerLock" 21 | | "fullscreen" 22 | | "openExternal" 23 | | "unknown" 24 | > 25 | >( 26 | import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL 27 | ? [[new URL(import.meta.env.VITE_DEV_SERVER_URL).origin, new Set()]] 28 | : [] 29 | ); 30 | 31 | /** 32 | * List of origins that you allow open IN BROWSER. 33 | * Navigation to origins below is possible only if the link opens in a new window 34 | * 35 | * @example 36 | * 40 | */ 41 | const ALLOWED_EXTERNAL_ORIGINS = new Set<`https://${string}`>([ 42 | "https://github.com", 43 | "https://yerba.vercel.app", 44 | "https://yerba.ping.gg", 45 | ]); 46 | 47 | app.on("web-contents-created", (_, contents) => { 48 | /** 49 | * Block navigation to origins not on the allowlist. 50 | * 51 | * Navigation is a common attack vector. If an attacker can convince the app to navigate away 52 | * from its current page, they can possibly force the app to open web sites on the Internet. 53 | * 54 | * @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation 55 | */ 56 | contents.on("will-navigate", (event, url) => { 57 | const { origin } = new URL(url); 58 | if (ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) { 59 | return; 60 | } 61 | 62 | // Prevent navigation 63 | event.preventDefault(); 64 | 65 | if (import.meta.env.DEV) { 66 | console.warn("Blocked navigating to an unallowed origin:", origin); 67 | } 68 | }); 69 | 70 | /** 71 | * Block requested unallowed permissions. 72 | * By default, Electron will automatically approve all permission requests. 73 | * 74 | * @see https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content 75 | */ 76 | contents.session.setPermissionRequestHandler( 77 | (webContents, permission, callback) => { 78 | const { origin } = new URL(webContents.getURL()); 79 | 80 | const permissionGranted = 81 | !!ALLOWED_ORIGINS_AND_PERMISSIONS.get(origin)?.has(permission); 82 | callback(permissionGranted); 83 | 84 | if (!permissionGranted && import.meta.env.DEV) { 85 | console.warn( 86 | `${origin} requested permission for '${permission}', but was blocked.` 87 | ); 88 | } 89 | } 90 | ); 91 | 92 | /** 93 | * Hyperlinks to allowed sites open in the default browser. 94 | * 95 | * The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows, 96 | * frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before. 97 | * You should deny any unexpected window creation. 98 | * 99 | * @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows 100 | * @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content 101 | */ 102 | contents.setWindowOpenHandler(({ url }) => { 103 | const { origin } = new URL(url); 104 | 105 | // @ts-expect-error Type checking is performed in runtime 106 | if (ALLOWED_EXTERNAL_ORIGINS.has(origin)) { 107 | // Open default browser 108 | shell.openExternal(url).catch(console.error); 109 | } else if (import.meta.env.DEV) { 110 | console.warn("Blocked the opening of an unallowed origin:", origin); 111 | } 112 | 113 | // Prevent creating new window in application 114 | return { action: "deny" }; 115 | }); 116 | 117 | /** 118 | * Verify webview options before creation 119 | * 120 | * Strip away preload scripts, disable Node.js integration, and ensure origins are on the allowlist. 121 | * 122 | * @see https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation 123 | */ 124 | contents.on("will-attach-webview", (event, webPreferences, params) => { 125 | const { origin } = new URL(params.src); 126 | if (!ALLOWED_ORIGINS_AND_PERMISSIONS.has(origin)) { 127 | if (import.meta.env.DEV) { 128 | console.warn( 129 | `A webview tried to attach ${params.src}, but was blocked.` 130 | ); 131 | } 132 | 133 | event.preventDefault(); 134 | return; 135 | } 136 | 137 | // Strip away preload scripts if unused or verify their location is legitimate 138 | delete webPreferences.preload; 139 | // @ts-expect-error `preloadURL` exists - see https://www.electronjs.org/docs/latest/api/web-contents#event-will-attach-webview 140 | delete webPreferences.preloadURL; 141 | 142 | // Disable Node.js integration 143 | webPreferences.nodeIntegration = false; 144 | }); 145 | }); 146 | --------------------------------------------------------------------------------