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 <rich.harris@vercel.com>
pull/11111/head
Dominic Gannaway 7 months ago committed by GitHub
parent e1b2d29eda
commit afe1d11a5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: hot module reloading support for Svelte 5

@ -415,15 +415,31 @@ export function client_component(source, analysis, options) {
const body = [
...state.hoisted,
...module.body,
b.export_default(
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
];
if (options.hmr) {
body.push(
b.export_default(
b.conditional(
b.import_meta_hot(),
b.call('$.hmr', b.member(b.import_meta_hot(), b.id('data')), b.id(analysis.name)),
b.id(analysis.name)
)
),
b.if(
b.import_meta_hot(),
b.stmt(b.call('import.meta.hot.acceptExports', b.literal('default')))
)
);
} else {
body.push(b.export_default(b.id(analysis.name)));
}
if (options.dev) {
if (options.filename) {
let filename = options.filename;

@ -600,6 +600,20 @@ export function throw_error(str) {
};
}
/**
* @return {import('estree').MemberExpression}
*/
export function import_meta_hot() {
return member(
{
type: 'MetaProperty',
meta: id('import'),
property: id('meta')
},
id('hot')
);
}
export {
await_builder as await,
let_builder as let,

@ -0,0 +1,44 @@
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set, source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
* @param {{ source: import("#client").Source<Component>; wrapper: Component; }} data
* @param {Component} component
*/
export function hmr(data, component) {
if (data.source) {
set(data.source, component);
} else {
data.source = source(component);
}
return (data.wrapper ??= /** @type {Component} */ (
(anchor, props) => {
let instance = {};
/** @type {import("#client").Effect} */
let effect;
block(() => {
const component = get(data.source);
if (effect) {
// @ts-ignore
for (var k in instance) delete instance[k];
destroy_effect(effect);
}
effect = branch(() => {
set_should_intro(false);
Object.assign(instance, component(anchor, props));
set_should_intro(true);
});
});
return instance;
}
));
}

@ -1,3 +1,4 @@
export { hmr } from './dev/hmr.js';
export { add_owner, mark_module_start, mark_module_end } from './dev/ownership.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';

@ -7,7 +7,7 @@ import {
init_operations
} from './dom/operations.js';
import { HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js';
import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
import {
hydrate_anchor,

@ -78,8 +78,8 @@
"content": {
"start": 23,
"end": 48,
"comment": null,
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n",
"comment": null
}
}
}

@ -78,8 +78,8 @@
"content": {
"start": 23,
"end": 48,
"comment": null,
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n",
"comment": null
}
}
}

@ -1074,8 +1074,8 @@
"content": {
"start": 7,
"end": 798,
"comment": null,
"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"
"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",
"comment": null
}
},
"js": [],

@ -393,8 +393,8 @@
"content": {
"start": 7,
"end": 378,
"comment": null,
"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"
"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",
"comment": null
}
},
"js": [],

@ -71,8 +71,8 @@
"content": {
"start": 43,
"end": 197,
"comment": null,
"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"
"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",
"comment": null
}
},
"js": [],

