├── tsconfig.json ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20221005221528_init │ │ └── migration.sql └── schema.prisma ├── vite.config.ts ├── src ├── client │ ├── tsconfig.json │ └── renderer.ts ├── server │ ├── preload.ts │ ├── tsconfig.json │ ├── ipcRequestHandler.ts │ ├── router.ts │ ├── constants.ts │ ├── main.ts │ └── prisma.ts └── api.d.ts ├── entitlements.mac.plist ├── index.html ├── install-engines-on-mac.js ├── copy-files.js ├── notarize.js ├── package.json ├── electron-builder.yml ├── .gitignore └── README.md /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | {"path": "./src/client"}, 5 | {"path": "./src/server"} 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /prisma/migrations/20221005221528_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "name" TEXT NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vite"; 2 | 3 | export default defineConfig(({mode}) => ({ 4 | build: { 5 | // sourcemaps have to be inline due to https://github.com/electron/electron/issues/22996 6 | sourcemap: "inline", 7 | }, 8 | base: mode === "production" ? "./" : "/", 9 | })); 10 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "strict": true, 5 | "module": "ES6", 6 | "moduleResolution": "Node", 7 | "noEmit": true, 8 | "allowSyntheticDefaultImports": true 9 | }, 10 | "include": [ 11 | "**/*", 12 | "../api.d.ts" 13 | ], 14 | "exclude": [ 15 | "../../node_modules", 16 | "../server" 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /src/server/preload.ts: -------------------------------------------------------------------------------- 1 | import {IElectronAPI, IpcRequest} from "../api"; 2 | import {contextBridge, ipcRenderer} from "electron"; 3 | 4 | const api: IElectronAPI = { 5 | node: () => process.versions.node, 6 | chrome: () => process.versions.chrome, 7 | electron: () => process.versions.electron, 8 | trpc: (req: IpcRequest) => ipcRenderer.invoke('trpc', req), 9 | }; 10 | contextBridge.exposeInMainWorld('appApi', api) 11 | -------------------------------------------------------------------------------- /src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*", 4 | "../api.d.ts" 5 | ], 6 | "exclude": [ 7 | "../../node_modules" 8 | ], 9 | "compilerOptions": { 10 | "target": "es2018", 11 | "module": "commonjs", 12 | "outDir": "../../dist/server", 13 | "strict": true, 14 | "esModuleInterop": true, 15 | "inlineSources": false, 16 | "sourceMap": false, 17 | "removeComments": true, 18 | "allowSyntheticDefaultImports": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | output = "../src/generated/client" 7 | binaryTargets = ["native", "darwin", "darwin-arm64"] 8 | } 9 | 10 | datasource db { 11 | provider = "sqlite" 12 | url = env("DATABASE_URL") 13 | } 14 | 15 | model User { 16 | id Int @id @default(autoincrement()) 17 | name String 18 | } 19 | -------------------------------------------------------------------------------- /src/api.d.ts: -------------------------------------------------------------------------------- 1 | export type IpcRequest = { 2 | body: any; 3 | headers: any; 4 | method: string; 5 | url: string; 6 | }; 7 | 8 | export type IpcResponse = { 9 | body: any; 10 | headers: any; 11 | status: number; 12 | } 13 | 14 | export interface IElectronAPI { 15 | node: () => string; 16 | chrome: () => string; 17 | electron: () => string; 18 | trpc: (req: IpcRequest) => Promise; 19 | } 20 | 21 | declare global { 22 | interface Window { 23 | appApi: IElectronAPI; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.cs.allow-jit 7 | 8 | com.apple.security.cs.allow-unsigned-executable-memory 9 | 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | Hello from Electron renderer! 14 | 15 | 16 |

17 |

Users

18 | 19 |

20 | 
21 | 
22 | 
23 | 


--------------------------------------------------------------------------------
/install-engines-on-mac.js:
--------------------------------------------------------------------------------
 1 | // For packing this Electron app on Mac, we have to have the Prisma binaries for
 2 | // both darwin (for Intel Mac) and darwin-arm64 (for M1 Mac). Those binaries will be included in the extraResources.
 3 | // The correct binary for the platform this app runs on will be selected at runtime.
 4 | // See `qePath` in `src/server/constants.ts`, which gets set with the current platform
 5 | // and passed into the Prisma Client constructor.
 6 | 
 7 | // Specifying both darwin and darwin-arm64 is also necessary in the schema.prisma file, so that the generated
 8 | // client will be able to use either.
 9 | 
10 | if (process.platform === "darwin") {
11 |     const child_process = require('child_process');
12 | 
13 |     child_process.execSync('rm -rf node_modules/@prisma/engines && npm install @prisma/engines', {
14 |         stdio: [0, 1, 2],
15 |         env: {
16 |             ...process.env,
17 |             PRISMA_CLI_BINARY_TARGETS: "darwin,darwin-arm64",
18 |         },
19 |     });
20 | }
21 | 


--------------------------------------------------------------------------------
/copy-files.js:
--------------------------------------------------------------------------------
 1 | const fs = require('fs-extra')
 2 | const path = require("path");
 3 | const replace = require('replace-in-file');
 4 | 
 5 | // fix long prisma loading times caused by scanning from process.cwd(), which returns "/" when run in electron
 6 | // (thus it scans all files on the computer.) See https://github.com/prisma/prisma/issues/8484
 7 | const files = path.join(__dirname, "src", "generated", "client", "**/*.js");
 8 | console.log("looking at files ", files)
 9 | const results = replace.sync({
10 |     files: files,
11 |     from: /process.cwd\(\)/g,
12 |     to: `require('electron').app.getAppPath()`,
13 |     countMatches: true
14 | });
15 | console.log('Replacement results:', results);
16 | 
17 | // Copy the generated prisma client to the dist folder
18 | fs.copySync(path.join(__dirname, "src", "generated"),
19 |     path.join(__dirname, "dist", "generated"),{
20 |         filter: (src, dest) => {
21 |             // Prevent duplicate copy of query engine. It will already be in extraResources in electron-builder.yml
22 |             if (src.match(/query_engine/) || src.match(/libquery_engine/) || src.match(/esm/)){
23 |                 return false;
24 |             }
25 |             return true;
26 |         }
27 |     });
28 | 
29 | 


