Skip to content

fix(cli): ensure type:module before Vite loads vite.config.ts (fixes #184)#197

Open
yunus25jmi1 wants to merge 1 commit intocloudflare:mainfrom
yunus25jmi1:fix/issue-184-cloudflare-plugin-esm-require
Open

fix(cli): ensure type:module before Vite loads vite.config.ts (fixes #184)#197
yunus25jmi1 wants to merge 1 commit intocloudflare:mainfrom
yunus25jmi1:fix/issue-184-cloudflare-plugin-esm-require

Conversation

@yunus25jmi1
Copy link
Contributor

Problem

Projects without "type": "module" in package.json cause Vite to bundle vite.config.ts through esbuild's CJS path, producing a .vite-temp/*.timestamp.mjs file that contains require() calls for the imported plugins. On Node 22 in CI (GitHub Actions, ubuntu-latest), require()-ing a .mjs file is not supported and the build fails immediately:

failed to load config from /home/runner/work/.../vite.config.ts
Error: Dynamic require of "file:///…/@cloudflare/vite-plugin/dist/index.mjs" is not supported
    at file:///.../node_modules/.vite-temp/vite.config.ts.timestamp-xxx.mjs:5:9

The affected user had set up their project manually (without vinext init) or before the init step added "type": "module". vinext init already adds this field, but vinext dev / vinext build were not enforcing it.

Fix

Call ensureESModule(root) + renameCJSConfigs(root) in both the dev() and buildApp() CLI commands, before Vite is loaded. This reuses the same logic already used by vinext init.

  • renameCJSConfigs renames postcss.config.js / tailwind.config.js / .eslintrc.js etc. → .cjs first, preventing breakage in those files once "type": "module" is added
  • ensureESModule adds "type": "module" to package.json
  • Both are no-ops when the project is already correctly configured (no extra I/O on the hot path)
  • A warning is printed when a change is made, pointing users to run vinext init to make it permanent

Tests

9 new unit tests added to tests/deploy.test.ts under the describe block "ensureESModule + renameCJSConfigs — issue #184 compatibility":

  • Adds type:module when missing
  • Idempotent — safe to call multiple times
  • Returns false when no package.json exists
  • Renames CJS postcss.config.jspostcss.config.cjs before adding type:module
  • Skips rename for ESM-style postcss.config.js (no module.exports or require())
  • Renames tailwind.config.js when CJS
  • Renames .eslintrc.js when CJS
  • Renames CJS config that uses require() (not only module.exports)
  • Handles multiple CJS config files simultaneously

All 169 tests pass (pnpm exec vitest run tests/deploy.test.ts).

Fixes #184

@southpolesteve
Copy link
Collaborator

/bigbonk review

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@197

commit: 6f43066

@yunus25jmi1
Copy link
Contributor Author

@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.

@yunus25jmi1
Copy link
Contributor Author

@southpolesteve @elithrar kindly review the changes.

@yunus25jmi1
Copy link
Contributor Author

kindly re-review the changes. There was a GitHub issue which failed the deploy examples/ Comment Preview URL

@southpolesteve
Copy link
Collaborator

/bigbonk review this again please!

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

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

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

  1. Silent file mutations during dev/build are dangerous. Renaming postcss.config.js.cjs and rewriting package.json during a vinext dev invocation 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.js renamed but type:module not yet added). Consider wrapping both operations atomically, or at minimum doing the package.json write before the renames so a partial failure is recoverable.

  2. The warning message is misleading. It says "Run vinext init to 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., set type:module as an env/flag that Vite reads, rather than mutating package.json).

  3. Tests are thorough but don't test the actual ensureViteConfigCompatibility function. The new tests exercise ensureESModule and renameCJSConfigs in isolation (which already had tests in the same file). The new wrapper function ensureViteConfigCompatibility — which has its own logic (checking hasViteConfig(), reading and parsing package.json, the early-return conditions) — has no direct tests. This is the code most likely to have bugs.

  4. Two describe blocks 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 existing ensureESModule and renameCJSConfigs describe blocks.

` warning.`
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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):

Suggested change
}
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.`
);
}
}

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` +
Copy link
Contributor

Choose a reason for hiding this comment

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

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

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"
//
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 4, 2026

Review posted successfully. Here's a summary of the feedback I left on PR #197:

Review: REQUEST_CHANGES

The fix addresses a real problem (issue #184) and the approach of reusing shared utilities is sound. However, I identified several issues:

Implementation bugs/concerns:

  1. Duplicate package.json parsing in ensureViteConfigCompatibility — it reads/parses the file, then ensureESModule reads/parses it again, creating a TOCTOU race
  2. hasViteConfig() uses process.cwd() while the function accepts a root parameter — there's already a hasViteConfig(root) in utils/project.ts that should be used instead
  3. Misleading warning message — says "run vinext init to make this change permanent" but the change was already permanently written to disk
  4. No guard against explicit "type": "commonjs" — the code would override an explicit user choice
  5. Non-atomic file mutations — if the process crashes between CJS renames and package.json write, the project is left in an inconsistent state

Test concerns:

  • Two new describe blocks with near-identical tests that also duplicate existing test coverage
  • The actual new function (ensureViteConfigCompatibility) is not directly tested — only the underlying utilities it calls

github run

…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
@yunus25jmi1 yunus25jmi1 force-pushed the fix/issue-184-cloudflare-plugin-esm-require branch from edadc53 to f1c52d4 Compare March 5, 2026 00:28
@yunus25jmi1
Copy link
Contributor Author

@southpolesteve Kindly review the latest changes. The workflow is all green.

@yunus25jmi1
Copy link
Contributor Author

@hyoban Kindly review the latest changes. The workflow is all green.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vinext build fails in CI (GitHub Actions): Dynamic require of @cloudflare/vite-plugin not supported in ESM context

2 participants