Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependency accept callback does not work in watch mode #4950

Closed
6 tasks done
nlwillia opened this issue Jan 13, 2024 · 2 comments
Closed
6 tasks done

Dependency accept callback does not work in watch mode #4950

nlwillia opened this issue Jan 13, 2024 · 2 comments
Labels

Comments

@nlwillia
Copy link

Describe the bug

Vite's HMR API has a deps signature that lets a module observe the reload of a dependency without reloading itself.

This doesn't currently seem to work with vite-node.

The repro example is a simple module that starts a http server and imports a value from a dependent. The base module has an accept callback for the dependency.

if (import.meta.hot) {
  import.meta.hot.accept(['./dep.js'], ([dep]) => {
    currentAnswer = dep?.answer;
    console.log(`updated answer to ${dep?.answer}`);
  });
}

The expectation is that if the dep is modified, the parent module will not reload, but it's accept callback will run.

Instead, the behavior depends on a variety of factors.

Mode ts vs. js means running vite-node -w basic.ts vs. compiling to js first and running vite-node -w basic.js. Result is what happens when dep.ts (or js) is changed.

Mode OS Result
ts Linux EADDRINUSE
js Linux [vite-node] hot updated: /src/dep.js via ..., however the answer doesn't change
ts Windows EADDRINUSE
js Windows EADDRINUSE

Running a server provides a way of seeing when the base module is reloading and shouldn't. In all cases, I was expecting to see the hot updated message and the answer to change, but the only case where the deps-based accept was recognized and the module didn't reload was js/Linux, and there the accept callback never runs.

There seems to be a lot of sensitivity internally to how paths are registered with hotModulesMap in hmr.ts being used to relate paths in events coming back from the vite dev server based on the transformed source to the dep specifiers registered by the module's HMR API call. That is only partly working on Linux, and not at all on Windows.

I noticed while debugging that Windows paths were getting particularly mangled. The issue seems to be in the base importAnalysis plugin where vite uses path.posix to normalize the url. vite-node is passing in a path like C:/project/file.ts which gets turned into /project/C:/project/file.ts. This can be avoided by passing a posix-absolute /C:/project/file.ts to begin with, but there's a lot of identifier juggling going on in this code-base, and I don't understand it well enough to make a more specific recommendation.

Reproduction

https://stackblitz.com/edit/vitest-dev-vitest-ien7n8?file=src%2Fbasic.ts

System Info

System:
    OS: Windows 10 10.0.19045
    CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor
    Memory: 13.64 GB / 31.92 GB
  Binaries:
    Node: 20.9.0 - C:\Program Files\nodejs\node.EXE
    npm: 10.1.0 - C:\Program Files\nodejs\npm.CMD
    pnpm: 8.14.1 - C:\Program Files\nodejs\pnpm.CMD
  Browsers:
    Chrome: 120.0.6099.217
    Edge: Chromium (120.0.2210.121)
    Internet Explorer: 11.0.19041.3636

Used Package Manager

pnpm

Validations

@sheremet-va
Copy link
Member

This is a known issue, it will be fixed by vitejs/vite#12165

@nlwillia
Copy link
Author

nlwillia commented Feb 1, 2024

I saw that the other issue was merged and wanted to try it out. While it's still very early (vite 5.1 isn't published yet), things do seem to be working with that branch. Here's the setup for anyone else who finds this.

  1. Vite 5.1 or later (I grabbed the beta from vite-envs and referenced it as a file dependency "vite": "file:./vite-5.1.0-beta.2.tgz")
  2. A runner that creates a dev server as illustrated in the other issue, minimally:
// runner.js, run as `node runner.js`
import {createServer, createViteRuntime} from 'vite'

await (await (createViteRuntime(await createServer()))).executeEntrypoint('/index.ts')
  1. An index file with an accept function. import.meta.hot.accept(['/mod1.ts', '/mod2.ts'], rebindFn)

Some things to note about accepting...

  • The dep names have to be exact. ./mod.ts, ./mod.js or /mod.js won't match. (This can be confusing if the module specifier at the source level is not the same as Vite's internal normalization.)
  • The accept argument is the whole module rather than just what the source imported.
  • Modules that don't change are positionally undefined in the accept array argument.

I created a helper function for generalizing a function that's regenerated based on module updates.

function rebindable(fn, ...deps) {
    let currFn = fn(...deps)
    return [(...args) => currFn(...args), mods => (currFn = fn(...mods.map((e, i) => deps[i] = e ?? deps[i])))]
}

And that can be plugged into something like Express...

const [handlerFn, rebindFn] = rebindable(({hmr1}, {hmr2}) => {
    return (req, res) => {
        res.status(200).send(`hmr1=${hmr1()} hmr2=${hmr2()}`)
    }
}, {hmr1}, {hmr2})

Express().use('/', handlerFn).listen(1234)

import.meta.hot?.accept(['/hmr1.ts', '/hmr2.ts'], rebindFn)

I confirmed that the top file wasn't reloaded for dep changes and that deps and deps of deps could update and be seen in the result which was the behavior I was originally expecting. Given the work upstream, this whole package is probably obsolete, so I'll close the issue.

@nlwillia nlwillia closed this as completed Feb 1, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Feb 16, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

2 participants