--------------------------------------------------------------------------------
/notarize.js:
--------------------------------------------------------------------------------
 1 | require('dotenv').config();
 2 | const fs = require('fs')
 3 | const path = require('path')
 4 | const electron_notarize = require('electron-notarize');
 5 | 
 6 | module.exports = async function (params) {
 7 |     if (process.platform !== 'darwin') {
 8 |         return
 9 |     }
10 | 
11 |     console.log('afterSign hook triggered', params)
12 | 
13 |     let appId = 'Your app ID'
14 | 
15 |     let appPath = path.join(
16 |         params.appOutDir,
17 |         `${params.packager.appInfo.productFilename}.app`
18 |     )
19 |     if (!fs.existsSync(appPath)) {
20 |         console.log('skip')
21 |         return
22 |     }
23 | 
24 |     console.log(
25 |         `Notarizing ${appId} found at ${appPath} with Apple ID ${process.env.APPLE_ID}`
26 |     )
27 | 
28 |     try {
29 |         await electron_notarize.notarize({
30 |             tool: 'notarytool',
31 |             appBundleId: appId,
32 |             appPath: appPath,
33 |             appleId: process.env.APPLE_ID,
34 |             appleIdPassword: process.env.APPLE_ID_PASSWORD,
35 |             teamId: process.env.APPLE_TEAM_ID
36 |         })
37 |     } catch (error) {
38 |         console.error(error)
39 |     }
40 | 
41 |     console.log(`Done notarizing ${appId}`)
42 | }
43 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "electron-prisma-trpc-example",
 3 |   "version": "1.3.0",
 4 |   "repository": "https://github.com/awohletz/electron-prisma-trpc-example-releases",
 5 |   "description": "An example repo showing how to use Electron with tRPC and Prisma",
 6 |   "main": "dist/server/main.js",
 7 |   "scripts": {
 8 |     "build": "vite build && prisma generate && tsc --build && node copy-files.js && node install-engines-on-mac.js",
 9 |     "start": "npm run build && cross-env NODE_ENV=development electron .",
10 |     "pack": "npm run build && electron-builder --dir",
11 |     "dist": "npm run build && electron-builder",
12 |     "publish": "npm run build && dotenv -- electron-builder -p always"
13 |   },
14 |   "author": "Ayron Wohletz",
15 |   "license": "ISC",
16 |   "devDependencies": {
17 |     "@types/node": "^20.14.8",
18 |     "cross-env": "^7.0.3",
19 |     "dotenv": "^16.4.5",
20 |     "dotenv-cli": "^7.4.2",
21 |     "electron": "^31.0.2",
22 |     "electron-builder": "^24.13.3",
23 |     "electron-notarize": "^1.2.1",
24 |     "fs-extra": "^11.2.0",
25 |     "replace-in-file": "^7.2.0",
26 |     "typescript": "^5.5.2",
27 |     "vite": "^5.3.1"
28 |   },
29 |   "dependencies": {
30 |     "@prisma/client": "^5.15.1",
31 |     "@prisma/engines": "^5.15.1",
32 |     "@trpc/client": "11.0.0-rc.421",
33 |     "@trpc/server": "11.0.0-rc.421",
34 |     "electron-log": "^5.1.5",
35 |     "fix-esm": "^1.0.1",
36 |     "prisma": "^5.15.1",
37 |     "superjson": "^2.2.1",
38 |     "zod": "^3.23.8"
39 |   }
40 | }
41 | 


