Skip to content

feat(plugin-vite): add ability to ship esm bundle#4184

Open
erickzhao wants to merge 15 commits intonextfrom
esm-cjs-interop-vite-plugin
Open

feat(plugin-vite): add ability to ship esm bundle#4184
erickzhao wants to merge 15 commits intonextfrom
esm-cjs-interop-vite-plugin

Conversation

@erickzhao
Copy link
Copy Markdown
Member

@erickzhao erickzhao commented Mar 20, 2026

Closes #4016

Automatically inferring based on type: module in package.json limits flexibility for apps that want to ship CommonJS within their Electron app.

ESM in Electron has its own set of limitations and caveats, and CommonJS is still the standard way to ship your JavaScript app bundle.

This PR takes a different approach by adding type: module/commonjs as an option to the Vite plugin, and modifying the bundle output in that manner as well.

@erickzhao erickzhao added the next label Mar 24, 2026
@erickzhao erickzhao marked this pull request as ready for review March 24, 2026 19:51
@erickzhao erickzhao requested a review from a team as a code owner March 24, 2026 19:51
@erickzhao erickzhao changed the title feat(plugin-vite): configurable esm + cjs options feat(plugin-vite): add ability to ship esm bundle Mar 24, 2026
inlineDynamicImports: true,
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
entryFileNames: isEsm ? '[name].mjs' : '[name].js',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make the other .cjs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense to me.

input: forgeConfigSelf.entry,
output: {
format: 'cjs',
format: isEsm ? 'es' : 'cjs',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: ESM preloads will only work in unsandboxed renderers according to the docs.

Since the Vite config doesn't have access to the parameters passed to the BrowserWindow constructor to allow us to pick the right format, we could account for that by tweaking the JSDoc in packages/plugin/vite/src/Config.ts to make this caveat clear, and maybe even adding some troubleshooting code like the following directly to the createWindow function in the Vite templates' main files to preemptively provide support for this issue:

// ESM preloads only work if your renderer is unsandboxed, which is disabled
// by default for security reasons. If your preload file fails to load and
// your renderer is sandboxed (i.e. the `webPreferences.sandbox` option in your
// `BrowserWindow` constructor is `true` or isn't set), please set
// `config.build.rollupOptions.output.format` to `commonjs` in your
// `vite.preload.config.ts` file.
mainWindow.webContents.on('preload-error', (event, preloadPath, error) => {
  if(preloadPath.endsWith('.mjs') &&

    // optional - these might be unnecessary or even wrong, but syntax errors
    // thrown when using `import` or top-level `await` in a non-ESM context
    // both contain the word "module" and they're bound to be the most common
    // ones in this scenario 🤷🏼
    // would be fine to omit these conditions, though, even if it means
    // showing this message for unrelated errors thrown in the preload.
    error.stack?.startsWith('SyntaxError') &&
    error.message.includes('module')
  ) {
    console.error(`Fail to load ${preloadPath}. Make sure you're using the \`commonjs\` output format in \`vite.preload.config.ts\` if your renderer is sandboxed.`)
  }
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erickzhao
Copy link
Copy Markdown
Member Author

@erikian @MarshallOfSound I updated the PR!

Importantly, I changed the output to always have .cjs and .mjs for both main process and preload outputs. This is a breaking change for existing plugin users, but has the benefit of making the output module format very explicit.

Alternatives considered:

  • We could change it back to only have preload targets output .mjs and make type: module in package.json required for ESM (minimal change).
  • We could make .mjs for ESM and .js for CJS (no breaking changes).

Not sure what you guys prefer design-wise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants