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

Reference: Vite #7

Open
yyx990803 opened this issue May 20, 2020 · 52 comments
Open

Reference: Vite #7

yyx990803 opened this issue May 20, 2020 · 52 comments

Comments

@yyx990803
Copy link

Maybe you are not aware of it, but Vite has a working implementation of ESM-based HMR that covers most of the proposed usage. I think at the very least it would serve as a useful reference for this spec. Being included in the discussion also helps alignment of the API design to avoid potential fragmentation.

@FredKSchott
Copy link
Owner

@yyx990803 oh nice! For some reason I was under the impression that your HMR implementation leveraged Rollup internally, not sure why I thought that.

Any interest on collaborating on this then? We're kind of working backwards from "working implementation" towards "well-documented spec", so it's still very early.

from a quick look at your implementation, it looks like there's a few vue-specific things that make sense for Vite but not for a generalized implementation, which could be a blocker unless it could sit nicely on top of a generalize spec.

I'd also love to understand the limitations/tradeoffs of your event bubbling strategy. I think we just got something working in FredKSchott/snowpack#335 (review) that could be interesting for Vite, would love to get your thoughts on that if you have any.

@FredKSchott
Copy link
Owner

FredKSchott commented May 20, 2020

also lol where are all these 👍 reaction emojis coming from? 👋👋👋

@yyx990803
Copy link
Author

yyx990803 commented May 20, 2020

  • AFAIK Rollup doesn't have anything built-in related to HMR.

  • The Vue specific parts don't really affect the JS-oriented APIs. Technically the Vue file HMR can be done using the JS API, but in the case of Vite I just implemented it directly to be a bit more efficient.

  • Vite's HMR does support bubbling. @JoviDeCroock ran into some unrelated issues that led him to assume there was no bubbling but it has been fixed.

Disclosure: I shared this repo with the Vue team since it's relevant to what we are working on.

@FredKSchott
Copy link
Owner

FredKSchott commented May 21, 2020

Gotcha, yea I was aware of how you did event bubbling but curious how it compared to what we've implemented. But, I realize I also didn't provide much details about what we've implemented. Let me write up how our event bubbling works and the can ping you for feedback.

If you're interested in collaborating on this together, let me know! I'd genuinely love to if we can align on a common design and save us both some duplicate implementation time. From a quick look at vite, the differences we'd want to iron out are:

  • import.meta.hot vs. import hot from "some-package"
  • if (import.meta.dot) vs if (__DEV__) conditional wrapping
  • (in vite, not snowpack) accept() dependency whitelist
  • (in snowpack, not sure vite) invalidate() & decline()
  • (in snowpack, not sure vite) data passing from dispose() to accept()

@yyx990803
Copy link
Author

yyx990803 commented May 21, 2020

import.meta.hot vs. import hot from "some-package"

👍 Getting rid of tool-specific imports is a good idea. I see it's now been updated to get rid of the import + manual context creation and directly use import.meta.hot - I think that makes sense and should be fairly straightforward for Vite to support.

if (import.meta.hot) vs if (__DEV__) conditional wrapping

If we move to import.meta.hot, then this also makes sense.

(in vite, not snowpack) accept() dependency whitelist

This is supported in webpack's HMR, and in the past it's been useful in the cases of e.g. a global state store that can hot swap its sub modules.

(in snowpack, not sure vite) invalidate() & decline()

I've never found a use case for these two APIs to be honest (after supporting HMR for Vue, React and Preact), but they should be fairly easy to support.

It seems invalidate isn't much different from just calling location.reload()?

@FredKSchott
Copy link
Owner

This is supported in webpack's HMR, and in the past it's been useful in the cases of e.g. a global state store that can hot swap its sub modules.

👍 Makes sense to add for Snowpack & ESM-HMR.

It seems invalidate isn't much different from just calling location.reload()?

In Webpack's implementation both events are supposed to bubble up to the parent, so they act more like forcing a "file changed' from the client instead of from the server file watcher. But at the moment we go for the naive solution and just force a refresh.

"A module can accept a dependency, but can call invalidate when the change of the dependency is not handleable" - https://webpack.js.org/api/hot-module-replacement/#invalidate

This use-case makes sense in theory, although I agree I haven't seen it in practice either and would be fine to leave this behavior undefined for now (ex: "the ESM-HMR client MAY bubble this event up to be handled by a parent, or refresh the page)

