fix(cli): ensure type:module before Vite loads vite.config.ts (fixes #184)#197
Conversation
|
/bigbonk review |
commit: |
|
@copilot The failing job encountered repeated 500 Internal Server Error responses from GitHub during git fetch operations, resulting in exit code 128. This is not an issue with your code or workflow configuration. |
|
@southpolesteve @elithrar kindly review the changes. |
|
kindly re-review the changes. There was a GitHub issue which failed the deploy examples/ Comment Preview URL |
|
/bigbonk review this again please! |
There was a problem hiding this comment.
Review Summary
The fix addresses a real problem (issue #184): projects without "type": "module" in package.json break Vite's config loading on Node 22. The approach of reusing ensureESModule + renameCJSConfigs from utils/project.ts is sound, and the test coverage is thorough.
However, I have concerns about silently mutating the user's package.json and renaming their config files as a side effect of vinext dev / vinext build. This is a significant behavioral change that needs more careful handling. I also found some issues in the implementation.
Key Concerns
-
Silent file mutations during dev/build are dangerous. Renaming
postcss.config.js→.cjsand rewritingpackage.jsonduring avinext devinvocation can surprise users. If the process crashes or is killed mid-write, it can leave the project in an inconsistent state (e.g.,postcss.config.jsrenamed buttype:modulenot yet added). Consider wrapping both operations atomically, or at minimum doing thepackage.jsonwrite before the renames so a partial failure is recoverable. -
The warning message is misleading. It says "Run
vinext initto make this change permanent" — but the change was already written to disk by the time the user sees this message. There's nothing impermanent about it. Either remove the misleading language or make the behavior truly non-persistent (e.g., settype:moduleas an env/flag that Vite reads, rather than mutatingpackage.json). -
Tests are thorough but don't test the actual
ensureViteConfigCompatibilityfunction. The new tests exerciseensureESModuleandrenameCJSConfigsin isolation (which already had tests in the same file). The new wrapper functionensureViteConfigCompatibility— which has its own logic (checkinghasViteConfig(), reading and parsingpackage.json, the early-return conditions) — has no direct tests. This is the code most likely to have bugs. -
Two
describeblocks with near-identical tests. The PR adds both "ESM config compatibility — issue #184" and "ensureESModule + renameCJSConfigs — issue #184 compatibility" blocks that largely duplicate each other and also duplicate the existingensureESModuleandrenameCJSConfigsdescribe blocks.
| ` warning.` | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Bug: ensureViteConfigCompatibility duplicates package.json parsing.
This function reads and parses package.json itself (lines 201-212), then calls ensureESModule(root) which reads and parses the same package.json again. More importantly, if ensureESModule returns false for any reason (file disappeared, parse error in its own try/catch), the if (added) check silently swallows the failure — the user gets no feedback that something went wrong after CJS configs were already renamed.
This redundant parsing also means there's a TOCTOU race: the function checks pkg.type === "module" at line 212, then ensureESModule checks it again. If something else modifies the file between the two reads, behavior is unpredictable.
Consider removing the duplicate parsing and just calling ensureESModule directly, letting it handle the idempotency check internally (which it already does):
| } | |
| function ensureViteConfigCompatibility(root: string): void { | |
| // Only act when there is a vite.config — auto-config mode sets | |
| // configFile: false and doesn't go through Vite's file-loading path. | |
| if (!hasViteConfig()) return; | |
| const pkgPath = path.join(root, "package.json"); | |
| if (!fs.existsSync(pkgPath)) return; | |
| // Rename any `.js` CJS config files first so they don't break after we | |
| // add "type": "module". | |
| const renamed = renameCJSConfigs(root); | |
| for (const [oldName, newName] of renamed) { | |
| console.warn( | |
| ` [vinext] Renamed ${oldName} → ${newName} (required for "type": "module")` | |
| ); | |
| } | |
| // Add "type": "module" so Vite loads vite.config.ts as ESM. | |
| const added = ensureESModule(root); | |
| if (added) { | |
| console.warn( | |
| ` [vinext] Added "type": "module" to package.json so Vite can load` + | |
| ` vite.config.ts as ESM.\n` + | |
| ` Run \`vinext init\` to review all project configuration.` | |
| ); | |
| } | |
| } |
packages/vinext/src/cli.ts
Outdated
| console.warn( | ||
| ` [vinext] Added "type": "module" to package.json so Vite can load` + | ||
| ` vite.config.ts as ESM.\n` + | ||
| ` Run \`vinext init\` to make this change permanent and avoid this` + |
There was a problem hiding this comment.
Misleading warning message. The change is already written to package.json by this point — it's not temporary or reversible. Telling users to "run vinext init to make this change permanent" implies the change isn't permanent yet, which is confusing.
Consider rewording to something like:
[vinext] Added "type": "module" to package.json (required for Vite ESM config loading).
Run `vinext init` to review all project configuration.
|
|
||
| // Rename any `.js` CJS config files first so they don't break after we | ||
| // add "type": "module". | ||
| const renamed = renameCJSConfigs(root); |
There was a problem hiding this comment.
CJS config renames happen even when type:module is already set. The early return if (pkg.type === "module") return; at line 212 prevents this in the normal case, but if package.json parsing fails (line 207 catch), the function returns early before renames. That's fine. However, if someone has "type": "commonjs" explicitly set, this code will rename their CJS configs and add "type": "module" — overriding an explicit user choice. You might want to check for pkg.type !== undefined (any explicit value) rather than just pkg.type === "module".
| // and generate `require()` calls for ESM-only packages like | ||
| // @cloudflare/vite-plugin, which fails on Node 22 with: | ||
| // "Dynamic require of .../index.mjs is not supported" | ||
| // |
There was a problem hiding this comment.
Duplicate test coverage. This describe("ESM config compatibility — issue #184") block adds tests for ensureESModule and renameCJSConfigs that are largely identical to the existing describe("ensureESModule") (line 860) and describe("renameCJSConfigs") (line 906) blocks, and also to the second new block describe("ensureESModule + renameCJSConfigs — issue #184 compatibility") (line 1868).
For example:
- "adds type:module when vite.config.ts is present" — same as existing test at line 861
- "is idempotent" — same as existing test at line 872
- "renames CJS postcss.config.js" — same as existing test at line 907
Consider removing one of the two new describe blocks entirely (they test the same functions) and focusing the remaining tests on scenarios not already covered — like the monorepo case and the "preserves all fields" case, which are genuinely new.
packages/vinext/src/cli.ts
Outdated
| function ensureViteConfigCompatibility(root: string): void { | ||
| // Only act when there is a vite.config — auto-config mode sets | ||
| // configFile: false and doesn't go through Vite's file-loading path. | ||
| if (!hasViteConfig()) return; |
There was a problem hiding this comment.
hasViteConfig() uses process.cwd() but root is passed as a parameter. The hasViteConfig() function (defined at line 135) hardcodes process.cwd(), while this function accepts root as a parameter. If anyone ever calls ensureViteConfigCompatibility with a root different from process.cwd(), the vite config check will look in the wrong directory.
Currently this is safe because both call sites pass process.cwd(), but it's fragile. Consider using the hasViteConfig(root) from utils/project.ts instead, which accepts a root parameter:
| if (!hasViteConfig()) return; | |
| if (!hasViteConfig()) return; |
Should be:
// Import hasViteConfig from utils/project.ts which accepts root param
// or inline the check here
const hasConfig =
fs.existsSync(path.join(root, "vite.config.ts")) ||
fs.existsSync(path.join(root, "vite.config.js")) ||
fs.existsSync(path.join(root, "vite.config.mjs"));
if (!hasConfig) return;
Note: There's already a hasViteConfig(root: string) exported from utils/project.ts (line 107) that does exactly this — you're already importing from that module.
|
Review posted successfully. Here's a summary of the feedback I left on PR #197: Review: REQUEST_CHANGESThe fix addresses a real problem (issue #184) and the approach of reusing shared utilities is sound. However, I identified several issues: Implementation bugs/concerns:
Test concerns:
|
…loudflare#184) Projects that lack "type":"module" in package.json caused Vite to bundle vite.config.ts through esbuild's CJS path, producing a .vite-temp/*.mjs file that contains a CJS require() for @cloudflare/vite-plugin. On Node 22 in CI (GitHub Actions ubuntu-latest), require()-ing an .mjs file is not supported and the build fails with: Error: Dynamic require of "…/@cloudflare/vite-plugin/dist/index.mjs" is not supported Fix: call ensureESModule(root) + renameCJSConfigs(root) in both the dev() and buildApp() CLI commands, before Vite is loaded. This mirrors what vinext init already does, but applies lazily for projects that were set up manually or before the init step was introduced. - renameCJSConfigs renames postcss.config.js / tailwind.config.js / .eslintrc.js etc. to .cjs first, to avoid breakage in those files after ESM mode is enabled - ensureESModule adds "type":"module" to package.json - Both are no-ops when the project is already correctly configured - A warning is emitted when a change is made, pointing users to vinext init - 9 new unit tests cover the full scenario including multi-file rename Fixes cloudflare#184
edadc53 to
f1c52d4
Compare
|
@southpolesteve Kindly review the latest changes. The workflow is all green. |
|
@hyoban Kindly review the latest changes. The workflow is all green. |
Problem
Projects without
"type": "module"inpackage.jsoncause Vite to bundlevite.config.tsthrough esbuild's CJS path, producing a.vite-temp/*.timestamp.mjsfile that containsrequire()calls for the imported plugins. On Node 22 in CI (GitHub Actions, ubuntu-latest),require()-ing a.mjsfile is not supported and the build fails immediately:The affected user had set up their project manually (without
vinext init) or before theinitstep added"type": "module".vinext initalready adds this field, butvinext dev/vinext buildwere not enforcing it.Fix
Call
ensureESModule(root)+renameCJSConfigs(root)in both thedev()andbuildApp()CLI commands, before Vite is loaded. This reuses the same logic already used byvinext init.renameCJSConfigsrenamespostcss.config.js/tailwind.config.js/.eslintrc.jsetc. →.cjsfirst, preventing breakage in those files once"type": "module"is addedensureESModuleadds"type": "module"topackage.jsonvinext initto make it permanentTests
9 new unit tests added to
tests/deploy.test.tsunder the describe block "ensureESModule + renameCJSConfigs — issue #184 compatibility":type:modulewhen missingfalsewhen nopackage.jsonexistspostcss.config.js→postcss.config.cjsbefore addingtype:modulepostcss.config.js(nomodule.exportsorrequire())tailwind.config.jswhen CJS.eslintrc.jswhen CJSrequire()(not onlymodule.exports)All 169 tests pass (
pnpm exec vitest run tests/deploy.test.ts).Fixes #184