prisma: pnpm workspace + nextjs: `ENOENT: no such file or directory, open '...\.next\server\pages\api\schema.prisma'`

Bug description

I have a pnpm workspace with three projects:

  • / -root project
  • /packages/database - Prisma installed as a dependency, re-export generated prisma client
  • /packages/next - A standard next.js typescript template, importing the database project

When I run the next project I get this error:

error - Error: ENOENT: no such file or directory, open 'C:\Users\hkhen\dev\pnpm-next\packages\next\.next\server\pages\api\schema.prisma'

I’ve tried a few variations: prisma and @prisma/client 3.12.0 and 3.13.0-dev.15, standard output directory and output = "../src/generated/prisma", all give the same error.

It sounds like this is kind of the same error as #10361, but it seems like next.js does something special handling of dirname. Regular node-js server apps works fine with the same setup.

How to reproduce

Terminal 1:

git clone git@github.com:ineentho/pnpm-nextjs-repro.git
cd pnpm-nextjs-repro
pnpm i # prisma generate runs as a postinstall hook
pnpm start # run next in dev-mode

Termnal 2:

curl localhost:3000/api/hello

The first terminal will log the error:

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
wait  - compiling...
event - compiled client and server successfully in 643 ms (124 modules)
wait  - compiling /api/hello...
wait  - compiling...
warn  - ../database/src/generated/prisma/runtime/index.js
Module not found: Can't resolve 'encoding' in 'C:\Users\hkhen\dev\pnpm-nextjs-repro\packages\database\src\generated\prisma\runtime'
2022-04-16T15:35:50.611Z prisma:tryLoadEnv Environment variables not found at null
2022-04-16T15:35:50.612Z prisma:tryLoadEnv Environment variables not found at undefined
2022-04-16T15:35:50.612Z prisma:tryLoadEnv No Environment variables loaded
2022-04-16T15:35:50.631Z prisma:tryLoadEnv Environment variables not found at null
2022-04-16T15:35:50.631Z prisma:tryLoadEnv Environment variables not found at undefined
2022-04-16T15:35:50.631Z prisma:tryLoadEnv No Environment variables loaded
2022-04-16T15:35:50.631Z prisma:client dirname C:\Users\hkhen\dev\pnpm-nextjs-repro\packages\next\.next\server\pages\api
2022-04-16T15:35:50.631Z prisma:client relativePath ..\..\..\prisma
2022-04-16T15:35:50.632Z prisma:client cwd C:\Users\hkhen\dev\pnpm-nextjs-repro\packages\next\.next\server\pages\api
2022-04-16T15:35:50.634Z prisma:client clientVersion: 3.12.0
2022-04-16T15:35:50.634Z prisma:client clientEngineType: library
error - Error: ENOENT: no such file or directory, open 'C:\Users\hkhen\dev\pnpm-nextjs-repro\packages\next\.next\server\pages\api\schema.prisma'
warn  - ../database/src/generated/prisma/runtime/index.js
Module not found: Can't resolve 'encoding' in 'C:\Users\hkhen\dev\pnpm-nextjs-repro\packages\database\src\generated\prisma\runtime'
wait  - compiling /_error (client and server)...
wait  - compiling...
warn  - ../database/src/generated/prisma/runtime/index.js
Module not found: Can't resolve 'encoding' in 'C:\Users\hkhen\dev\pnpm-nextjs-repro\packages\database\src\generated\prisma\runtime'
2022-04-16T15:35:50.868Z compression no compression: not acceptable

Environment & setup

  • OS: Windows 10
  • Database: Postgres
  • Node.js version: v17.3.0

Prisma Version

prisma                  : 3.12.0
@prisma/client          : 3.12.0
Current platform        : windows
Query Engine (Node-API) : libquery-engine 22b822189f46ef0dc5c5b503368d1bee01213980 (at ..\..\node_modules\.pnpm\@prisma+engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980\node_modules\@prisma\engines\query_engine-windows.dll.node)
Migration Engine        : migration-engine-cli 22b822189f46ef0dc5c5b503368d1bee01213980 (at ..\..\node_modules\.pnpm\@prisma+engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980\node_modules\@prisma\engines\migration-engine-windows.exe)
Introspection Engine    : introspection-core 22b822189f46ef0dc5c5b503368d1bee01213980 (at ..\..\node_modules\.pnpm\@prisma+engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980\node_modules\@prisma\engines\introspection-engine-windows.exe)
Format Binary           : prisma-fmt 22b822189f46ef0dc5c5b503368d1bee01213980 (at ..\..\node_modules\.pnpm\@prisma+engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980\node_modules\@prisma\engines\prisma-fmt-windows.exe)
Default Engines Hash    : 22b822189f46ef0dc5c5b503368d1bee01213980
Studio                  : 0.459.0

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 4
  • Comments: 33 (14 by maintainers)

Most upvoted comments

That does seem to work on mine right out of the box at least!

Hey guys, thanks for the feedback.

