pglite icon indicating copy to clipboard operation
pglite copied to clipboard

Document bundling with esbuild

Open skyqrose opened this issue 1 year ago • 8 comments

the problem

I'm setting up PGLite on a project that uses esbuild to bundle. I tried to start it like this:

import { PGlite } from "@electric-sql/pglite";
document.addEventListener("DOMContentLoaded", () => {
  PGlite.create();
});

But I get this error about new URL("./postgres.wasm") at this line in startWasmDownload:

Uncaught (in promise) TypeError: URL constructor: ./postgres.wasm is not a valid URL.
    Er utils.ts:17
    K_ pglite.ts:198
    I_2 pglite.ts:123
    create pglite.ts:165

And two of these errors about new URL("./postgres.data") at this line in getFsBundle:

Uncaught (in promise) TypeError: URL constructor: ./postgres.data is not a valid URL.
    Pr utils.ts:70
    K_ pglite.ts:209
    I_2 pglite.ts:123
    create pglite.ts:165

My esbuild process is dead simple, just esbuild src/index.tsx --bundle --outfile=build/index.js.

I'm using pglite version 0.2.15

what to do

I think this is either a bug or missing feature in esbuild, not this project.

But I'm posting this issue here because:

  • PGLite could change to an approach that esbuild supports.
  • PGLite could add documentation recommending an approach for people using esbuild.
    • Those docs said "If you come across any issues with PGlite and a specific bundler, please open an issue"
  • So that other people who run into the same problem can find this issue and learn from my struggles.

workaround

My workaround was to pass in the files manually

  const [wasmModule, fsBundle] = await Promise.all([
    WebAssembly.compileStreaming(fetch("/postgres.wasm")),
    fetch("/postgres.data").then((response) => response.blob()),
  ]);
  const db = await PGlite.create({
    wasmModule,
    fsBundle,
  });

I also had to copy those two files from node_modules/@electric-sql/pglite/dist/ to my build directory so my webserver would serve them.

Alternatively, there are also workarounds and plugins based around helping esbuild handle this case, but I haven't tried them.

skyqrose avatar Dec 21 '24 01:12 skyqrose

Hi @skyqrose, thank you for the issue report!

PGLite could change to an approach that esbuild supports.

Do you know of an approach that esbuild supports but doesn't break other bundlers?

tdrz avatar Dec 21 '24 11:12 tdrz

unfortunately no, I don't know much about how bundlers work, so can't be helpful in finding a solution there.

skyqrose avatar Dec 21 '24 15:12 skyqrose

Thank you @skyqrose for sharing and workaround. You've saved my day.

For anyone needing a workaround without a self-hosted web service, public CDNs like jsDeliver or unpkg can be used.

For example:

const cdn = 'https://cdn.jsdelivr.net/npm/@electric-sql/[email protected]';

const [wasmModule, fsBundle] = await Promise.all([
  WebAssembly.compileStreaming(fetch(`${cdn}/dist/postgres.wasm`)),
  fetch(`${cdn}/dist/postgres.data`).then((response) => response.blob()),
]);

const db = await PGlite.create({
  wasmModule,
  fsBundle,
});

jihchi avatar Dec 22 '24 08:12 jihchi

Unfortunately it seems that esbuild doesn't follow the pattern that other bundlers have settled on interpreting new URL('./relative/path/to/file', import.meta.url) as a way to find files to bundle, then rewrite that for the new bundle.

I looks like there are a few options linked above. We should add docs to the bundlers page showing how to enable this new URL('./relative/path/to/file', import.meta.url) pattern or work around it with the wasmModule option.

samwillis avatar Jan 15 '25 15:01 samwillis

I believe these are related https://github.com/electric-sql/pglite/issues/414

We can use docs for all bundler that don't support equivalent of

new URL('./relative/path/to/file', import.meta.url)

divyenduz avatar Feb 22 '25 08:02 divyenduz

I've successfully implemented a workaround for using PGlite with bun build --compile based on https://github.com/electric-sql/pglite/issues/478#issuecomment-2558377422.

Instead of using a CDN, I embedded the WASM files directly using Bun's with { type: "file" } import syntax:

1. Copy WASM files from node_modules

During the build process, copy pglite.wasm and pglite.data from node_modules/@electric-sql/pglite/dist/ to your source directory (e.g., src/pglite-assets/). Automated this in my build script.

2. Create a wrapper module (src/pglite-wrapper.ts)

import { PGlite } from "@electric-sql/pglite";
import type { PGliteOptions } from "@electric-sql/pglite";

