Skip to content

[vitest-pool-workers] Cache only per-directory facts when classifying .js modules - #15096

Open
LeSingh1 wants to merge 2 commits into
cloudflare:mainfrom
LeSingh1:fix/module-fallback-type-module-cache
Open

[vitest-pool-workers] Cache only per-directory facts when classifying .js modules#15096
LeSingh1 wants to merge 2 commits into
cloudflare:mainfrom
LeSingh1:fix/module-fallback-type-module-cache

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #15095.

isWithinTypeModuleContext() folds two different kinds of fact into one cached boolean:

  • pkg.type === "module" — a property of the package, so safe to cache per directory.
  • maybeModulePath === filePath — a property of the file being resolved, so not safe to cache per directory.

Both were stored under a directory key, so the first .js file resolved out of a package decided the classification workerd was given for every other file in it. A dual-format package ("main": "dist/index.cjs.js", "module": "dist/index.esm.js", no "type") therefore got its module type from import order: load the ES module build first and the CommonJS build is subsequently returned as an esModule; load the CommonJS build first and the ES module build is returned as a commonJsModule.

This change caches only { typeModule, modulePath } — both package-level facts — and runs the entry-point comparison against filePath on every call. The lookup stays one Map hit per parent directory, so no extra package.json reads are introduced.

The added test resolves both entries of a dual-format package in one order and asserts each gets its own correct type. It fails on main with expected { …(2) } to have property "commonJsModule" and passes with the change. The 12 existing tests in the file are untouched and still pass, and pnpm -F @cloudflare/vitest-pool-workers check:type is clean.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: internal module-resolution fix, no public API or documented behaviour changes.

Note

This is a contribution from an AI agent: Claude Code, Claude Opus 5.


Open in Devin Review

… .js modules

isWithinTypeModuleContext() treats a .js file as ESM when its nearest
package.json has "type": "module" OR when the file is that package's
"module" entry point. The second condition depends on the file, but the
combined result was cached keyed on the directory, so the first .js file
resolved out of a package decided the classification for every other one.

For a dual-format package ("main" CJS, "module" ESM, no "type"), the
CommonJS build could be returned to workerd as an ES module — its
module.exports assignments then produce no exports — or the ESM build
returned as CommonJS, depending on which was imported first.

Cache "type" per directory and compare the "module" entry path per call.
@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6df4bd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Aug 9, 2026
@workers-devprod
workers-devprod requested review from a team and edmundhung and removed request for a team August 9, 2026 01:06
@workers-devprod

workers-devprod commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines 102 to 107
for (const parentPath of parentPaths) {
const cache = dirPathTypeModuleCache.get(parentPath);
if (cache !== undefined) {
return cache;
const cached = dirPathPackageCache.get(parentPath);
if (cached !== undefined) {
return cached.typeModule || cached.modulePath === filePath;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Nested packages can still be misclassified as ES modules based on which file was loaded first

A file's module format is decided from the first already-remembered ancestor folder (dirPathPackageCache.get(parentPath) at packages/vitest-pool-workers/src/pool/module-fallback.ts:103-106) instead of from the closest package description, so a file inside a nested package can be handed to the runtime with the wrong format depending on load order.

Impact: A dependency's file can be loaded as the wrong module format, producing confusing syntax/import errors in tests.

Early-return on any cached ancestor short-circuits the nearest `package.json` lookup

getParentPaths() returns parents nearest-first, but the first loop returns as soon as ANY ancestor is present in the cache, even if a nearer directory has an un-read package.json. Example: resolving /root/a.js caches /root (say type: "module"). Later resolving /root/node_modules/pkg/dist/x.js walks dist, pkg, node_modules, /root — none of the nearer dirs are cached, so /root's entry wins and pkg/package.json is never read, classifying the CommonJS dependency as ESM. The same applies to the new cached.modulePath === filePath comparison, which is then made against an unrelated package's module entry.

This hole predates the PR but is the same order-dependent misclassification class the PR sets out to fix, and it also makes the new test order-sensitive if a temp-root ancestor gets cached first. A fix would be to only consult the cache for the nearest directory that actually contains a package.json (e.g. also cache "no package.json here" negatives so the walk continues correctly).

Prompt for agents
In packages/vitest-pool-workers/src/pool/module-fallback.ts, isWithinTypeModuleContext() walks parent directories nearest-first and returns on the first cached DirPathPackageInfo it finds. Because only directories that actually contained a package.json are ever inserted into dirPathPackageCache, a cached far ancestor can short-circuit the walk before a nearer, not-yet-read package.json is discovered, causing a nested package's files to be classified using the wrong package's `type`/`module` fields (order-dependent). Consider recording negative results too (directories known to have no package.json) so the cached walk mirrors exactly the same nearest-package.json semantics as the uncached filesystem walk, or merge the two loops into a single walk that consults the cache per directory and falls back to reading package.json for that same directory before moving up.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread .changeset/module-fallback-type-module-cache.md Outdated
@pkg-pr-new

pkg-pr-new Bot commented Aug 9, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15096

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15096

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15096

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15096

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15096

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15096

miniflare

npm i https://pkg.pr.new/miniflare@15096

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15096

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15096

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15096

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15096

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@15096

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15096

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15096

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15096

wrangler

npm i https://pkg.pr.new/wrangler@15096

commit: e6df4bd

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +114 to +119
const info: DirPathPackageInfo = {
typeModule: pkg.type === "module",
modulePath: pkg.module ? posixPath.join(parentPath, pkg.module) : "",
};
dirPathPackageCache.set(parentPath, info);
return info.typeModule || info.modulePath === filePath;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Multi-file ES module builds of packages without a module type declaration are now treated as CommonJS

Files that sit next to a package's ES module entry point are no longer classified as ES modules (info.typeModule || info.modulePath === filePath at packages/vitest-pool-workers/src/pool/module-fallback.ts:119), so tests importing a package whose ES module build is split across several files fail with syntax errors.
Impact: Tests that depend on packages shipping a multi-file ES module build without a declared module type can break with parse errors that did not occur before.

Why the per-directory cache change removes ESM classification from sibling chunks

Previously, resolving dist/index.esm.js of a package whose package.json has "module": "dist/index.esm.js" and no "type" cached true for the package directory, so every other .js file resolved from that package (e.g. relative chunk imports like ./chunk-abc.js emitted by Rollup/esbuild ESM builds) was also returned to workerd as an esModule.

After this change the cache stores { typeModule: false, modulePath: "…/dist/index.esm.js" }, and the entry-point comparison is made per file. The entry itself is still ESM, but each sibling chunk now fails both checks and is returned as a commonJsModule, so workerd parses export/import statements as CommonJS and throws.

These chunk files are resolved directly on disk in maybeGetTargetFilePath() (packages/vitest-pool-workers/src/pool/module-fallback.ts:199-221), so Vite never re-classifies them; the classification at module-fallback.ts:556-558 is authoritative.

A possible mitigation is to keep treating files reached from an ESM entry point (or files inside the directory containing the module entry) as ESM, rather than only the exact entry-point path.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Oh.. good catch. This looks like a regression from the current solution 🤔

edmundhung

This comment was marked as outdated.

workers-devprod

This comment was marked as outdated.

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

Labels

None yet

Projects

Status: Approved

3 participants