Skip to main content

Investigating `createRequire is not a function`

Why webpack broke Prisma's WASM query compiler in a plain Next.js ESM app, and the two-line escape hatch that fixed it.

· 8 min read

TL;DR

In a Next.js 15 ESM app using Prisma's new prisma-client generator with driverAdapters and queryCompiler, webpack rewrote a perfectly valid Node.js path-resolution flow badly enough that createRequire stopped being the real Node function. The reliable workaround was to bypass webpack with __non_webpack_require__, and Prisma later shipped a cleaner upstream fix in 6.11.0.

On this page

The task looked boring. Spin up a demo app, generate a Prisma client, run next dev, move on.

Instead, the dev server died with this:

⨯ TypeError: createRequire is not a function

At the time I was testing a very normal stack: Next.js 15, default webpack, ESM via "type": "module", the new Prisma prisma-client generator, Prisma Postgres, and Driver Adapters. No Edge runtime. No middleware. No custom webpack tricks. Just a server-rendered Node.js app that should have worked.

This post is the cleaned-up version of that debugging session. If you only care about the outcome, here it is: the generated Prisma code was fine in plain Node.js. The real bug was webpack intercepting a Node-only path-resolution flow and returning something that looked enough like createRequire to be confusing, but not enough to work.

If you are reading this because you hit the same crash today, check your Prisma version first. This exact fix later shipped upstream in Prisma 6.11.0. The rest of the post explains why the crash happened and why the workaround was so specific.

The setup

The new prisma-client generator writes TypeScript directly into your codebase instead of emitting a client into node_modules. With queryCompiler enabled, that generated code also loads a WebAssembly query compiler from @prisma/client/runtime at runtime. Driver Adapters are the other half of the setup: Prisma talks to the database through a JavaScript driver such as @prisma/adapter-pg, while the query compiler turns Prisma queries into SQL.

My schema was minimal:

prisma/schema.prisma
generator client {
  provider        = "prisma-client"
  output          = "../lib/.generated/prisma"
  previewFeatures = ["driverAdapters", "queryCompiler"]
}

The generated client crashed here:

lib/.generated/prisma/internal/class.ts
getQueryCompilerWasmModule: async () => {
  const { readFile } = await import('node:fs/promises')
  const { createRequire } = await import('node:module')
  const require = createRequire(import.meta.url)
  // ^^^^^^^^^^ ⨯ TypeError: createRequire is not a function

  const wasmModulePath = require.resolve(
    "@prisma/client/runtime/query_compiler_bg.postgresql.wasm"
  )
  const wasmModuleBytes = await readFile(wasmModulePath)

  return new globalThis.WebAssembly.Module(wasmModuleBytes)
}

That code is completely reasonable in Node.js:

  • import node:module
  • call createRequire(import.meta.url) because the file is ESM
  • use require.resolve() to find the .wasm file on disk
  • read the bytes and construct WebAssembly.Module

Two issues had already reported the same failure: prisma#27049 and prisma#27343. At that point there was no workaround, so I started tracing the generated code path myself.

The real problem

The key mistake was assuming await import('node:module') would keep plain Node.js semantics inside webpacked server code.

It does not.

Webpack does not treat that import as “let Node resolve this later at runtime.” It treats it as “an import inside code I am bundling,” then tries to analyze and transform it ahead of time. That transformation is what broke the generated code. By the time execution reached createRequire(import.meta.url), createRequire was no longer the real Node function.

This is also why the obvious ESM alternative did not help. import.meta.resolve would have expressed the same intent more directly, but webpack did not support it either. The generated code was fine. The bundler boundary was not.

Webpack has a related open bug for this exact territory: incorrect handling of createRequire and require.

Once I understood that, the rest of the session became a narrower question: how do I preserve Node semantics long enough to load the .wasm file?

Dead end #1: import the WASM directly

My first idea was to remove createRequire entirely and import the .wasm file as a module:

getQueryCompilerWasmModule: async () => {
  return await import(
    "@prisma/client/runtime/query_compiler_bg.postgresql.wasm"
  )
}

Webpack rejected that immediately:

Module parse failed: Unexpected character '' (1:0)
The module seem to be a WebAssembly module, but module is not flagged
as WebAssembly module for webpack.
BREAKING CHANGE: Since webpack 5 WebAssembly is not enabled by default
and flagged as experimental feature.
You need to enable one of the WebAssembly experiments via
'experiments.asyncWebAssembly: true' (based on async modules) or
'experiments.syncWebAssembly: true' (like webpack 4, deprecated).

Fair enough. In webpack 5, WebAssembly support is opt-in.

Dead end #2: enable asyncWebAssembly

So I enabled it:

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config, { isServer: _ }) => {
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
    }
    return config
  },
}

export default nextConfig

That changed the failure mode, but it did not fix the bug:

Module not found: Can't resolve './query_compiler_bg.js'

At first this error looked promising. It suggested webpack was finally reading the .wasm file. The problem was that the file itself contains an import for ./query_compiler_bg.js, while Prisma publishes provider-specific siblings such as query_compiler_bg.postgresql.js.

I confirmed the embedded import with strings:

terminal
strings node_modules/@prisma/client/runtime/query_compiler_bg.postgresql.wasm \
  | grep "query_compiler_bg.js" \
  | head -1

# --> ./query_compiler_bg.js

And the runtime directory looked like this:

terminal
ls node_modules/@prisma/client/runtime/query_compiler_bg*
query_compiler_bg.mysql.js
query_compiler_bg.mysql.mjs
query_compiler_bg.mysql.wasm
query_compiler_bg.postgresql.js
query_compiler_bg.postgresql.mjs
query_compiler_bg.postgresql.wasm
query_compiler_bg.sqlite.js
query_compiler_bg.sqlite.mjs
query_compiler_bg.sqlite.wasm
query_compiler_bg.sqlserver.js
query_compiler_bg.sqlserver.mjs
query_compiler_bg.sqlserver.wasm

The mismatch comes from two separate steps. The wasm-bindgen build script emits imports for query_compiler_bg.js. Later, Prisma’s client generation logic renames the published artifacts by database provider. The binary still expects the generic filename. The package ships a provider-specific one.

So yes, webpack was now processing the .wasm. It was just exposing a second mismatch.

Dead end #3: patch the binary

At that point I was deep enough in the rabbit hole that patching the binary felt reasonable.

I wrote a small script to rewrite the import reference inside the .wasm file:

patch-wasm.sh
#!/bin/bash
# Requirements: cargo binstall wasm-tools

FILE_IN="node_modules/@prisma/client/runtime/query_compiler_bg.postgresql.wasm"
FILE_OUT="node_modules/@prisma/client/runtime/query_compiler_bg.postgresql.patched.wasm"

ORIG="query_compiler_bg.js"
REPL="query_compiler_bg.postgresql.js"

wasm-tools print "${FILE_IN}" \
  | sed "s/${ORIG}/${REPL}/g" \
  | wasm-tools parse -o "${FILE_OUT}"

echo "✅ All occurrences replaced."

wasm-tools validate "${FILE_OUT}"

Then I pointed the generated code at the patched artifact:

getQueryCompilerWasmModule: async () => {
  const queryCompilerWasm = await import(
    "@prisma/client/runtime/query_compiler_bg.postgresql.patched.wasm"
  )
  return queryCompilerWasm
}

New error:

⨯ [TypeError: WebAssembly.Instance(): Argument 0 must be a WebAssembly.Module]

This was the moment the entire asyncWebAssembly branch fell apart.

Webpack’s asyncWebAssembly support does not hand back a raw WebAssembly.Module. It compiles and instantiates the module for you, then returns an object containing the exports:

{
  memory: Memory [WebAssembly.Memory] {},
  __wbg_querycompiler_free: [Function: 1553],
  querycompiler_new: [Function: 3660],
  querycompiler_compile: [Function: 3578],
  querycompiler_compileBatch: [Function: 3579],
  __wbindgen_malloc: [Function: 3320],
  __wbindgen_realloc: [Function: 3428],
  __wbindgen_exn_store: [Function: 4342],
  __externref_table_alloc: [Function: 899],
  __wbindgen_export_4: Table [WebAssembly.Table] {},
  __externref_table_dealloc: [Function: 2543],
  __wbindgen_start: [Function: 60]
}

That object is useful if your application wants “import the Wasm, get the exports, call the functions.” Prisma’s runtime needed something different. Its WasmQueryCompilerLoader expects the raw WebAssembly.Module so it can instantiate it itself with its own imports and lifecycle.

In other words, even a perfectly patched binary would still have been the wrong type. I had already seen the same shape of failure in prisma#23536, where Cloudflare Pages + Remix ran into a nearly identical WebAssembly.Instance mismatch. Different bundler, same underlying problem: the runtime wanted a module, the bundler handed back an instance-like export object.

The fix: tell webpack to stay out of the way

Once the failed experiments were stripped away, the answer was almost embarrassingly small.

Nothing was wrong with Prisma’s original control flow. The only thing I needed was a way to keep webpack from rewriting it.

Webpack has an escape hatch for exactly that: __non_webpack_require__.

lib/.generated/prisma/internal/class.ts
getQueryCompilerWasmModule: async () => {
  const { readFile } = __non_webpack_require__('node:fs/promises')
  const { createRequire } = __non_webpack_require__('node:module')
  const _require = createRequire(import.meta.url)

  const wasmModulePath = _require.resolve(
    "@prisma/client/runtime/query_compiler_bg.postgresql.wasm"
  )
  const wasmModuleBytes = await readFile(wasmModulePath)

  return new globalThis.WebAssembly.Module(wasmModuleBytes)
}

That was it. Two lines changed, everything worked.

Why this works:

  • Next.js server code still runs in Node.js, so require exists at runtime
  • __non_webpack_require__ tells webpack not to parse or transform that require
  • the rest of the flow stays exactly the same: resolve the file path on disk, read bytes, construct WebAssembly.Module manually

This was the first solution that preserved the semantics Prisma actually needed instead of trying to convince webpack to approximate them.

What shipped upstream

Shortly after, webpack merged support for using /* webpackIgnore: true */ with require.resolve():

(
  typeof __non_webpack_require__ === 'function'
    ? __non_webpack_require__
    : require
).resolve(
  /* webpackIgnore: true */
  '@prisma/client/runtime/query_compiler_bg.postgresql.wasm'
)

Prisma adopted that cleaner approach, and the fix shipped in Prisma 6.11.0.

What I would remember next time

  • If a Node builtin behaves strangely inside bundled server code, suspect transformed semantics before you suspect the builtin itself.
  • Webpack’s WebAssembly support is optimized for “bundle and instantiate this module,” not for libraries that need raw bytes or a raw WebAssembly.Module.
  • When generated code is correct in plain Node.js, the right fix may be to opt out of the bundler, not to keep teaching the bundler more context.

This bug looked like a Prisma problem, then a Wasm packaging problem, then a broken binary. In the end it was mostly a bundler boundary problem. That is the part I would want to remember.

References

  • prisma#27049: TypeError: createRequire is not a function with the query compiler
  • prisma#27343: generated prisma-client code can’t be bundled by Next.js
  • prisma#23536: same WebAssembly.Instance type mismatch on Cloudflare Pages + Remix
  • webpack#19607: incorrect handling of createRequire and require
  • webpack#16693: import.meta.resolve support
  • webpack#19201: /* webpackIgnore: true */ for require.resolve