--------------------------------------------------------------------------------
/src/server/ipcRequestHandler.ts:
--------------------------------------------------------------------------------
 1 | import {AnyRouter, inferRouterContext} from "@trpc/server";
 2 | import {IpcRequest, IpcResponse} from "../api";
 3 | import {resolveResponse} from "@trpc/server/http";
 4 | 
 5 | export async function ipcRequestHandler(
 6 |   opts: {
 7 |     req: IpcRequest;
 8 |     router: TRouter;
 9 |     allowBatching?: boolean;
10 |     onError?: (o: {error: Error; req: IpcRequest}) => void;
11 |     endpoint: string;
12 |     createContext?: (params: {req: IpcRequest}) => Promise>;
13 |   },
14 | ): Promise {
15 |   const createContext = async () => {
16 |     return opts.createContext?.({ req: opts.req });
17 |   };
18 | 
19 |   // adding a fake "https://electron" to the URL so it can be parsed
20 |   const url = new URL("https://electron" + opts.req.url);
21 |   const path = url.pathname.slice(opts.endpoint.length + 1);
22 |   const response = await resolveResponse({
23 |     req: new Request(url, {
24 |       method: opts.req.method,
25 |       headers: opts.req.headers,
26 |       body: opts.req.body,
27 |     }),
28 |     createContext,
29 |     path,
30 |     router: opts.router,
31 |     allowBatching: opts.allowBatching,
32 |     onError(o) {
33 |       opts?.onError?.({ ...o, req: opts.req });
34 |     },
35 |     error: null
36 |   });
37 | 
38 |   const headersObj: Record = {};
39 |   for (const [k, v] of response.headers) {
40 |     headersObj[k] = v;
41 |   }
42 | 
43 |   return {
44 |     body: await response.json(),
45 |     headers: headersObj,
46 |     status: response.status,
47 |   };
48 | }
49 | 


--------------------------------------------------------------------------------
/src/server/router.ts:
--------------------------------------------------------------------------------
 1 | import {initTRPC} from '@trpc/server';
 2 | import { prisma } from './prisma';
 3 | import * as z from 'zod';
 4 | // HACK: The `superjson` library is ESM-only (does not support CJS), while our codebase is CJS.
 5 | // This is a workaround to still get to use the latest version of the library from our codebase.
 6 | // https://github.com/blitz-js/superjson/issues/268
 7 | // https://www.npmjs.com/package/fix-esm
 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment
 9 | const fixESM = require("fix-esm");
10 | import type SuperJSON from "superjson";
11 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
12 | const superjson: SuperJSON = fixESM.require("superjson");
13 | 
14 | const t = initTRPC.create({
15 |   transformer: superjson
16 | });
17 | 
18 | export const appRouter = t.router({
19 |   users: t.procedure
20 |     .query(() => {
21 |       return prisma.user.findMany();
22 |     }),
23 |   userById: t.procedure
24 |     .input((val: unknown) => {
25 |       if (typeof val !== 'number') {
26 |         throw new Error('invalid input');
27 |       }
28 |       return val;
29 |     })
30 |     .query(({input: id}) => {
31 |       return prisma.user.findUnique({
32 |         where: {
33 |           id,
34 |         }
35 |       })
36 |     }),
37 |   userCreate: t.procedure
38 |     .input(z.object({
39 |       name: z.string(),
40 |       dateCreated: z.date(),
41 |     }))
42 |     .mutation(async ({input: {name, dateCreated}}) => {
43 |       console.log("Creating user on ", dateCreated.toLocaleString());
44 |       const user = await prisma.user.create({
45 |         data: {
46 |           name
47 |         }
48 |       });
49 | 
50 |       return user;
51 |     })
52 | });
53 | 
54 | export type AppRouter = typeof appRouter;
55 | 