@FredKSchott
Copy link
Owner

As far as I can tell those are the only differences, so it feels like we're actually not far off here! I added some more details to the README, so it might make sense for you to look over those and make sure I didn't miss anything about Vite.

Would a shared TypeScript type declaration be a good artifact to come out of this, that we could both pin to?

@yyx990803
Copy link
Author

I agree I haven't seen it in practice either and would be fine to leave this behavior undefined for now

Would it then make sense to maybe leave them out of the spec for now to reduce the maintenance surface (since users can simply call location.reload() on their own) and see if real use cases emerge in the future?

Would a shared TypeScript type declaration be a good artifact to come out of this, that we could both pin to?

Sounds like a good idea. There is currently one in Vite: https://github.com/vuejs/vite/blob/master/hmr.d.ts - we just need to make a version that augments the ImportMeta global interface instead.

Note Vite currently has a custom hot.on method for arbitrary custom events but I don't think that needs to be part of the spec.

@FredKSchott
Copy link
Owner

FredKSchott commented May 21, 2020

I agree I haven't seen it in practice either and would be fine to leave this behavior undefined for now

Of course right after I write this, I find a use case in react-refresh that I believe both of us want to handle:

I like the idea of a server being allowed to implement this more advanced handling, but not being required to. I think that's a good middle ground for HMR implementers that also protects the HMR consumer from having to leave the sandboxed HMR API to resolve a bad update themselves with a more bare-metal location.reload() (consumer asks, "is location.reload() the right way to do this?").

If you feel strongly the other way, I'm okay with removing for now and then revisiting in a couple of weeks when we tackle that React Fast Refresh use case mentioned above.

Note Vite currently has a custom hot.on method for arbitrary custom events but I don't think that needs to be part of the spec.

👍

@yyx990803
Copy link
Author

I'm actually ok with including them if there are valid use cases, although in this case it seems we can simply skip the accept call if exports is not a refresh boundary? (i.e. just don't accept instead of accept then reject)

@FredKSchott
Copy link
Owner

The scenario would be an exported non-component function added to a module after-the-fact (file goes from all React components to 1+ non-React component exports). Does Vite's implementation handle that? Snowpack's current implementation couldn't without a way to conditionally accept the update.

@yyx990803
Copy link
Author

Oh I see, yeah it would require updating the accept data on the server. I think it makes sense to include them then.

@FredKSchott
Copy link
Owner

If the interface makes sense so far, then I'm curious to talk about implementation details. I think I found an difference that affects how we would implement dependency whitelisting for the accept call, based on https://github.com/vuejs/vite/blob/59da38c185b428a178d320b8bd5187b34bd942aa/playground/testHmrManual.js.

No Dependencies Whitelist

  • Snowpack: accept({module, data} => ...)
  • Vite: accept((module) => ...)

Not a huge difference, assuming you were okay with scoping the module property and adding support for data passing.

Dependencies Whitelist

  • Snowpack: accept(['./some.js', './deps.js'], {module, data} => ...)
  • Vite: accept(['./some.js', './deps.js'], ([someModule, depsModule]) => ...)

So one thing that appears special in Vite is passing the dependencies of a module to the accept call of the parent. How important is that use-case for you? This breaks a few key assumptions that we've built on top of (mainly, that an accept call always accepts an update to the current module) that would be very difficult to unwind to implement on our side. I don't see this implemented in other HMR specs, so curious to hear more of the reasoning for this if it's something that you need.

@yyx990803
Copy link
Author

Re dependencies whitelist usage:

I'm not sure how your implementation goes, but if we are not replacing the accepting module, and not passing the new accepted modules via the callback, then you need to be updating the actual import bindings - which requires rewriting the imports of accepted deps to let and updating them before calling the callback?

e.g.

// main.js
import foo from './foo.js'

import.meta.hot.accept('./foo.js', () => {
  // if main.js is not replaced, then the `foo` binding would be stale
  // unless it's somehow rewritten into a let binding and updated behind the scene
})

I actually think accepting the dep whitelist via the callback is more explicit and more consistent with the self accepting usage.

@FredKSchott
Copy link
Owner

Totally, this may be a difference between our implementations. Right now, in your example:

// main.js
import foo from './foo.js'
import.meta.hot.accept(['./foo.js'], ({module}) => {
  // Not shown: updating main.js to accept this update 
})
  1. When foo.js changes, the event bubbles up to main.js.
  2. Both modules are marked as needing an update.
  3. The HMR engine imports a new version of main.js which will import a new version of foo.js as its dependency.
  4. The new instance of main.js is passed as the module instance.

@yyx990803
Copy link
Author

yyx990803 commented May 22, 2020

Yeah, so here's a use case we've had quite often for the whitelist usage (pseudo code based on Vuex usage, but applies to Redux as well for store.replaceReducer):

// store.js
import moduleA from './modules/a'
import moduleB from './modules/b'

export const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

import.meta.hot.accept(['./modules/a', './modules/b'], ([newA, newB]) => {
  store.replaceModules({
    a: newA,
    b: newB
  })
})

The key here is the store is exported and used by the application. Preserving the current store.js instance is important - otherwise, you would create a new store but the app will still be using the old one. And with your current implementation, it seems a bit awkward to force rerender the app with a new store provider from the hot callback of store.js.

@FredKSchott
Copy link
Owner

That makes a lot of sense, and I can't think of any other good way to handle that otherwise without re-exporting your imports to grab them in the callback, which feels gross.

How does this work with event bubbling? If modules/a is the file that's changed, is newA an updated instance but newB is still the same as it was originally?

@yyx990803
Copy link
Author

How does this work with event bubbling? If modules/a is the file that's changed, is newA an updated instance but newB is still the same as it was originally?

In Vite's current implementation, newB will be the unchanged, original module. There is some bookkeeping to make sure only the dirty modules are re-imported.

@FredKSchott
Copy link
Owner

Awesome, I'm +1 to add to the spec.

@FredKSchott
Copy link
Owner

@yyx990803 support for what you've described has been added. Based on what you've said, I think that's the last bit of Vite HMR that wasn't covered here, other than any Vue specific stuff which would be out of scope for this generalized proposal.

The last step would be on your end: to move to import.meta.hot + to make the syntactic changes to the Vite accept callback. I believe those are the only two things, based on convo above.

@yyx990803
Copy link
Author

yyx990803 commented May 25, 2020

So I noticed a few differences while looking at the latest spec, and I propose some slight syntax changes regarding the callback signature. Currently the spec requires accessing the reloaded module as a nested property on the object passed to the callback. This might make sense if the callback is accepting many different things, but in the context here there are really only two categories: the updated module(s), and the persisted data. Considering usage of data is rare, in most cases the callback really only need access to the updated modules. Having to destructure it from modules (and when using deps, under a different key deps) seems to bring no real benefits and result in a more verbose API.

Proposed Changes (updated based the following comment):

Self Accepting

// current spec
import.meta.hot.accept(({ module: { foo }, data }) => {
  // ...
})

// proposed change
const data = import.meta.hot.data
import.meta.hot.accept(({ foo }) => {
  // ...
})

Deps

// current spec
import.meta.hot.accept(['./depA', './depB'], ({ deps: [depA, depB], data }) => {
  // ...
})

// proposed change
const data = import.meta.hot.data
import.meta.hot.accept(['./depA', './depB'], ([depA, depB]) => {
  // ...
})

In addition, the spec doesn't seem to support single dep via string, which is supported in webpack:

// proposed addition, not currently covered in spec
import.meta.hot.accept('./depA', (newDepA) => {
  // ...
})

@yyx990803
Copy link
Author

yyx990803 commented May 25, 2020

Follow up: based on webpack's implementation and https://github.com/rixo/rollup-plugin-hot - both attaches persisted data on the hot object. Is there any particular reason to pass it via a nested property on the callback argument instead?

If we simply access the persisted data via import.meta.hot.data (which additionally allows access outside of callbacks) - it gets rid of the need to pass data as an argument so the callback can expect only the updated module.

@FredKSchott
Copy link
Owner

FredKSchott commented May 25, 2020

Okay, I'm on board with moving data out of the callback. Since the accept() callback always runs in the original module context, we can essentially just support import.meta.hot.data as a recommended user-land pattern without doing anything on our end to specifically support it. This should also address your concern that data passing is rare and not worth our time.

