prisma / prisma

Next-generation ORM for Node.js & TypeScript | PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, MongoDB and CockroachDB
https://www.prisma.io
Apache License 2.0
39.42k stars 1.54k forks source link

Improve loading time for Electron apps #10289

Open millsp opened 2 years ago

millsp commented 2 years ago

I'm using Prisma inside of an Electron app. This is perhaps a not supported/intended use of Prisma. But FYI, when run by Electron, process.cwd() returns "/". So the findSync call scans from the root directory which takes a long time (~60s).

I'm working around this by running a build script that replaces process.cwd() with require('electron').app.getAppPath() in the generated client code:

const dirname = findSync(require('electron').app.getAppPath(), [
    "src/generated/client",
    "generated/client",
], ['d'], ['d'], 1)[0] || __dirname

This cuts down the time it takes to instantiate the Prisma client to ~10ms.

Originally posted by @awohletz in https://github.com/prisma/prisma/issues/8484#issuecomment-969403133

florianbepunkt commented 2 years ago

Since process.cwd() returns the system root in Electron (which is correct behavior), this will search all users's files and therefore trigger multiple access permissions alerts during first start. "Do you want this app to access your photos? Your contacts? etc.". This leads to user confusion ("Why does this app need my photos?"). It would be great if a path could be passed to prisma client directly.

janpio commented 2 years ago

Is there a way to reliably detect Electron so we could differentiate if we use process.cwd() vs. require('electron').app.getAppPath()? Or is require('electron').app.getAppPath() not even always the right answer in Electron context? (We always try to avoid adding configuration options when possible.)

florianbepunkt commented 2 years ago

Apologies, this is a rather long answer, but using require('electron').app.getAppPath() will not work as expected.

Problem

require('electron').app.getAppPath() is not a viable solution. It would make it impossible to securely expose Prisma to the renderer process during preload, since require('electron').app.getAppPath() is not accessible in a preload script (which you would use to expose select node.js apis and code like prisma to the browser).

We expose prisma via ContextBridge from the main process to the renderer process. This in line with what other uses do and allows for a very convenient, yet secure, use of prisma in Electron (e. g. https://github.com/prisma/prisma/discussions/7889?sort=top#discussioncomment-1618858)

So we can simply use window.prisma.myCollection.create() in the browser window.

What we currently do

Since you cannot use app.getAppPath in a preload script, we expose the app path and some other prisma related stuff like query engine paths via IPC, which we can then read from the preload script.

// main.ts process, where electron.app.getAppPath() is accessible
ipcMain.on('config:get-app-path', (event) => {
  event.returnValue = app.getAppPath();
});
// shortened for brevity
// we also get the query engine path and the db location here, exposed in a similar fashion
// preload script where we can access IPC
import { ipcRenderer } from 'electron';
import { PrismaClient } from '@prisma/client';

const dbPath = ipcRenderer.sendSync('config:get-prisma-db-path');
const qePath = ipcRenderer.sendSync('config:get-prisma-qe-path');

export const prisma = new PrismaClient({
  datasources: {
    db: {
      url: `file:${dbPath}`,
    },
  },
  __internal: {
    engine: {
      // @ts-expect-error
      binaryPath: qePath,
    },
  },
});

Then we use a custom postinstall script that replaces the process.cwd() call with an IPC call that will return the app path.

const replace = require('replace-in-file');
const path = require('path');

const options = {
  files: path.join(__dirname, '../node_modules/', '.prisma', 'client', 'index.js'),
  from: 'findSync(process.cwd()',
  to: `findSync(require("electron").ipcRenderer.sendSync('config:get-app-path')`,
};

const results = replace.sync(options);

Since IPC handlers are done in user land and other people might structure their projects differently, I'm afraid there is no "one-fits-all" solution here. Therefore my suggestion to expose a path config variable. BTW: This very same issue should be applicable to all use cases where you use prisma for a custom CLI (like internal tooling), where process.cwd() would return the path from where the CLI command is called instead of the origin, where the CLI command's code exist.

Suggestion: Expose via param

// preload script
const dbPath = ipcRenderer.sendSync('config:get-prisma-db-path');
const qePath = ipcRenderer.sendSync('config:get-prisma-qe-path');
const appPath = ipcRenderer.sendSync('config:get-app-path');

export const prisma = new PrismaClient({
  datasources: {
    db: {
      url: `file:${dbPath}`,
    },
  },
  __internal: {
    cwd: appPath, // this does not exist
    engine: {
      // @ts-expect-error
      binaryPath: qePath,
    },
  },
});

Detecting electron

To answer your original question, you can detect electron like this:

// https://github.com/cheton/is-electron/blob/master/index.js
// https://github.com/electron/electron/issues/2288
function isElectron() {
    // Renderer process
    if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') {
        return true;
    }

    // Main process
    if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) {
        return true;
    }

    // Detect the user agent when the `nodeIntegration` option is set to false
    if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) {
        return true;
    }

    return false;
}

module.exports = isElectron;
florianbepunkt commented 2 years ago

There is another, but related, issue:

The generated prisma client defines the search paths in findSync in a OS-specific manner.

Example after using the build script by @awohletz

const dirname = findSync(require('electron').app.getAppPath(), [
    "src/main/prisma-app-client",
    "main/prisma-app-client",
], ['d'], ['d'], 1)[0] || __dirname

This causes an error on windows where the search path would be

const dirname = findSync(require('electron').app.getAppPath(), [
    "src\\main\\prisma-app-client",
    "main\\prisma-app-client",
], ['d'], ['d'], 1)[0] || __dirname

It would be helpful if the matches would be written in a cross-platform conform manner.