--------------------------------------------------------------------------------
/src/client/renderer.ts:
--------------------------------------------------------------------------------
 1 | import {createTRPCProxyClient, httpBatchLink, loggerLink} from '@trpc/client';
 2 | import type {AppRouter} from "../server/router";
 3 | import {IpcRequest} from "../api";
 4 | import superjson from 'superjson';
 5 | 
 6 | const trpc = createTRPCProxyClient({
 7 |   links: [
 8 |     loggerLink(),
 9 |     httpBatchLink({
10 |       url: '/trpc',
11 | 
12 |       // custom fetch implementation that sends the request over IPC to Main process
13 |       fetch: async (input, init) => {
14 |         const req: IpcRequest = {
15 |           url: input instanceof URL ? input.toString() : typeof input === 'string' ? input : input.url,
16 |           method: input instanceof Request ? input.method : init?.method!,
17 |           headers: input instanceof Request ? input.headers : init?.headers!,
18 |           body: input instanceof Request ? input.body : init?.body!,
19 |         };
20 | 
21 |         const resp = await window.appApi.trpc(req);
22 |         // Since all tRPC really needs is the JSON, and we already have the JSON deserialized,
23 |         // construct a "fake" fetch Response object
24 |         return {
25 |           json: () => Promise.resolve(resp.body)
26 |         }
27 |       },
28 |       transformer: new superjson(),
29 |     }),
30 |   ],
31 | });
32 | 
33 | const information = document.getElementById('info');
34 | 
35 | if (information) {
36 |   information.innerText = `This app is using Chrome (v${window.appApi.chrome()}), Node.js (v${window.appApi.node()}), and Electron (v${window.appApi.electron()})`;
37 | }
38 | 
39 | const loadUsers = async () => {
40 |   const user = await trpc.users.query();
41 |   const userId1 = await trpc.userById.query(1);
42 |   const resp = document.getElementById('resp');
43 |   if (resp) {
44 |     resp.innerText = JSON.stringify(user, null, 2);
45 |   }
46 | }
47 | 
48 | loadUsers();
49 | 
50 | const addUser = async () => {
51 |   const user = await trpc.userCreate.mutate({
52 |     name: 'New User',
53 |     dateCreated: new Date()
54 |   });
55 |   const resp = document.getElementById('resp');
56 |   if (resp) {
57 |     resp.innerText = JSON.stringify(user, null, 2);
58 |   }
59 | }
60 | 
61 | window.addEventListener('DOMContentLoaded', () => {
62 |   const btn = document.getElementById('add-user');
63 |   if (btn) {
64 |     btn.addEventListener('click', addUser);
65 |   }
66 | });
67 | 


--------------------------------------------------------------------------------
/electron-builder.yml:
--------------------------------------------------------------------------------
 1 | productName: ElectronPrismaTrpcExample
 2 | appId: com.funtoimagine.electron-prisma-trpc-example
 3 | copyright: Copyright 2022 Ayron Wohletz
 4 | afterSign: notarize.js
 5 | publish:
 6 |   provider: github
 7 |   private: false
 8 |   releaseType: release
 9 |   publishAutoUpdate: true
10 | directories:
11 |   buildResources: resources
12 |   output: packed
13 | files:
14 |   - dist/**/*
15 |   - localization/!(locales)
16 |   - prisma/**/*
17 |   - "!prisma/app.db"
18 |   - resources/**/*
19 |   # @prisma is not needed in the packed app unless using prisma migrate
20 |   - "!**/node_modules/@prisma/engines/introspection-engine*"
21 |   - "!**/node_modules/@prisma/engines/schema-engine*"
22 |   - "!**/node_modules/@prisma/engines/prisma-fmt*"
23 |   - "!**/node_modules/@prisma/engines/query_engine-*"
24 |   - "!**/node_modules/@prisma/engines/libquery_engine*"
25 |   - "!**/node_modules/prisma/query_engine*"
26 |   - "!**/node_modules/prisma/libquery_engine*"
27 |   - "!**/node_modules/prisma/**/*.mjs"
28 | extraFiles:
29 |   - localization/locales/**/*
30 | extraResources: # Only if you need to run prisma migrate
31 |   - node_modules/@prisma/engines/schema-engine*
32 |   - node_modules/@prisma/engines/query*
33 |   - node_modules/@prisma/engines/libquery*
34 |   - THIRD-PARTY-LICENSES.txt
35 | win:
36 |   target:
37 |     - nsis
38 |   asar:
39 |     smartUnpack: false
40 |   asarUnpack: # only if you need to run prisma migrate:
41 |     - prisma
42 |   signingHashAlgorithms: [ 'sha256' ]
43 |   publisherName: Fun to Imagine LLC
44 |   signAndEditExecutable: true
45 |   verifyUpdateCodeSignature: true
46 |   # had to add artifactName here because electron-builder was generating latest.yml with hyphens instead of spaces,
47 |   # which screwed up auto updates
48 |   artifactName: ${productName}-Setup-${version}.${ext}
49 | linux:
50 |   target:
51 |     - snap
52 |     - AppImage
53 |   asarUnpack: # only if you need to run prisma migrate:
54 |     - prisma
55 | mac:
56 |   category: public.app-category.productivity
57 |   #  gatekeeperAssess: true
58 |   target:
59 |     - target: dmg
60 |       arch:
61 |         - x64
62 |         - arm64
63 |     - target: zip # zip is required because of electron-userland/electron-builder#2199
64 |       arch:
65 |         - x64
66 |         - arm64
67 |   entitlements: ./entitlements.mac.plist
68 |   entitlementsInherit: ./entitlements.mac.plist
69 |   hardenedRuntime: true
70 |   asarUnpack: # only if you need to run prisma migrate:
71 |     - prisma
72 | 
73 | 


--------------------------------------------------------------------------------
/src/server/constants.ts:
--------------------------------------------------------------------------------
 1 | import path from "path";
 2 | import {app} from "electron";
 3 | 
 4 | export const isDev = process.env.NODE_ENV === "development";
 5 | export const dbPath = path.join(app.getPath('userData'), "app.db");
 6 | export const dbUrl = isDev ? process.env.DATABASE_URL! : "file:" + dbPath;
 7 | 
 8 | // Hacky, but putting this here because otherwise at query time the Prisma client
 9 | // gives an error "Environment variable not found: DATABASE_URL" despite us passing
10 | // the dbUrl into the prisma client constructor in datasources.db.url
11 | process.env.DATABASE_URL = dbUrl;
12 | 
13 | // This needs to be updated every time you create a migration!
14 | export const latestMigration = "20221005221528_init";
15 | export const platformToExecutables: any = {
16 |   win32: {
17 |     migrationEngine: 'node_modules/@prisma/engines/schema-engine-windows.exe',
18 |     queryEngine: 'node_modules/@prisma/engines/query_engine-windows.dll.node',
19 |   },
20 |   linux: {
21 |     migrationEngine: 'node_modules/@prisma/engines/schema-engine-debian-openssl-1.1.x',
22 |     queryEngine: 'node_modules/@prisma/engines/libquery_engine-debian-openssl-1.1.x.so.node'
23 |   },
24 |   darwin: {
25 |     migrationEngine: 'node_modules/@prisma/engines/schema-engine-darwin',
26 |     queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin.dylib.node'
27 |   },
28 |   darwinArm64: {
29 |     migrationEngine: 'node_modules/@prisma/engines/schema-engine-darwin-arm64',
30 |     queryEngine: 'node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node',
31 |   }
32 | };
33 | const extraResourcesPath = app.getAppPath().replace('app.asar', ''); // impacted by extraResources setting in electron-builder.yml
34 | 
35 | function getPlatformName(): string {
36 |   const isDarwin = process.platform === "darwin";
37 |   if (isDarwin && process.arch === "arm64") {
38 |     return process.platform + "Arm64";
39 |   }
40 | 
41 |   return process.platform;
42 | }
43 | 
44 | const platformName = getPlatformName();
45 | 
46 | export const mePath = path.join(
47 |   extraResourcesPath,
48 |   platformToExecutables[platformName].migrationEngine
49 | );
50 | export const qePath = path.join(
51 |   extraResourcesPath,
52 |   platformToExecutables[platformName].queryEngine
53 | );
54 | 
55 | 
56 | export interface Migration {
57 |   id: string;
58 |   checksum: string;
59 |   finished_at: string;
60 |   migration_name: string;
61 |   logs: string;
62 |   rolled_back_at: string;
63 |   started_at: string;
64 |   applied_steps_count: string;
65 | }
66 | 
67 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 | 
110 | # Serverless directories
111 | .serverless/
112 | 
113 | # FuseBox cache
114 | .fusebox/
115 | 
116 | # DynamoDB Local files
117 | .dynamodb/
118 | 
119 | # TernJS port file
120 | .tern-port
121 | 
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 | 
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 | 
132 | packed
133 | 
134 | .idea
135 | src/generated
136 | prisma/app.db
137 | *.pfx
138 | 


--------------------------------------------------------------------------------
/src/server/main.ts:
--------------------------------------------------------------------------------
 1 | import {appRouter} from "./router";
 2 | import {app, BrowserWindow, ipcMain, protocol} from 'electron';
 3 | import path from "path";
 4 | import {ipcRequestHandler} from "./ipcRequestHandler";
 5 | import {IpcRequest} from "../api";
 6 | import fs from "fs";
 7 | import {dbPath, dbUrl, latestMigration, Migration} from "./constants";
 8 | import log from "electron-log";
 9 | import {prisma, runPrismaCommand} from "./prisma";
