├── .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 | 
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 |
--------------------------------------------------------------------------------