We have been working on a simpler way to make things work for you, as the workaround we proposed had clear limitations for TS projects. We worked on an experimental webpack plugin that ensures your Prisma files are copied correctly. I’d love if you could give it a try (https://www.npmjs.com/package/experimental-prisma-webpack-plugin). We have tested it in many different scenarios, so we hope this works for you.

const { PrismaPlugin } = require('experimental-prisma-webpack-plugin')

module.exports = {
  output: 'standalone',
  webpack: (config, { isServer }) => {
    if (isServer) {
      config.plugins = [...config.plugins, new PrismaPlugin()]
    }

    return config
  },
}

Possible workaround Context, I’m using PNPM workspaces and my prisma config is in a packages that is used by a Nest.js app. The Nest.js app bundle the generated Prisma build with. webpack.

In post build of prisma generate, I run the following script and it’s doing the trick (for now). If regularDirname variable name is changed, the script will need an update

const fs = require("node:fs");

const generateLibPath = __dirname + "/../build/index.js";

const file = fs.readFileSync(generateLibPath);
const lines = file.toString().split("\n");

const newLines = lines.map((line) =>
  line.startsWith("const regularDirname")
    ? "const regularDirname = path.dirname(require.resolve('.'))"
    : line
);

const joinedNewLine = newLines.join("\n");

fs.writeFileSync(generateLibPath, joinedNewLine);

Thanks for the report, I will take a look.

@millsp I’m using pnpm link rather than workspace and this along with PRISMA_CLIENT_ENGINE_TYPE=binary solved my issue 🙏

For anyone wondering, I “fixed” my issue by adding the clients (plural) package to webpack externals, manually compiling index.ts using tsc and setting index.js as main in package.json while using the Typescript file for types.

Obviously, this is not ideal.

@millsp thank you for the attention on this issue! I tried this plugin out but unfortunately I get a bunch of schema validation errors.

For context, I have to run two clients out of one package that is installed by the nextjs project and it seems that’s causing the issue.

I tried having the clients as two separate packages but that caused an issue where the first client instantiating would seemingly prevent the other one from doing so. E.g. if I go to a page that uses client 1 in getServerSideProps and then go to another page that uses client 2, client 2 would just hang. Same happens if I go to the client 2 page first, except its client 1 that would hang.

Custom output directories are used in all aforementioned scenarios btw.

@janpio here is a repro project.

I finally got something working! It relies on special-casing magic Vercel paths, but it works!

First, you must add public-hoist-pattern[]=*prisma* to your .npmrc. The default is also to include *eslint* and *prettier* (though I don’t use prettier so I left that one out). Overall, my .npmrc looks like this:

auto-install-peers=true
link-workspace-packages=deep
save-workspace-protocol=rolling

public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prisma*

Here’s the updated patch:

diff --git a/generator-build/index.js b/generator-build/index.js
index 888e53268d94ba836cad78fd2d24e59066449657..33b9f49bf94f67caf5425851d84a272c70828847 100644
--- a/generator-build/index.js
+++ b/generator-build/index.js
@@ -23390,13 +23390,18 @@ const hasDirname = typeof __dirname !== 'undefined' && __dirname !== '/'
 // will work in most cases, ie. if the client has not been bundled
 const regularDirname = hasDirname && fs.existsSync(path.join(__dirname, 'schema.prisma')) && __dirname
 
+const dirnameCandidates = [
+  ${defaultRelativeOutdir ? `${JSON.stringify(defaultRelativeOutdir)},` : ""}
+  ${serverlessRelativeOutdir ? `${JSON.stringify(serverlessRelativeOutdir)},` : ""}
+];
+
 // if the client has been bundled, we need to look for the folders
-const foundDirname = !regularDirname && findSync(process.cwd(), [
-    ${defaultRelativeOutdir ? `${JSON.stringify(defaultRelativeOutdir)},` : ""}
-    ${serverlessRelativeOutdir ? `${JSON.stringify(serverlessRelativeOutdir)},` : ""}
-], ['d'], ['d'], 1)[0]
+const foundDirname = !regularDirname && findSync(process.cwd(), dirnameCandidates, ['d'], ['d'], 1)[0]
+
+// in monorepos, they could be at a higher level
+const monorepoFoundDirname = !regularDirname && !foundDirname && dirnameCandidates.map(x => path.join(process.cwd(), x)).find(x => fs.existsSync(path.join(x, 'schema.prisma')))
 
-const dirname = regularDirname || foundDirname || __dirname`;
+const dirname = regularDirname || foundDirname || monorepoFoundDirname || __dirname`;
 }
 __name(buildDirnameFind, "buildDirnameFind");
 function buildDirnameDefault() {
diff --git a/runtime/index.js b/runtime/index.js
index db697a88c418f974ffa9fb3100809b0cd9c0aea6..6dd3ba59c9e1c8797ad56e29063a58644ae08471 100644
--- a/runtime/index.js
+++ b/runtime/index.js
@@ -24549,6 +24549,16 @@ var exitHooks = new ExitHooks();
 var LibraryEngine = class extends Engine {
   constructor(config2, loader = new DefaultLibraryLoader(config2)) {
     super();
+    if (!import_fs7.default.existsSync(config2.datamodelPath)) {
+      debug11(`Couldn't find datamodelPath '${config2.datamodelPath}', may be on Vercel.`);
+      if (import_fs7.default.existsSync('/var/task/node_modules/.pnpm')) {
+        const dotPnpmPackageName = import_fs7.readdirSync('/var/task/node_modules/.pnpm').find(x => x.startsWith('@prisma+client'));
+        config2.datamodelPath = `/var/task/node_modules/.pnpm/${dotPnpmPackageName}/node_modules/.prisma/client/schema.prisma`
+      }
+      else {
+        debug11(`Couldn't find '/var/task/node_modules/.pnpm', something is broken.`);
+      }
+    }
     this.datamodel = import_fs7.default.readFileSync(config2.datamodelPath, "utf-8");
     this.config = config2;
     this.libraryStarted = false;

Basically, in addition to what the patch did previously (which works well for local development), if it can’t find the file it will now check a known Vercel deployment path to see if it’s there. This is obviously not an ideal solution, but if this issue is a complete blocker for you, it should at least help for the time being.

Some notes:

  • This does not work with outputFileTracingRoot: path.join(__dirname, '../../'). For some reason Vercel isn’t able to trace files correctly with it set (which is sort of counter-intuitive).
  • PNPM patches are a bit buggy (or at least their interaction with caching is), so you’ll probably need to redeploy without the build cache/delete some/all of your node_modules to re-apply. The lockfile can also get confused, but I find that if I pnpm i @prisma/client in the database package again it fixes it.
  • Sometimes, especially when changing hoist-pattern and reinstalling, attempting to npx prisma generate would throw a mess of errors. I don’t know whose fault that is but if you delete all of your node_modules (npx rimraf **/node_modules) and your PNPM store, and then re-download/install everything, it fixes itself.
  • In the patch, re-assigning config2.datamodelPath is important. I don’t know why, and the log outputs are kind of baffling when you don’t re-assign it. The existence of the directory in node_modules/.pnpm seems to be dependent on it…?
  • While I sincerely hope that all of this will work for others, keep in mind that this is wonky territory and it may be highly dependent on your particular monorepo configuration.

For those looking for a quick solution now, I came up with a PNPM patch which resolved the issue for me (no idea how portable it is, I haven’t tried it on the repro yet)

Create patches/@prisma__client@4.9.0.patch at the root with:

diff --git a/generator-build/index.js b/generator-build/index.js
index 888e53268d94ba836cad78fd2d24e59066449657..33b9f49bf94f67caf5425851d84a272c70828847 100644
--- a/generator-build/index.js
+++ b/generator-build/index.js
@@ -23390,13 +23390,18 @@ const hasDirname = typeof __dirname !== 'undefined' && __dirname !== '/'
 // will work in most cases, ie. if the client has not been bundled
 const regularDirname = hasDirname && fs.existsSync(path.join(__dirname, 'schema.prisma')) && __dirname
 
+const dirnameCandidates = [
+  ${defaultRelativeOutdir ? `${JSON.stringify(defaultRelativeOutdir)},` : ""}
+  ${serverlessRelativeOutdir ? `${JSON.stringify(serverlessRelativeOutdir)},` : ""}
+];
+
 // if the client has been bundled, we need to look for the folders
-const foundDirname = !regularDirname && findSync(process.cwd(), [
-    ${defaultRelativeOutdir ? `${JSON.stringify(defaultRelativeOutdir)},` : ""}
-    ${serverlessRelativeOutdir ? `${JSON.stringify(serverlessRelativeOutdir)},` : ""}
-], ['d'], ['d'], 1)[0]
+const foundDirname = !regularDirname && findSync(process.cwd(), dirnameCandidates, ['d'], ['d'], 1)[0]
+
+// in monorepos, they could be at a higher level
+const monorepoFoundDirname = !regularDirname && !foundDirname && dirnameCandidates.map(x => path.join(process.cwd(), x)).find(x => fs.existsSync(path.join(x, 'schema.prisma')))
 
-const dirname = regularDirname || foundDirname || __dirname`;
+const dirname = regularDirname || foundDirname || monorepoFoundDirname || __dirname`;
 }
 __name(buildDirnameFind, "buildDirnameFind");
 function buildDirnameDefault() {

Then in your top-level package.json you’ll want to add

  "pnpm": {
    "patchedDependencies": {
      "@prisma/client@4.9.0": "patches/@prisma__client@4.9.0.patch"
    }
  }

And then npx prisma generate again. I also have hoist-pattern[]=*prisma* in my .npmrc which may be important, though I’m honestly not sure.

What I think is going on is that prisma is actually finding the right path, but since .prisma/client is in node_modules/.pnpm (which is in the root), the relative path (relative to the app folder) will have some backtracking which the findSync function doesn’t seem to support. So the whole thing falls back to using __dirname to find the path, which Next.js lies about (that’s why it ends up searching in .next/server/pages). However, I don’t fully understand what prisma is doing and so it’s possible that this is only working for me by some strange coincidence.