10 | 
11 | const createWindow = async () => {
12 | 
13 |   let needsMigration;
14 |   const dbExists = fs.existsSync(dbPath);
15 |   if (!dbExists) {
16 |     needsMigration = true;
17 |     // prisma for whatever reason has trouble if the database file does not exist yet.
18 |     // So just touch it here
19 |     fs.closeSync(fs.openSync(dbPath, 'w'));
20 |   } else {
21 |     try {
22 |       const latest: Migration[] = await prisma.$queryRaw`select * from _prisma_migrations order by finished_at`;
23 |       needsMigration = latest[latest.length-1]?.migration_name !== latestMigration;
24 |     } catch (e) {
25 |       log.error(e);
26 |       needsMigration = true;
27 |     }
28 |   }
29 | 
30 |   if (needsMigration) {
31 |     try {
32 |       const schemaPath = path.join(
33 |         app.getAppPath().replace('app.asar', 'app.asar.unpacked'),
34 |         'prisma',
35 |         "schema.prisma"
36 |       );
37 |       log.info(`Needs a migration. Running prisma migrate with schema path ${schemaPath}`);
38 | 
39 |       // first create or migrate the database! If you were deploying prisma to a cloud service, this migrate deploy
40 |       // command you would run as part of your CI/CD deployment. Since this is an electron app, it just needs
41 |       // to run every time the production app is started. That way if the user updates the app and the schema has
42 |       // changed, it will transparently migrate their DB.
43 |       await runPrismaCommand({
44 |         command: ["migrate", "deploy", "--schema", schemaPath],
45 |         dbUrl
46 |       });
47 |       log.info("Migration done.")
48 | 
49 |       // seed
50 |       // log.info("Seeding...");
51 |       // await seed(prisma);
52 | 
53 |     } catch (e) {
54 |       log.error(e);
55 |       process.exit(1);
56 |     }
57 |   } else {
58 |     log.info("Does not need migration");
59 |   }
60 | 
61 |   const win = new BrowserWindow({
62 |     width: 1024,
63 |     height: 1024,
64 |     webPreferences: {
65 |       preload: path.join(__dirname, 'preload.js'),
66 |     },
67 |   });
68 | 
69 |   win.loadFile('dist/index.html');
70 |   win.webContents.openDevTools()
71 | };
72 | 
73 | app.whenReady().then(() => {
74 |   ipcMain.handle('trpc', (event, req: IpcRequest) => {
75 |     return ipcRequestHandler({
76 |       endpoint: "/trpc",
77 |       req,
78 |       router: appRouter,
79 |       createContext: async () => {
80 |         return {};
81 |       }
82 |     });
83 |   })
84 | 
85 |   createWindow();
86 | 
87 |   app.on('activate', () => {
88 |     if (BrowserWindow.getAllWindows().length === 0) {
89 |       createWindow();
90 |     }
91 |   });
92 | });
93 | 
94 | app.on('window-all-closed', () => {
95 |   if (process.platform !== 'darwin') {
96 |     app.quit();
97 |   }
98 | });
99 | 


--------------------------------------------------------------------------------
/src/server/prisma.ts:
--------------------------------------------------------------------------------
 1 | import log from "electron-log";
 2 | import { PrismaClient } from "../generated/client";
 3 | import {dbUrl, mePath, qePath} from "./constants";
 4 | import path from "path";
 5 | import {fork} from "child_process";
 6 | 
 7 | log.info("DB URL", dbUrl);
 8 | log.info("QE Path", qePath);
 9 | 