On the flip side, the more verbose API is intentional:

  1. module.hot.accept()
  2. module.hot.accept((selfModule) => {
  3. module.hot.accept('./depA', (depA) => {
  4. module.hot.accept(['./depA', './depB'], ([depA, depB]) => {

Supporting 4 different interfaces here is a flaw in Webpack's original design worth tackling. Going back through some Webpack history, this wasn't an intentional design, it's just how the API had to evolve to avoid breaking changes in the 7 years since it was originally implemented.

With 7 years of HMR hindsight and no legacy users to worry about, this is the right time to tackle this clean up. Given that this is an advanced API, explicit & concise are more valuable (and easier to document) than implicit & having-many-permutations.

@yyx990803
Copy link
Author

yyx990803 commented May 25, 2020

This may come down to personal preference but I don't really see a problem here - accept is an overloaded function by design (the type inference also works perfectly fine). If we really want to be explicit, then we should split it into multiple methods like hot.acceptSelf, hot.acceptDeps?

@yyx990803
Copy link
Author

we can essentially just support import.meta.hot.data as a recommended user-land pattern without doing anything on our end to specifically support it

I think the implementation still needs to ensure the same data object is persisted across reload of the same module.

@FredKSchott
Copy link
Owner

I think the implementation still needs to ensure the same data object is persisted across reload of the same module.

Good point, this is given for free by our implementation but should still be called out as an explicit requirement for a valid ESM-HMR implementation.

@yyx990803
Copy link
Author

yyx990803 commented May 25, 2020

What do you think about

import.meta.hot.data
import.meta.hot.accept()
import.meta.hot.accept((selfModule) => {})
import.meta.hot.acceptDeps(['./depA', './depB'], ([depA, depB]) => {})

@FredKSchott
Copy link
Owner

I feel like the current design hits the right balance between the max-verbose and the max-dynamic designs you've outlined:

  • arguments are optional to support multiple use-cases
    • (accept(), accept(cb), accept(deps, cb)).
  • BUT the callback function signature never changes on you.

@yyx990803
Copy link
Author

yyx990803 commented May 25, 2020

Even though the callback signature is the same, the values in the object being passed in changes based on the 1st argument. The deps key is only present when you pass an array of deps - which, IMO, isn't really that different from passing in different args to the callback.

Having two separate methods makes the intention more explicit (it's easier to look at what method is being called than looking at the arguments), and also makes the usage less verbose.

@FredKSchott
Copy link
Owner

In the documented API, accept(cb) and accept([], cb) behave identically

@yyx990803
Copy link
Author

yyx990803 commented May 25, 2020

accept(({ module, deps }) => {
  // deps is empty
  // module is the relevant value
})

accept(['./depA'], ({ module, deps }) => {
  // deps now non-empty and is the relevant value
  // and do we really need module here if self module is not replaced in this case?
})

Compare to:

accept((selfModule) => {
  // selfModule is the relevant value, because we are self-accepting
  // self module is re-instantiated
})

acceptDeps(['./depA'], (deps) => {
  // deps is the relevant value because we are accepting deps
  // self module is NOT re-instantiated
})

I guess personally for me it feels weird for an API method to be overloaded with different signatures and then you are expected to retrieve the relevant value from different properties under the object passed to a callback. I don't think I've come across many APIs designed like that before. If we want to be explicit, I would much prefer two API methods for self-accepting vs. accepting deps, since they are fundamentally doing different things (especially when there are important behavior differences in terms of self module re-instantiation).

@FredKSchott
Copy link
Owner

Does this help?

accept(['./depA'], ({ module, deps }) => {
  // deps & module could both be relevant to your use-case.
  // self module is always updated.
})

accept() is intentionally always self-accepting in this API. I don't believe I've seen a use-case where you need to accept deps but cannot also accept the current module (if you don't need it, you can ignore it). But would love to see one if you have one.

On the flip side we simplify documentation, interface, and implementation with this less complex implementation.

@yyx990803
Copy link
Author

yyx990803 commented May 25, 2020

Based on what we've discussed here and the example here, when you are accepting deps, the self module should not be re-instantiated. This is necessary to preserve the identity of the exported store instance. Otherwise the store replacement example will not work, because you are calling replaceModules on a newly created store instance which the app has no knowledge of.

@FredKSchott
Copy link
Owner

FredKSchott commented May 25, 2020

You're right, that's a bug in the example. It should instead be:

Whoops, I read too quickly. That example is actually fine as is: you would only want to update the current module's store and could ignore the newly created module instance's store.

Or, you could also:

import moduleA from "./modules/a.js";
import moduleB from "./modules/b.js";

export let store = import.meta.hot.data.store || createStore({
  modules: { a: moduleA, b: moduleB },
});

import.meta.hot.accept(
  ["./modules/a.js", "./modules/b.js"],
  ({ module, deps }) => {
    store.replaceModules({
      a: deps[0].default,
      b: deps[1].default,
    });
  }
);
import.meta.hot.dispose(() => {
    import.meta.hot.data.store = store;
});

If what you're looking for is something that listens to update events in your dependencies, I think there could be a place for that. Something like import.meta.hot.listen(deps, callback) maybe? But accepting events on your dependencies behalf introduces its own set of issues that I've tried to avoid here:

  • How does data passing work with the dispose handler?
  • Other dependents now have an outdated reference?

To be clear, these are all problems that Webpack/Rollup/System.js don't have since they can hot-swap a module out with an update. But with ESM we need to self-accept updates to make sure that we never have more than one copy of the module running in the application, since a dependent can't update dependency on its behalf.

@yyx990803
Copy link
Author

yyx990803 commented May 26, 2020

That example is actually fine as is: you would only want to update the current module's store and could ignore the newly created module instance's store.

On your next edit, the store in the callback will be a stale one that is never used, so future updates will have no effect.

The example using let binding should fix that, but is far less ergonomic than it could be.

How does data passing work with the dispose handler?

You don't - data passing is only relevant for self accepting modules that perform side effects on update. In this case, the deps modules (or reducers) should be side-effect free (and therefore is safe to be hot replaced)

Other dependents now have an outdated reference?

In Vite's implementation, we walk the import chains of any changed module - and only perform hot updates if all import chains reach a hot boundary. If there are any importer of the changed module (even up the chain) that is not a boundary (i.e. has no explicit logic to handle the update), the app will be reloaded in full. This guarantees there will never be stale references.

If you do not reload the page in full, then re-instantiating the self module with its deps doesn't really get rid of the stale reference problem. For example, if there is another file in your app that directly imports ./modules/a, it will get a stale reference regardless of whether store.js is re-instantiated.

@FredKSchott
Copy link
Owner

FredKSchott commented May 26, 2020

On your next edit, the store in the callback will be a stale one that is never used, so future updates will have no effect.

Maybe this is a misunderstanding of the spec: the accept handler is the only way to apply updates from the new instance to the existing instance that's connected into your application:

// store.js
import moduleA from "./modules/a.js";
import moduleB from "./modules/b.js";

export const store = createStore({
  modules: { a: moduleA, b: moduleB },
});

import.meta.hot.accept(
  ["./modules/a.js", "./modules/b.js"],
  ({ module, deps }) => {
    store.replaceModules({
      a: deps[0].default,
      b: deps[1].default,
    });
  }
);
  1. store.js is updated, and the accept() callback is called with a new instance of store.js with a new store export.
  2. the accept handler in the original module is called, with a new, updated instance of store.js.
  3. The accept handler does nothing with this new instance, and returns.
  4. The original store export remains the same, and continues to be used by the application.

In Vite's implementation, we walk the import chains of any changed module - and only perform hot updates if all import chains reach a hot boundary.

Just to make sure I understand you 100%, in Vite does a change inside of modules/a.js in the example below trigger a full page reload, or is the change to modules/a.js considered accepted & fully handled by store.js?

/* store.js */
import moduleA from "./modules/a.js";
import moduleB from "./modules/b.js";
acceptDeps(["./modules/a.js", "./modules/b.js"], () => { ... });

/* foo.js */
import moduleA from "./modules/a.js";
import moduleB from "./modules/b.js";
// No accept handler

@yyx990803
Copy link
Author

I guess the misunderstanding probably roots from the difference in our implementations... wouldn't this new instance of store.js register a new accept callback that replaces the previous one? It sounds to me you are always using the first accept callback that was registered? What if the user edits the accept callback itself?


Just to make sure I understand you 100%, in Vite does a change inside of modules/a.js in the example below trigger a full page reload, or is the change to modules/a.js considered accepted & fully handled by store.js?

Yes, it would be a full reload because foo.js has no defined behavior for when a.js and b.js are updated.

@yyx990803
Copy link
Author

Given there seem to be many undefined behavior differences between our implementations, I'm not sure if it's worth it to bend them to conform to the exact same spec. Even if we achieve syntactical consistency, there are probably more fundamental differences in terms of actual behavior.

@FredKSchott
Copy link
Owner

Sorry, got bogged down with 2.0 release work. Yea, I guess that's fair, maybe 1:1 compatibility shouldn't be the goal here if you already have an implementation that you're happy with. I definitely appreciate where we've come together on this already though.

maybe stepping back from the interface/implementation itself: I'm genuinely curious how you got self-accepting modules to work in a way that also replaces the current instance of the module in the application. The "every module must be self-accepting" design of Snowpack's HMR is based off of this requirement: since we don't own the module system, we need to always be updating the first instance of the module itself since that's the one that will always be referenced inside the rest of the application.

If you have the time, I'd love to hear how that works in Vite (or, if that's documented somewhere, a reference to the docs would be great too).

@sokra
Copy link

sokra commented Jun 22, 2020

Heyho,

I see the need for a different HMR API for non-bundlers, as the webpack HMR API won't work for native ESM. I totally understand why you are creating a new spec for that. But could you do one thing: bind to a different name for it: e. g. import.meta.hmr. No method has the same semantics compared to the webpack HMR API, there is no reason to use the same method names. Instead it even hurts as one can't implement both API and stuff will conflict.

If you want to know the differences:

method webpack spec this spec
accept swaps module in cache won't swap
accept(handler) handler is an error handler handler gives you the new module
accept(deps, handler) import bindings are replaced, handler gives array of deps import bindings are not replaced, handler gives object with deps and module
dispose callback has data as argument callback has object as argument with data property
decline identical identical
invalidate causes an hmr update for the current module, used to prevent a full page reload forces a full page reload
import.meta.hot.data readonly writable

@yyx990803
Copy link
Author

@sokra is webpack using import.meta.hot as well? What's the reasoning behind that if you already support it via module.hot?

FWIW Vite is not 100% conforming to this spec (AFAIK snowpack is the only implementation of this), but it seems more reasonable for import.meta.hot to be used by native ESM based HMR, while webpack stay on module.hot?

@FredKSchott
Copy link
Owner

one can't implement both API and stuff will conflict.

Is this a useful goal? Parcel, Webpack, and Rollup (plugins) all include different module.hot implementations with varying levels of support, and that hasn't seemed to be an issue for them (See the Preact team here, building different plugins to implement for each: https://github.com/JoviDeCroock/prefresh)

Thanks for taking a look btw!

@jkrems
Copy link

jkrems commented Jul 22, 2020

Looks like webpack landed its API as import.meta.webpackHot: https://github.com/webpack/webpack/pull/11075/files

@43081j
Copy link

43081j commented Nov 3, 2020

we tried to follow the client interface of this repo in my implementation of hmr over at the modernweb repo (here).

i was just about to create an issue about the problems/disagreements we had but it looks like @yyx990803 already came to same conclusions!

basically im not a fan of these:

  • dispose(callback) - it really sounds like it'll do something immediately, why not onDispose or disposeCallback or something
  • accept([dep1, dep2], callback) - wish this was a separate method instead of cramming it all into one (i.e. acceptDeps)
  • ({ deps, module }) => { ... } - don't see any use of having it as an object, would much rather it be well known parameters

as you can see, Evan already suggested the last two and thats how he has implemented it in vite in fact. which means its already diverged from whats in this repo.

about the wording... dispose and invalidate both sound like they do something immediate, something synchronous. but in fact one is immediate and one is async (calls a callback later on).

@FredKSchott
Copy link
Owner

FredKSchott commented Nov 5, 2020

If splitting out accept and acceptDeps bring all 3 of our implementations into one big, happy "generalized" spec... then I'm willing to swallow my personal preference. I'm a bit out of this head space, but happy to revisit this next week in Snowpack.

({ deps, module }) vs. (deps) & (module) is less about personal preference and more about forwards-compatability (and flexibility, if some environments want to build special behavior on top of a common spec). For example, Snowpack added a bubbled: boolean property to the accepted event. I think that this has real, objective benefits over the alternative.

@rixo has been a huge HMR advocate in the Svelte ecosystem, I know they've integrated with esm-hmr and curious if they have any thoughts here re: these remaining differences.

@43081j
Copy link

43081j commented Nov 7, 2020

it seems like @yyx990803 went with:

accept((mod) => { ... });
acceptDeps([a, b], ([a, b]) => { ... });

which means you can't do both in one, as in you can't have "self" and some dependencies. im fine with that personally but is worth noting.

as for your bubbled example, it sounds like you're treating it like an event when it isn't. if we were emitting events, of course i'd agree the module should be detail of the event. really we're in a callback architecture, not an event driven one.

so maybe a middle ground is:

accept((dep, meta) => { ... });
acceptDeps(['./foo', './bar'], ([foo, bar], meta) => { ... });

or some such thing, since it seems like an edge case anyway for wanting this extra object.

@rixo
Copy link

rixo commented Nov 7, 2020

To me it's the "accept deps" use case that seems edgy, and the bubbled arg that seems really useful and just in the right place where you need it in the accept handler. I guess this depends on the underlying framework / technology you're implementing HMR adapters for. My personal philosophy would be to enable every use cases in the most straightforward fashion for the consumer when easy / possible for the HMR provider.

Apart from that, I kind of agree with @43081j that the name of the methods of the API could have more explicit names. Let's be honest, this naming is the legacy of Webpack that had its own approach at the time. The naming has been continued mimetically by other tools (Parcel, Nollup, Vite, my own confidential Rollup plugin...), each with their own adaptation on the approach...

Now I'm personally hesitant to go back on this since it will break all existing Framework adapters (Svelte, React...), but I really don't have strong feelings about this. I'm going to follow suit, both in my adapter and my HMR provider, with anything that gathers a consensus in this regard.

I want to insist heavily on the point that having even a "somewhat" standard API really pays off. For Svelte, I have basically the same code supporting Snowpack, Vite, and Rollup with just a tiny amount of care need to cater for the specific needs of each (Vite needing the accept call written in the module for static analysis, and Snowpack not passing the data object to the dispose handlers). In contrast, I need special adapter layers for Webpack and Nollup, and this undoubtedly adds maintenance burden and delays in adding support for this kind of targets.

And so I think preserving, or even increasing, the consensus really is the important goal here. I'm willing to follow any direction regarding naming etc. in support of this goal, if some actor feels strongly enough that they'd would otherwise go their own way. I'd be pretty disappointed if the bubbled information, in one form of another, is dropped from the spec though. This really enables superior HMR behavior in the case of Svelte for example.

@rixo
Copy link

rixo commented Nov 7, 2020

Also worth noting, in addition to breaking all existing big Framework adapters (which is probably a pretty limited and closed-scope issue, because everyone concerned is closely following the game), changing namings would also impact every single user / lib that have made the effort of implementing HMR adapters in their own code. This seems like another level of breaking change, unfortunately.

@43081j
Copy link

43081j commented Nov 7, 2020

it would be interesting to hear from the rest of you what usage of the HMR API is like as opposed to automatic solutions. presumably vite "just works" and nobody has to deal with the hmr api directly, for example.

our implementation is similar as we don't expect many to use the api directly but rather use plugins which interact with it.

@rixo
Copy link

rixo commented Nov 7, 2020

True that there is probably not a lot of end users that put their own hands into HMR. I personally do it because it can be useful to increase HMR comfort in a project, but that's probably because I have prior knowledge for it.

However I suspect there exists a whole lot of users that have inherited some HMR code that they don't understand (i.e. can't fix) from a starter template, that is something that you typically never upgrade for a given project.

(Still not a blocker to follow consensus, as far as I'm concerned.)

@43081j
Copy link

43081j commented Nov 15, 2020

FYI in modernweb repo we now adopted the same signatures as vite:

accept((module) => { ... });
acceptDeps([dep1, dep2], ([dep1, dep2]) => { ... });

@glaucomorais
Copy link

import.meta.hot.data readonly is counterproductive.

I was trying to use it, but it was always empty and when I tried to store data to persist during the module change, an exception occurred saying that it is just a getter.

And in the guide it says that I could use it to pass data between module versions...

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

No branches or pull requests

7 participants