Skip to content

Commit afe1d11

Browse files
trueadmRich-Harris
andauthored
feat: hot module reloading support for Svelte 5 (#11106)
* feat: hot module reloading support for Svelte 5 * fix lockfile * tweaks * types * lint * lint * tweaks * add hmr flag * tweak * tweaks * move HMR logic into its own module * simplify * tidy up types * fix test * lint * need some indirection here or references break * prevent transitions during HMR update --------- Co-authored-by: Rich Harris <[email protected]>
1 parent e1b2d29 commit afe1d11

File tree

23 files changed

+147
-64
lines changed

23 files changed

+147
-64
lines changed

.changeset/olive-moons-act.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: hot module reloading support for Svelte 5

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -415,15 +415,31 @@ export function client_component(source, analysis, options) {
415415
const body = [
416416
...state.hoisted,
417417
...module.body,
418-
b.export_default(
419-
b.function_declaration(
420-
b.id(analysis.name),
421-
[b.id('$$anchor'), b.id('$$props')],
422-
component_block
423-
)
418+
b.function_declaration(
419+
b.id(analysis.name),
420+
[b.id('$$anchor'), b.id('$$props')],
421+
component_block
424422
)
425423
];
426424

425+
if (options.hmr) {
426+
body.push(
427+
b.export_default(
428+
b.conditional(
429+
b.import_meta_hot(),
430+
b.call('$.hmr', b.member(b.import_meta_hot(), b.id('data')), b.id(analysis.name)),
431+
b.id(analysis.name)
432+
)
433+
),
434+
b.if(
435+
b.import_meta_hot(),
436+
b.stmt(b.call('import.meta.hot.acceptExports', b.literal('default')))
437+
)
438+
);
439+
} else {
440+
body.push(b.export_default(b.id(analysis.name)));
441+
}
442+
427443
if (options.dev) {
428444
if (options.filename) {
429445
let filename = options.filename;

packages/svelte/src/compiler/utils/builders.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,20 @@ export function throw_error(str) {
600600
};
601601
}
602602

603+
/**
604+
* @return {import('estree').MemberExpression}
605+
*/
606+
export function import_meta_hot() {
607+
return member(
608+
{
609+
type: 'MetaProperty',
610+
meta: id('import'),
611+
property: id('meta')
612+
},
613+
id('hot')
614+
);
615+
}
616+
603617
export {
604618
await_builder as await,
605619
let_builder as let,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { block, branch, destroy_effect } from '../reactivity/effects.js';
2+
import { set, source } from '../reactivity/sources.js';
3+
import { set_should_intro } from '../render.js';
4+
import { get } from '../runtime.js';
5+
6+
/**
7+
* @template {(anchor: Comment, props: any) => any} Component
8+
* @param {{ source: import("#client").Source<Component>; wrapper: Component; }} data
9+
* @param {Component} component
10+
*/
11+
export function hmr(data, component) {
12+
if (data.source) {
13+
set(data.source, component);
14+
} else {
15+
data.source = source(component);
16+
}
17+
18+
return (data.wrapper ??= /** @type {Component} */ (
19+
(anchor, props) => {
20+
let instance = {};
21+
22+
/** @type {import("#client").Effect} */
23+
let effect;
24+
25+
block(() => {
26+
const component = get(data.source);
27+
28+
if (effect) {
29+
// @ts-ignore
30+
for (var k in instance) delete instance[k];
31+
destroy_effect(effect);
32+
}
33+
34+
effect = branch(() => {
35+
set_should_intro(false);
36+
Object.assign(instance, component(anchor, props));
37+
set_should_intro(true);
38+
});
39+
});
40+
41+
return instance;
42+
}
43+
));
44+
}

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { hmr } from './dev/hmr.js';
12
export { add_owner, mark_module_start, mark_module_end } from './dev/ownership.js';
23
export { await_block as await } from './dom/blocks/await.js';
34
export { if_block as if } from './dom/blocks/if.js';

packages/svelte/src/internal/client/render.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
init_operations
88
} from './dom/operations.js';
99
import { HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js';
10-
import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js';
10+
import { flush_sync, push, pop, current_component_context } from './runtime.js';
1111
import { effect_root, branch } from './reactivity/effects.js';
1212
import {
1313
hydrate_anchor,

packages/svelte/tests/parser-legacy/samples/css/output.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@
7878
"content": {
7979
"start": 23,
8080
"end": 48,
81-
"comment": null,
82-
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
81+
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n",
82+
"comment": null
8383
}
8484
}
8585
}

packages/svelte/tests/parser-legacy/samples/whitespace-after-style-tag/output.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@
7878
"content": {
7979
"start": 23,
8080
"end": 48,
81-
"comment": null,
82-
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
81+
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n",
82+
"comment": null
8383
}
8484
}
8585
}

packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,8 +1074,8 @@
10741074
"content": {
10751075
"start": 7,
10761076
"end": 798,
1077-
"comment": null,
1078-
"styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n\t\th1:nth-of-type(10n+1){\n background: red;\n }\n\t\th1:nth-of-type(-2n+3){\n background: red;\n }\n\t\th1:nth-of-type(+12){\n background: red;\n }\n\t\th1:nth-of-type(+3n){\n background: red;\n }\n"
1077+
"styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n\t\th1:nth-of-type(10n+1){\n background: red;\n }\n\t\th1:nth-of-type(-2n+3){\n background: red;\n }\n\t\th1:nth-of-type(+12){\n background: red;\n }\n\t\th1:nth-of-type(+3n){\n background: red;\n }\n",
1078+
"comment": null
10791079
}
10801080
},
10811081
"js": [],

packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,8 @@
393393
"content": {
394394
"start": 7,
395395
"end": 378,
396-
"comment": null,
397-
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n"
396+
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n",
397+
"comment": null
398398
}
399399
},
400400
"js": [],

packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@
7171
"content": {
7272
"start": 43,
7373
"end": 197,
74-
"comment": null,
75-
"styles": "\n\t@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\");\n\th1 {\n\t\tfont-weight: bold;\n\t\tbackground: url(\"whatever\");\n\t}\n"
74+
"styles": "\n\t@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\");\n\th1 {\n\t\tfont-weight: bold;\n\t\tbackground: url(\"whatever\");\n\t}\n",
75+
"comment": null
7676
}
7777
},
7878
"js": [],

packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import TextInput from './Child.svelte';
77
var root_1 = $.template(`Something`, 1);
88
var root = $.template(`<!> `, 1);
99

10-
export default function Bind_component_snippet($$anchor, $$props) {
10+
function Bind_component_snippet($$anchor, $$props) {
1111
$.push($$props, true);
1212

1313
let value = $.source('');
@@ -36,4 +36,6 @@ export default function Bind_component_snippet($$anchor, $$props) {
3636
$.render_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
3737
$.append($$anchor, fragment_1);
3838
$.pop();
39-
}
39+
}
40+
41+
export default Bind_component_snippet;

packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import "svelte/internal/disclose-version";
44
import * as $ from "svelte/internal/client";
55

6-
export default function Bind_this($$anchor, $$props) {
6+
function Bind_this($$anchor, $$props) {
77
$.push($$props, false);
88
$.init();
99

@@ -13,4 +13,6 @@ export default function Bind_this($$anchor, $$props) {
1313
$.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo);
1414
$.append($$anchor, fragment);
1515
$.pop();
16-
}
16+
}
17+
18+
export default Bind_this;

packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import "svelte/internal/disclose-version";
44
import * as $ from "svelte/internal/client";
55

6-
export default function Class_state_field_constructor_assignment($$anchor, $$props) {
6+
function Class_state_field_constructor_assignment($$anchor, $$props) {
77
$.push($$props, true);
88

99
class Foo {
@@ -26,4 +26,6 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
2626
}
2727

2828
$.pop();
29-
}
29+
}
30+
31+
export default Class_state_field_constructor_assignment;

packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as $ from "svelte/internal/client";
55

66
var root = $.template(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, 3);
77

8-
export default function Main($$anchor, $$props) {
8+
function Main($$anchor, $$props) {
99
$.push($$props, true);
1010

1111
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
@@ -35,4 +35,6 @@ export default function Main($$anchor, $$props) {
3535

3636
$.append($$anchor, fragment);
3737
$.pop();
38-
}
38+
}
39+
40+
export default Main;

packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import "svelte/internal/disclose-version";
44
import * as $ from "svelte/internal/client";
55

6-
export default function Each_string_template($$anchor, $$props) {
6+
function Each_string_template($$anchor, $$props) {
77
$.push($$props, false);
88
$.init();
99

@@ -19,4 +19,6 @@ export default function Each_string_template($$anchor, $$props) {
1919

2020
$.append($$anchor, fragment);
2121
$.pop();
22-
}
22+
}
23+
24+
export default Each_string_template;

packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import "svelte/internal/disclose-version";
44
import * as $ from "svelte/internal/client";
55

6-
export default function Function_prop_no_getter($$anchor, $$props) {
6+
function Function_prop_no_getter($$anchor, $$props) {
77
$.push($$props, true);
88

99
let count = $.source(0);
@@ -30,4 +30,6 @@ export default function Function_prop_no_getter($$anchor, $$props) {
3030

3131
$.append($$anchor, fragment);
3232
$.pop();
33-
}
33+
}
34+
35+
export default Function_prop_no_getter;

packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import * as $ from "svelte/internal/client";
55

66
var root = $.template(`<h1>hello world</h1>`);
77

8-
export default function Hello_world($$anchor, $$props) {
8+
function Hello_world($$anchor, $$props) {
99
$.push($$props, false);
1010
$.init();
1111

1212
var h1 = root();
1313

1414
$.append($$anchor, h1);
1515
$.pop();
16-
}
16+
}
17+
18+
export default Hello_world;

packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function reset(_, str, tpl) {
1212

1313
var root = $.template(`<input> <input> <button>reset</button>`, 1);
1414

15-
export default function State_proxy_literal($$anchor, $$props) {
15+
function State_proxy_literal($$anchor, $$props) {
1616
$.push($$props, true);
1717

1818
let str = $.source('');
@@ -35,4 +35,6 @@ export default function State_proxy_literal($$anchor, $$props) {
3535
$.pop();
3636
}
3737

38+
export default State_proxy_literal;
39+
3840
$.delegate(["click"]);

packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import "svelte/internal/disclose-version";
44
import * as $ from "svelte/internal/client";
55

6-
export default function Svelte_element($$anchor, $$props) {
6+
function Svelte_element($$anchor, $$props) {
77
$.push($$props, true);
88

99
let tag = $.prop($$props, "tag", 3, 'hr');
@@ -13,4 +13,6 @@ export default function Svelte_element($$anchor, $$props) {
1313
$.element(node, tag, false);
1414
$.append($$anchor, fragment);
1515
$.pop();
16-
}
16+
}
17+
18+
export default Svelte_element;

playgrounds/demo/server.js

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import fs from 'node:fs';
22
import path from 'node:path';
33
import { fileURLToPath } from 'node:url';
44
import express from 'express';
5-
import { createServer as createViteServer, build } from 'vite';
5+
import { createServer as createViteServer } from 'vite';
66

7-
const PORT = process.env.PORT || '3000';
7+
const PORT = process.env.PORT || '5173';
88

99
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1010

@@ -26,30 +26,13 @@ async function createServer() {
2626
return;
2727
}
2828

29-
// Uncomment the line below to enable optimizer.
30-
// process.env.SVELTE_ENV = 'hydrate';
29+
const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
30+
const transformed_template = await vite.transformIndexHtml(req.originalUrl, template);
31+
const { html: appHtml, head: headHtml } = await vite.ssrLoadModule('./src/entry-server.ts');
3132

32-
await build({
33-
root: path.resolve(__dirname, './'),
34-
build: {
35-
minify: false,
36-
rollupOptions: {
37-
output: {
38-
manualChunks(id) {
39-
if (id.includes('svelte/src')) {
40-
return 'vendor';
41-
}
42-
}
43-
}
44-
}
45-
}
46-
});
47-
48-
const template = fs.readFileSync(path.resolve(__dirname, 'dist', 'index.html'), 'utf-8');
49-
50-
const { html: appHtml, head: headHtml } = await vite.ssrLoadModule('/src/entry-server.ts');
51-
52-
const html = template.replace(`<!--ssr-html-->`, appHtml).replace(`<!--ssr-head-->`, headHtml);
33+
const html = transformed_template
34+
.replace(`<!--ssr-html-->`, appHtml)
35+
.replace(`<!--ssr-head-->`, headHtml);
5336

5437
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
5538
});

0 commit comments

Comments
 (0)