// Import using Bun's file loader - these get embedded in the compiled binary
import wasmPath from "./pglite-assets/pglite.wasm" with { type: "file" };
import dataPath from "./pglite-assets/pglite.data" with { type: "file" };

export async function createPGlite(
  dataDir: string,
  options?: PGliteOptions
): Promise<PGlite> {
  // Read the embedded files
  const [wasmBuffer, dataBuffer] = await Promise.all([
    Bun.file(wasmPath).arrayBuffer(),
    Bun.file(dataPath).arrayBuffer(),
  ]);

  // Compile the WASM module
  const wasmModule = await WebAssembly.compile(wasmBuffer);

  // Create a Blob for the fs bundle
  const fsBundle = new Blob([dataBuffer]);

  // Create PGlite instance with pre-loaded modules
  const db = await PGlite.create(dataDir, {
    ...options,
    wasmModule,
    fsBundle,
  });

  return db;
}

3. Use the wrapper instead of new PGlite()

// Before:
const db = new PGlite(dataDir, options);

// After:
const db = await createPGlite(dataDir, options);

Summary

  • Bun's with { type: "file" } syntax tells the bundler to embed these files in the compiled binary
  • We bypass the new URL("./pglite.data", import.meta.url) resolution issue entirely
  • The WASM and data files are pre-loaded into memory and passed directly to PGlite.create()

This is a bug in bun. The proper fix should be in Bun's bundler to automatically detect and embed files referenced via new URL(literal, import.meta.url) - similar to how it handles with { type: "file" } imports. This is already tracked in https://github.com/oven-sh/bun/issues/15032.

divyenduz avatar Oct 11 '25 15:10 divyenduz

I have same issues with Vite (rolldowl-vite in React Router v7). I'm Using PGLite with Playwright to run db tests in CI, so it's a dev dep.

Error: ENOENT: no such file or directory, open '/Users/bob/web/myapp/foo/build/pglite.wasm'
    at open (node:internal/fs/promises:642:25)
    at Module.readFile (node:internal/fs/promises:1279:14)
    at Tr (file:///Users/bob/web/myapp/node_modules/.pnpm/@[email protected]/node_modules/@electric-sql/pglite/dist/chunk-3WWIVTCY.js:1:18243) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/Users/bob/web/myapp/foo/build/pglite.wasm'
}

Solution was to exlude it optimizeDeps.exclude (like the docs suggest) and only use a dynamic import const { PGlite } = await import('@electric-sql/pglite').

Also a simple Vite plugin to copy the needed files over does the job. So new URL('./pglite.data', import.meta.url) works, because obvs import.meta.url points to the build dir:

export default defineConfig(({ command }) => ({
  plugins: [
    {
      name: 'copy-pglite-files',
      async buildStart(_options) {
        if (command === 'serve') return
        const pgliteDir = 'node_modules/@electric-sql/pglite/dist'
        cpSync(`${pgliteDir}/pglite.data`, 'build/pglite.data')
        cpSync(`${pgliteDir}/pglite.wasm`, 'build/pglite.wasm')
      },
    },
  ],
  optimizeDeps: {
    exclude: ['@electric-sql/pglite'],
  },
  // ...
}))

hilja avatar Nov 05 '25 14:11 hilja

import { PGlite } from "@electric-sql/pglite"; import type { PGliteOptions } from "@electric-sql/pglite";

// Import using Bun's file loader - these get embedded in the compiled binary import wasmPath from "./pglite-assets/pglite.wasm" with { type: "file" }; import dataPath from "./pglite-assets/pglite.data" with { type: "file" };

export async function createPGlite( dataDir: string, options?: PGliteOptions ): Promise<PGlite> { // Read the embedded files const [wasmBuffer, dataBuffer] = await Promise.all([ Bun.file(wasmPath).arrayBuffer(), Bun.file(dataPath).arrayBuffer(), ]);

// Compile the WASM module const wasmModule = await WebAssembly.compile(wasmBuffer);

// Create a Blob for the fs bundle const fsBundle = new Blob([dataBuffer]);

// Create PGlite instance with pre-loaded modules const db = await PGlite.create(dataDir, { ...options, wasmModule, fsBundle, });

return db; }

@divyenduz great workaround! did you ever figure out how to get it to work with extension e.g. vector?

I've tried adding import vectorPath from "./pglite-assets/vector.tar.gz" with { type: "file" }; yet still hitting:

     detail: "Could not open extension control file \"/tmp/pglite/share/postgresql/extension/vector.control\": No such file or directory.",
       hint: "The extension must first be installed on the system where PostgreSQL is running.",

Dizzzmas avatar Nov 14 '25 18:11 Dizzzmas