The generated bridge runs in the preload, not in the renderer. Most setup failures come from generating it in the wrong build, compiling it with the wrong TypeScript project, or loading an unbundled preload in Electron's sandbox.
This guide is organized by symptom. If you have an exact error message, look it up in the diagnostics reference instead.
ipc-bridge.ts from the main-process *.ipc.ts files.electron external to that bundle.preload.Commit the generated source and run electron-ipc-module check in CI. It makes
bridge changes reviewable and lets a fresh clone type-check before generation.
window.ipc is undefinedCheck the earliest failure first:
BrowserWindow points at the built preload, not its TypeScript
source or a stale output path.contextBridge.exposeInMainWorld ran with the same key used by the
renderer.expose option, confirm the current generated file
contains both the exposure call and the global declaration.A sandboxed preload cannot load ESM or resolve arbitrary packages. It must be a
single self-contained CommonJS file. tsc output alone is usually insufficient
inside a "type": "module" package because its .js output remains ESM.
Cannot use import statement outside a moduleThe sandbox's preload loader received ESM. Bundle the preload to CommonJS instead of renaming the output. A Rollup configuration can be as small as:
export default {
input: "dist/preload.js",
external: ["electron"],
output: { file: "dist/preload.cjs", format: "cjs", inlineDynamicImports: true },
};
An ESM preload requires sandbox: false and an .mjs extension. Prefer
keeping the sandbox and bundling to CommonJS.
Verify all of the following:
*.ipc.ts; test files containing .test. are
intentionally ignored.ipcDir resolves from the build process's working directory and selects the
expected files.tsconfig is an application config that includes those files. A solution
config with "files": [] is not enough.defineIpcModule call with a string-literal
prefix and preferably a plain channel object literal.The built-in logger discards debug, so the per-module channel and event counts
only appear when you pass your own logger to the plugin or the generator API —
the CLI has no flag for it. Analyzer warnings mean part of the bridge could not
be typed completely, and --quiet does not suppress them.
With expose: "ipc", the generated file declares Window.ipc. The renderer's
TypeScript project still has to include that file. Check include, files, and
project references, then restart the editor's TypeScript server after changing
the project graph.
Without expose, derive the declaration instead of duplicating signatures:
import type { bridge } from "../main/generated/ipc-bridge.js";
declare global {
interface Window {
ipc: typeof bridge;
}
}
Regenerate with exactly the same options used by check. The expose key,
ipcDir, outFile, and tsconfig all affect the expected output.
npx electron-ipc-module generate \
--ipc-dir ./main/ipc \
--out-file ./main/generated/ipc-bridge.ts \
--tsconfig ./tsconfig.preload.json \
--expose ipc
npx electron-ipc-module check \
--ipc-dir ./main/ipc \
--out-file ./main/generated/ipc-bridge.ts \
--tsconfig ./tsconfig.preload.json \
--expose ipc
Put the shared arguments in package scripts so local generation and CI cannot drift.
Use generate --watch when no build plugin owns the watch lifecycle. With
Rollup or Vite, put electron-ipc-module/rollup-plugin in the preload build.
Editing an existing *.ipc.ts file should regenerate the bridge. Depending on
the build tool, adding a brand-new IPC file may require restarting watch mode so
the new file enters its graph.
If a monorepo runner changes the working directory, use explicit paths or run the command from the package that owns the IPC files.
The file name selects the generated module property, while kebab-case channels and events become camel-cased methods:
| Source | Generated API |
|---|---|
profile.ipc.ts |
bridge.profile |
get-all |
bridge.profile.getAll() |
event profile-updated |
bridge.profile.onProfileUpdated() |
Keep one module per file. A second defineIpcModule call has no unambiguous
generated property and causes generation to fail rather than silently dropping
an API.
Run a smoke test against the packaged preload, not only a development server. Open a sandboxed, context-isolated hidden window and exercise an invoke, a send, and an emitted event. This catches output-path mistakes, missing files, format errors, and runtime/generator channel drift that TypeScript and mocked tests cannot observe.