vite-plugin-svelte: HMR breaks `context="module"` exports

Describe the bug

If you’ve exported a function using <script context="module">, HMR updates from that component causes other the exported function to no longer run from other components where it’s imported.

Reproduction

https://github.com/probablykasper/svelte-vite-context-module-exports-bug

  1. npx degit probablykasper/svelte-vite-context-module-exports-bug svelte-vite-context-module-exports-bug
  2. cd svelte-vite-context-module-exports-bug && npm i
  3. npm run dev
  4. Open the website and check that the Show and Hide buttons work
  5. Trigger HMR in Counter.svelte, for example by changing the <h3> tag
  6. Now the Show and Hide buttons are broken

Logs

No response

System Info

System:
    OS: macOS 10.15.7
    CPU: (8) x64 Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
    Memory: 1.21 GB / 32.00 GB
    Shell: 3.2.2 - /usr/local/bin/fish
  Binaries:
    Node: 14.16.0 - /var/folders/44/3sxbyc2d6wg90mj1s7s59bnm0000gn/T/fnm_multishells/23267_1628209481847/bin/node
    Yarn: 1.22.10 - /var/folders/44/3sxbyc2d6wg90mj1s7s59bnm0000gn/T/fnm_multishells/23267_1628209481847/bin/yarn
    npm: 6.14.11 - /var/folders/44/3sxbyc2d6wg90mj1s7s59bnm0000gn/T/fnm_multishells/23267_1628209481847/bin/npm
  Browsers:
    Brave Browser: 92.1.27.108
    Chrome: 92.0.4515.131
    Firefox: 89.0.2
    Safari: 14.1.2
  npmPackages:
    @sveltejs/vite-plugin-svelte: 1.0.0-next.15 => 1.0.0-next.15 
    svelte: ^3.42.1 => 3.42.1 
    vite: ^2.4.4 => 2.4.4

Severity

annoyance

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 16 (6 by maintainers)

Commits related to this issue

Most upvoted comments

the fix for this (partial HMR) might have uncovered this. Can’t tell if this is a bug in v-p-s / svelte-hmr or a problem in your project.

Please file a new issue and provide a minimal reproduction. Ideally without routify in a clean vite+svelte project.

should be fixed in 1.0.6

@Sayyiditow Hi! For now, the best workaround you can have is probably to split <script context=module> parts into their own .js file. I understand how this might not be practical in some situations, though.

The fix is in progress. Vite 3 has added the experimental option we need to address this on our side. I’m reserving some time to add it to svelte-hmr in the coming days, and hopefully it can make its way into the Vite plugin soon after.

FYI the fix effect will be that any change to the context=module part will cause invalidation of the whole .svelte SFC (single file component), meaning HMR will recreate all consumers (importers) of the SFC. If any of those consumers is not a Svelte component (or otherwise “accepted” HMR module), this will cause a full reload. Changes to the rest of the SFC (that is the Svelte component proper) will have HMR as usual. Edit I was out of my mind when writing this 😅 . What I described was the solution we did not implement because too brutal. The actual fix that has just been done is that only consumers of named exports (from context=module) get invalidated when the source module changes. Consumers of only the default export (that is, the Svelte component) do not get updated. For HMR this is the equivalent to have the part in context=module in a separate .js file – and this is the best than can be handled from within Svelte.

broken code from repo above

App.svelte

<script lang="ts">
  import Counter, { visible } from './Counter.svelte'
</script>

<main>
  <Counter />
  <button on:click={() => visible.set(true)}>Show</button>
  <button on:click={() => visible.set(false)}>Hide</button>
</main>

Counter.svelte

<script lang="ts" context="module">
  import { writable } from 'svelte/store'

  let text = writable('')

  export const visible = writable(false)
</script>

<h3>Update me</h3>
<p>Visible: {$visible}</p>

what happens

When you edit Counter.svelte this results in a recompile and the js code is sent to the browser. svelte-hmr then “reapplies” that code and replaces all instances of Counter with new ones. The new Counters get a reference to a new instance of the visible store, as can be witnessed when clicking once so that the browser shows “visible true”, and then editing Counter.svelte. It updates to “visible false”, because it shows the initial value of the new instance.

The instance of App.svelte on the other hand remains unchanged and that imported the original instance of the visible store. So now you have 2 instances of the same store and of course updates to the one App.svelte has won’t propagate to the new Counter.

how to avoid it

Move the store to it’s own module to avoid recreating it on update

store.ts

import { writable } from 'svelte/store'
export const visible = writable(false)

App.svelte

<script lang="ts">
  import Counter from './Counter.svelte'
  import {visible} from "./store";
</script>

<main>
  <Counter />
  <button on:click={() => visible.set(true)}>Show</button>
  <button on:click={() => visible.set(false)}>Hide</button>
</main>

Counter.svelte

<script lang="ts">
    import {visible} from "./store";
</script>

<h3>Update bla</h3>
<p>Visible: {$visible}</p>

Now the hmr update of Counter no longer creates a new instance of the visible store and you can enjoy hmr.

cc @rixo

This is a limitation of the current implementation and trying to also invalidate all Components that import from an hmr updated Components context=module could cause a big cascade effect.