Conversation
| inlineDynamicImports: true, | ||
| entryFileNames: '[name].js', | ||
| chunkFileNames: '[name].js', | ||
| entryFileNames: isEsm ? '[name].mjs' : '[name].js', |
There was a problem hiding this comment.
Should we make the other .cjs?
There was a problem hiding this comment.
Yeah, makes sense to me.
| input: forgeConfigSelf.entry, | ||
| output: { | ||
| format: 'cjs', | ||
| format: isEsm ? 'es' : 'cjs', |
There was a problem hiding this comment.
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.`)
}
})|
@erikian @MarshallOfSound I updated the PR! Importantly, I changed the output to always have Alternatives considered:
Not sure what you guys prefer design-wise. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017u4ESDPPS9dMHzk1Yz6cR8
The `-typescript` template packages were removed in #4229, but the end-to-end specs added to them by #4218 survived the merge. Their presence kept `packages/template/{vite,webpack}-typescript` in the tree, so `tools/utils.ts` tried to read a `package.json` that no longer exists, which broke the postinstall tsconfig/glue generation and the build. Also drop the stale template names from the shared `testForgeTemplate` helper. Ported from #4374 so CI on this branch can go green while base `next` is red; this becomes a no-op once next carries the fix. (cherry picked from commit bca8bb0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017u4ESDPPS9dMHzk1Yz6cR8
|
The The fix for Generated by Claude Code |
| if (this.config.outputFormat === 'es') { | ||
| const pj = await readJson(path.resolve(this.projectDir, 'package.json')); | ||
| if (pj.type !== 'module' && !pj.main?.endsWith('.mjs')) { | ||
| throw new Error( | ||
| `The Vite plugin is configured with outputFormat: "es", but your package.json does not have "type": "module" ` + | ||
| `and the main entry point does not use an .mjs extension. Electron requires one of these for ESM support in the main process. ` + | ||
| `See https://www.electronjs.org/docs/latest/tutorial/esm for more details.`, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 The resolveForgeConfig ESM check accepts type: "module" with a main that does NOT end in .mjs, but packageAfterCopy's new extension check (line 338-345) unconditionally requires main to end in .mjs when outputFormat: 'es'. A user who fixes their package.json per the first error message will still crash later during packaging with a different, unrelated-looking error. Fix: make the early check require the same .mjs suffix (or drop the type: 'module' alternative) so both checks agree.
Extended reasoning...
The PR's own test (VitePlugin.spec.ts:128-140) sets { main: '.vite/build/main.js', type: 'module' } with outputFormat: 'es' and asserts resolveForgeConfig succeeds. But packageAfterCopy (VitePlugin.ts:338-345) computes expectedExt = '.mjs' for outputFormat: 'es' and throws because main (main.js) does not end with .mjs, with no type==='module' escape hatch. So the same package.json that passes config resolution at the start of every forge command fails later during package/make, after the (potentially slow) Vite build has already run, with a confusingly different error than the one the user was told to fix.
Verification: nit. The two new validation checks are inconsistent, and this is reachable and codified by a test. resolveForgeConfig (VitePlugin.ts:287-296) accepts outputFormat 'es' when pj.type === 'module' || pj.main?.endsWith('.mjs'), so { main: '.vite/build/main.js', type: 'module' } passes. packageAfterCopy (VitePlugin.ts:338-345) sets expectedExt = '.mjs' and throws unless… | nit. The inconsistency…
There was a problem hiding this comment.
The merge in this branch replaced the static expectedExt check with a dynamic assertMainEntryExists fallback (now checking both .cjs and .mjs candidates), but the underlying disagreement is still there. vite.main.config.ts always emits [name].mjs when outputFormat: 'es' regardless of package.json's type field, so a user who follows resolveForgeConfig's advice and adds "type": "module" while leaving main as foo.js will pass the early check, then still hit a different error out of packageAfterCopy/assertMainEntryExists telling them to rename main to foo.mjs. The message is nicer now, but the two checks still don't actually agree — passing resolveForgeConfig via the type: 'module' branch does not guarantee success later. No test covers this end-to-end path (only resolveForgeConfig alone is tested for type: 'module' + .js main with outputFormat: 'es').
Third round: next gained #4374-#4393 since the last merge, most notably c012507 "feat(bundlers)!: emit main and preload bundles as .cjs" which overlaps this PR's isEsm-driven output naming. Kept the outputFormat-driven .mjs/.cjs ternaries in the Vite config generators (a superset of next's hardcoded .cjs), kept next's existence-based assertMainEntryExists check in packageAfterCopy and extended its upgrade hint to cover .mjs bundles, and dropped this branch's stricter extension-only check there so a custom .js bundle that exists still packages, matching next's new tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017u4ESDPPS9dMHzk1Yz6cR8
There was a problem hiding this comment.
Approved — the merge with next at commit 15b51a3 resolves the two issues I flagged in the prior review (the unconditional .cjs/.mjs extension check in packageAfterCopy that would have broken pre-existing main: '.js' projects, and the mismatch between that check and resolveForgeConfig's ESM validation). packageAfterCopy now only requires the main file to exist, with assertMainEntryExists providing an upgrade hint (extended to also detect stale .js next to .mjs) rather than a hard failure on extension mismatch.
What was reviewed: the reconciled assertMainEntryExists/packageAfterCopy logic in VitePlugin.ts, the outputFormat-driven branching in vite.main.config.ts and vite.preload.config.ts, the new resolveForgeConfig ESM validation in Config.ts, and the corresponding spec updates confirming the non-strict existence check and validation error paths.
Extended reasoning...
Overview
This is a merge-commit update to a previously-reviewed PR that adds an outputFormat ('cjs' | 'es') option to the Vite plugin. The merge reconciles this branch's ESM work with a newly-landed next commit (c012507, "emit main and preload bundles as .cjs") that independently implemented an overlapping existence-based check for stale main entries.
Security risks
None. The change is limited to build/packaging configuration (file extensions, module format selection, and package.json validation) and does not touch authentication, network, or privilege boundaries.
Level of scrutiny
Moderate-to-high is warranted given this touches the packaging path used by every Vite-based Forge app, but the specific delta since my last review is narrow and mechanical: it swaps the previous strict extension-enforcement (introduced in this branch) for the more lenient existence-based check that next had independently added, and extends that check's hint message to also cover .mjs. This directly closes both bugs I had flagged (breaking legacy .js main entries, and inconsistency between the early config-resolution check and the later packaging-time check).
Other factors
Spec files (VitePlugin.spec.ts) were updated to match: they assert that a pre-existing main.cjs file passes silently, that a stale .js next to .mjs produces the upgrade-hint error, and that the ESM validation errors/succeeds as expected. Third-party review threads from erikian and MarshallOfSound show as resolved by non-authors, and BlackHole1 approved an earlier revision. No new findings were produced by this run's bug hunt, and I found nothing further of concern while re-reading the reconciled code.
Closes #4016
Automatically inferring based on
type: moduleinpackage.jsonlimits 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/commonjsas an option to the Vite plugin, and modifying the bundle output in that manner as well.