Skip to content

fix(server): keep this.addWatchFile in load hook on a module's first request - #23861

Open
btea wants to merge 6 commits into
vitejs:mainfrom
btea:fix/addwatchfile-load-first-request
Open

btea wants to merge 6 commits into
vitejs:mainfrom
btea:fix/addwatchfile-load-first-request

Conversation

@btea

@btea btea commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

close #23860
refs #14967, #18103

@btea btea changed the title fix(server): keep in hook on a module's first request fix(server): keep this.addWatchFile in load hook on a module's first request Oct 22, 2026
@sapphi-red

Copy link
Copy Markdown
Member

This PR might also fix #18914

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

Validated this at 6288bda22ab6. The HMR playground passes 130 tests with 1 skipped, including the new first-request <link> case. Applying only the regression fixture to current main@a976df388 fails at the expected boundary (red remains red after editing dep-load.js), so the test distinguishes the fix rather than just covering the steady state.

I also checked the earlier ensure-before-load/revert pair (#12774/#12929). That regression depended on eagerly calling ensureWatchedFile for the resolved request; this change only creates the module-graph entry early, while filesystem watching still occurs after a successful read. I did not find a remaining correctness issue in this diff.

// Ensure the module node exists before `load` runs, so that module info
// and files registered via `this.addWatchFile` in the `load` hook can be
// associated with this module (#23860)
mod ??= await moduleGraph._ensureEntryFromUrl(url, undefined, resolved)

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.

Maybe we should use canSkipImportAnalysis(id) for the second argument. But this bug (?) exist before this PR, so let's check it seprately.

@sapphi-red

Copy link
Copy Markdown
Member

I've added a failing test. I think we need to fix this

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

The rollback added at c7d7322 can remove a pre-existing shared module, not only the node created by this request. removeModuleOnLoadFailure is derived from the raw URL lookup before resolution, but _ensureEntryFromUrl may resolve that new URL to an existing idToModuleMap entry used by another URL. If load then fails, _removeModule deletes the shared id/file entry (and only mod.url from urlToModuleMap), leaving aliases pointing at a detached node. Please have _ensureEntryFromUrl report whether it created the node, or compare the resolved node against the pre-existing id mapping before making rollback destructive. A regression with two URLs resolving to one id, where the second URL load fails, should preserve the first URL module.

@sapphi-red

Copy link
Copy Markdown
Member

I've added a new failing test.

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

The changed head addresses the shared-node rollback case I raised. removeModuleOnLoadFailure is now false when the resolved id already exists, and _removeModule independently retains a node once another URL, an importer, or a completed transform is attached. The new overlapping-load regression exercises the timing-sensitive alias path directly, and the hosted Node 20/22/24/26 plus macOS/Windows matrix is green. I don't see a remaining issue at 60c71b65.

@sapphi-red

Copy link
Copy Markdown
Member

I've pushed a different fix as the code had some bugs:

  1. Assume that /foo resolves to /foo.js, but loading that file fails. The previous code had already registered the module. Its cleanup sees /foo.js differs from the requested /foo, assumes another request needs the entry, and keeps it. Then, if you replace foo.js with foo.ts and retry /foo, Vite reuses the cached /foo.js resolution and fails again, although resolving /foo should find foo.ts.
  2. Assume two requests overlap:
    1. Request A: creates module node M, starts loading, pauses
    2. File changes: invalidates M, allowing a fresh request
    3. Request B: reuses M, starts loading successfully
    4. Request A: fails and removes M from the graph
    5. Request B: continues transforming with M, now detached
    6. Request B’s import analysis can then fail because its importer is missing from the graph, producing ERR_OUTDATED_OPTIMIZED_DEP, even though B loaded successfully.

This branch has not been deployed

No deployments
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.

this.addWatchFile in load is lost on a module's first request

3 participants