@ -7,7 +7,7 @@ import TextInput from './Child.svelte';
var root_1 = $.template(`Something`, 1);
var root = $.template(`<!> `, 1);
export default function Bind_component_snippet($$anchor, $$props) {
function Bind_component_snippet($$anchor, $$props) {
$.push($$props, true);
let value = $.source('');
@ -36,4 +36,6 @@ export default function Bind_component_snippet($$anchor, $$props) {
$.render_effect(() => $.set_text(text, ` value: ${$.stringify($.get(value))}`));
$.append($$anchor, fragment_1);
$.pop();
}
}
export default Bind_component_snippet;

@ -3,7 +3,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Bind_this($$anchor, $$props) {
function Bind_this($$anchor, $$props) {
$.push($$props, false);
$.init();
@ -13,4 +13,6 @@ export default function Bind_this($$anchor, $$props) {
$.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo);
$.append($$anchor, fragment);
$.pop();
}
}
export default Bind_this;

@ -3,7 +3,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Class_state_field_constructor_assignment($$anchor, $$props) {
function Class_state_field_constructor_assignment($$anchor, $$props) {
$.push($$props, true);
class Foo {
@ -26,4 +26,6 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
}
$.pop();
}
}
export default Class_state_field_constructor_assignment;

@ -5,7 +5,7 @@ import * as $ from "svelte/internal/client";
var root = $.template(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, 3);
export default function Main($$anchor, $$props) {
function Main($$anchor, $$props) {
$.push($$props, true);
// needs to be a snapshot test because jsdom does auto-correct the attribute casing
@ -35,4 +35,6 @@ export default function Main($$anchor, $$props) {
$.append($$anchor, fragment);
$.pop();
}
}
export default Main;

@ -3,7 +3,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Each_string_template($$anchor, $$props) {
function Each_string_template($$anchor, $$props) {
$.push($$props, false);
$.init();
@ -19,4 +19,6 @@ export default function Each_string_template($$anchor, $$props) {
$.append($$anchor, fragment);
$.pop();
}
}
export default Each_string_template;

@ -3,7 +3,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Function_prop_no_getter($$anchor, $$props) {
function Function_prop_no_getter($$anchor, $$props) {
$.push($$props, true);
let count = $.source(0);
@ -30,4 +30,6 @@ export default function Function_prop_no_getter($$anchor, $$props) {
$.append($$anchor, fragment);
$.pop();
}
}
export default Function_prop_no_getter;

@ -5,7 +5,7 @@ import * as $ from "svelte/internal/client";
var root = $.template(`<h1>hello world</h1>`);
export default function Hello_world($$anchor, $$props) {
function Hello_world($$anchor, $$props) {
$.push($$props, false);
$.init();
@ -13,4 +13,6 @@ export default function Hello_world($$anchor, $$props) {
$.append($$anchor, h1);
$.pop();
}
}
export default Hello_world;

@ -12,7 +12,7 @@ function reset(_, str, tpl) {
var root = $.template(`<input> <input> <button>reset</button>`, 1);
export default function State_proxy_literal($$anchor, $$props) {
function State_proxy_literal($$anchor, $$props) {
$.push($$props, true);
let str = $.source('');
@ -35,4 +35,6 @@ export default function State_proxy_literal($$anchor, $$props) {
$.pop();
}
export default State_proxy_literal;
$.delegate(["click"]);

@ -3,7 +3,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Svelte_element($$anchor, $$props) {
function Svelte_element($$anchor, $$props) {
$.push($$props, true);
let tag = $.prop($$props, "tag", 3, 'hr');
@ -13,4 +13,6 @@ export default function Svelte_element($$anchor, $$props) {
$.element(node, tag, false);
$.append($$anchor, fragment);
$.pop();
}
}
export default Svelte_element;

@ -2,9 +2,9 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import express from 'express';
import { createServer as createViteServer, build } from 'vite';
import { createServer as createViteServer } from 'vite';
const PORT = process.env.PORT || '3000';
const PORT = process.env.PORT || '5173';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -26,30 +26,13 @@ async function createServer() {
return;
}
// Uncomment the line below to enable optimizer.
// process.env.SVELTE_ENV = 'hydrate';
const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
const transformed_template = await vite.transformIndexHtml(req.originalUrl, template);
const { html: appHtml, head: headHtml } = await vite.ssrLoadModule('./src/entry-server.ts');
await build({
root: path.resolve(__dirname, './'),
build: {
minify: false,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('svelte/src')) {
return 'vendor';
}
}
}
}
}
});
const template = fs.readFileSync(path.resolve(__dirname, 'dist', 'index.html'), 'utf-8');
const { html: appHtml, head: headHtml } = await vite.ssrLoadModule('/src/entry-server.ts');
const html = template.replace(`<!--ssr-html-->`, appHtml).replace(`<!--ssr-head-->`, headHtml);
const html = transformed_template
.replace(`<!--ssr-html-->`, appHtml)
.replace(`<!--ssr-head-->`, headHtml);
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
});

@ -1,8 +1,10 @@
// @ts-ignore
import { mount, unmount } from 'svelte';
// @ts-ignore you need to create this file
import { mount, hydrate, unmount } from 'svelte';
import App from './App.svelte';
const component = mount(App, {
const root = document.getElementById('root')!;
const render = root.firstChild?.nextSibling ? hydrate : mount;
const component = render(App, {
target: document.getElementById('root')!
});
// @ts-ignore

@ -34,7 +34,7 @@
/** @type {import('../workers/workers').CompileMessageData | null} */
export let compiled;
$: if (selected) {
$: if (selected && js_editor && css_editor) {
if (selected.type === 'json') {
js_editor.set({ code: `/* Select a component to see its compiled code */`, lang: 'js' });
css_editor.set({ code: `/* Select a component to see its compiled code */`, lang: 'css' });

Loading…
Cancel
Save