10 | export const prisma = new PrismaClient({
11 |   log: ['info', 'warn', 'error',
12 |     //     {
13 |     //     emit: "event",
14 |     //     level: "query",
15 |     // },
16 |   ],
17 |   datasources: {
18 |     db: {
19 |       url: dbUrl
20 |     }
21 |   },
22 |   // see https://github.com/prisma/prisma/discussions/5200
23 |   // @ts-expect-error internal prop
24 |   __internal: {
25 |     engine: {
26 |       binaryPath: qePath
27 |     }
28 |   }
29 | });
30 | 
31 | 
32 | export async function runPrismaCommand({command, dbUrl}: {
33 |   command: string[];
34 |   dbUrl: string;
35 | }): Promise {
36 | 
37 | 
38 | 
39 |   log.info("Migration engine path", mePath);
40 |   log.info("Query engine path", qePath);
41 | 
42 |   // Currently we don't have any direct method to invoke prisma migration programatically.
43 |   // As a workaround, we spawn migration script as a child process and wait for its completion.
44 |   // Please also refer to the following GitHub issue: https://github.com/prisma/prisma/issues/4703
45 |   try {
46 |     const exitCode = await new Promise((resolve, _) => {
47 |       const prismaPath = path.resolve(__dirname, "..", "..", "node_modules/prisma/build/index.js");
48 |       log.info("Prisma path", prismaPath);
49 | 
50 |       const child = fork(
51 |         prismaPath,
52 |         command,
53 |         {
54 |           env: {
55 |             ...process.env,
56 |             DATABASE_URL: dbUrl,
57 |             PRISMA_SCHEMA_ENGINE_BINARY: mePath,
58 |             PRISMA_QUERY_ENGINE_LIBRARY: qePath,
59 | 
60 |             // Prisma apparently needs a valid path for the format and introspection binaries, even though
61 |             // we don't use them. So we just point them to the query engine binary. Otherwise, we get
62 |             // prisma:  Error: ENOTDIR: not a directory, unlink '/some/path/electron-prisma-trpc-example/packed/mac-arm64/ElectronPrismaTrpcExample.app/Contents/Resources/app.asar/node_modules/@prisma/engines/prisma-fmt-darwin-arm64'
63 |             PRISMA_FMT_BINARY: qePath,
64 |             PRISMA_INTROSPECTION_ENGINE_BINARY: qePath
65 |           },
66 |           stdio: "pipe"
67 |         }
68 |       );
69 | 
70 |       child.on("message", msg => {
71 |         log.info(msg);
72 |       })
73 | 
74 |       child.on("error", err => {
75 |         log.error("Child process got error:", err);
76 |       });
77 | 
78 |       child.on("close", (code, signal) => {
79 |         resolve(code);
80 |       })
81 | 
82 |       child.stdout?.on('data',function(data){
83 |         log.info("prisma: ", data.toString());
84 |       });
85 | 
86 |       child.stderr?.on('data',function(data){
87 |         log.error("prisma: ", data.toString());
88 |       });
89 |     });
90 | 
91 |     if (exitCode !== 0) throw Error(`command ${command} failed with exit code ${exitCode}`);
92 | 
93 |     return exitCode;
94 |   } catch (e) {
95 |     log.error(e);
96 |     throw e;
97 |   }
98 | }
99 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # Purpose
  2 | This repo demonstrates:
  3 | - Using tRPC over IPC to communicate between the main and renderer processes.
  4 | - Using Prisma with an SQLite database.
  5 | - End-to-end process of building, signing, notarizing, and publishing an Electron app with electron-builder on Mac and Windows.
  6 | 
  7 | electron-prisma-trpc-example intends to provide a clean proof-of-concept that you can pick-and-choose from and integrate into your own Electron app.
  8 | 
  9 | This is the next generation of https://github.com/awohletz/electron-prisma-template, simplified, trimmed down, and updated for the latest Electron and other dependencies.
 10 | 
 11 | ## What about React?
 12 | **See the `react` branch**.
 13 | 
 14 | The `main` branch of this repo uses the vanilla tRPC client and no frontend framework. This is to keep it minimal for those wanting to pull the techniques into their own Electron setups. However, I've also created a React version on the branch `react`. 
 15 | 
 16 | It uses React and Vite for hot-module reloading. I included a stubbed application menu and communication pipe from Main to Renderer.
 17 | 
 18 | ## Getting started
 19 | 1. Clone this repo. Then in the project root directory, do the following:
 20 | 2. Run `npm install`.
 21 | 4. Edit electron-builder.yml to fill in productName, appId, copyright, and publisherName.
 22 | 5. Set up code signing. Follow the instructions in https://www.electron.build/code-signing to set up code signing certificates for your platform. Also see my articles: 
 23 |     1. Windows: https://dev.to/awohletz/how-i-code-signed-an-electron-app-on-windows-30k5 
 24 |     1. Mac: https://dev.to/awohletz/how-i-sign-and-notarize-my-electron-app-on-macos-59bb
 25 | 5. Edit package.json to fill in your project details. Set the `repository` property to a Github repo where you will publish releases. When you run `npm run dist`, the app will be packaged and published to the Github repo.
 26 |    1. Create a Github repo for your app releases. See https://www.electron.build/configuration/publish#githuboptions
 27 |    2. Create an access token for your Github repo. See https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token
 28 | 6. Create a `.env` file that looks like this:
 29 | ```
 30 | DATABASE_URL=file:./app.db
 31 | # If you are signing and notarizing the app on Mac
 32 | APPLE_ID=your apple id
 33 | APPLE_ID_PASSWORD=your apple password
 34 | APPLE_TEAM_ID=your apple team ID
 35 | # If you want to publish releases to Github
 36 | GITHUB_TOKEN=your github access token
 37 | # If you want to code sign on Windows
 38 | CSC_LINK=yourWindowsCodeSigningCert.pfx
 39 | CSC_KEY_PASSWORD=your password for the Windows code signing cert
 40 | ```
 41 | 5. Now you can run `npm start` to start in dev mode and check out the example app. If you want to test building and publishing a release, see the below sections.
 42 | 
 43 | ## Scripts
 44 | ### `npm run build` 
 45 | Builds the project code and places it in `dist`. There are two steps to building: 
 46 |   1. Use Vite to transpile the frontend TypeScript code in `src/client` and move it to `dist`. 
 47 |   2. Generate the Prisma client in `src/generated`
 48 |   2. Use the TypeScript compiler (`tsc`) to 1. check types in `src/client` (Vite does not check types) and 2. build the backend TypeScript code in `src/server` and place the output in `dist/server`.
 49 |   3. Run copy-files.js -- Copy the generated Prisma client from `src/generated` to `dist/generated`
 50 |   4. Run install-engines-on-mac.js -- Make sure that node_modules/@prisma/engines has both darwin and darwin-arm64 binaries. These will get packed into the app if you run `npm run pack`.
 51 | 
 52 | ### `npm start` 
 53 | Build and start the app without packaging.
 54 | 
 55 | ### `npm run pack`
 56 | Build, pack, sign, and notarize the app for the current platform. The packed app will be output to `packed` directory. This is a fast way to test packing, signing, and notarizing. It does not publish the app to Github.
 57 | 
 58 | Once packed, you can test the outputted app in the `packed` directory. E.g. on Mac M1, open `packed/mac-arm64` in Finder and double-click on ElectronPrismaTrpcExample to run the app.
 59 | 
 60 | ### `npm run dist`
 61 | Build, pack, sign, and notarize the app for production. The only difference between `npm run dist` and `npm run publish` is that `npm run dist` does not publish the app to Github.
 62 | 
 63 | ### `npm run publish`
 64 | Build, pack, sign, notarize, and publish the app. Run this to publish a release to Github. The app will be published to the Github repo specified in `package.json`.
 65 | 
 66 | 
 67 | ## tRPC usage
 68 | The tRPC integration allows Renderer to communicate to Main and get responses back. From tRPC's perspective, the Renderer is a client and the Main process is a server. It does not know that it is communicating over IPC.
 69 | 
 70 | In `src/client/renderer.ts` I've provided a custom `fetch` implementation to tRPC client to send the requests over IPC. 
 71 | 
 72 | In `src/server/main.ts`, `ipcMain` listens for those IPC requests and fowards them to the tRPC server. To enable this, I built a `ipcRequestHandler` function, which is a customized version of [tRPC's fetchRequestHandler](https://trpc.io/docs/v10/fetch). Instead of sending fetch API Request and Response objects, which cannot be serialized over IPC, it sends plain JSON objects and converts them to Response objects in the Renderer code.
 73 | 
 74 | ## Prisma usage
 75 | electron-prisma-trpc-example uses Prisma to manage the SQLite database. To enable this, I had to leave the Prisma binaries out of app.asar. They do not work when packed inside app.asar. To leave them out, I specified them as excluded files in electron-builder.yml and as extraResources. Then I pass the query engine and migration engine paths from extraResources into the Prisma client constructor and the Prisma migrate command.  
 76 | 
 77 | To create a universal build on Mac M1 and Mac Intel, the build and install scripts pack both sets of Prisma binaries. 
 78 | 
 79 | ## Signing, notarizing, and publishing
 80 | The `electron-builder.yml` file has configuration to sign and notarize the app for Mac, Windows, and Linux. You'll have to customize this file to enter your own publisher and app info.
 81 | 
 82 | See https://github.com/awohletz/electron-prisma-trpc-example-releases for an example repo that holds the releases for this app. I publish releases to that repo using the `npm run publish` script.
 83 | 
 84 | Here are the steps to publish a release on Windows and Mac:
 85 | 1. Make sure you've set up code signing and have the appropriate env vars in `.env`, as mentioned above in Getting Started. 
 86 | 2. On your Windows computer, run `npm run publish`
 87 | 2. On your Mac computer, run `npm run publish`
 88 | 
 89 | These commands will build for their respective platforms and upload the release files to your Github repo.
 90 | 
 91 | ## Debugging
 92 | The key parts of the Prisma integration:
 93 | - The Prisma binaries/libraries do not work if packed inside of app.asar. Thus they have to be outside of the asar in extraResources. `npm run pack` should move them to ElectronPrismaTrpcExample/Contents/Resources/node_modules/@prisma/engines.
 94 | - The Prisma client has to be configured to look for the query engine at that extraResources location (ElectronPrismaTrpcExample/Contents/Resources/node_modules/@prisma/engines). This is done either by [environment variable](https://www.prisma.io/docs/concepts/components/prisma-engines#using-custom-engine-libraries-or-binaries) or by [passing in the path with an internal config prop on the Prisma client constructor](https://github.com/prisma/prisma/discussions/5200#discussioncomment-295575). electron-prisma-trpc-example uses both of these techniques: Env var for the Prisma migrate command (which runs in a separate process) and engine prop for the Prisma client used to do queries in the app. 
 95 | 
 96 | If you encounter a problem with Prisma related to it not finding the binaries, you can debug where it locates the binaries like so on a Mac:
 97 | 1. Open a terminal
 98 | 2. Run `export DEBUG=prisma*` (see [docs](https://www.prisma.io/docs/concepts/components/prisma-client/debugging#setting-the-debug-environment-variable))
 99 | 3. `cd` to the directory where the app package is. E.g. your Applications directory.
100 | 4. Run the app directly inside the .app package by entering on your terminal: `./ElectronPrismaTrpcExample.app/Contents/MacOS/ElectronPrismaTrpcExample`
101 | 5. Now you should see a bunch of debug output written to your terminal as the app starts. Prisma will show where it is searching for the binaries. 
102 | 
103 | 


--------------------------------------------------------------------------------