Skip to content

feat: hot module reloading support for Svelte 5 #11106

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

Merged
merged 20 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-moons-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: hot module reloading support for Svelte 5
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions packages/svelte/src/compiler/utils/builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 44 additions & 0 deletions packages/svelte/src/internal/client/dev/hmr.js
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/tests/parser-legacy/samples/css/output.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,4 +26,6 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
}

$.pop();
}
}

export default Class_state_field_constructor_assignment;
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,4 +35,6 @@ export default function Main($$anchor, $$props) {

$.append($$anchor, fragment);
$.pop();
}
}

export default Main;
Original file line number Diff line number Diff line change
Expand Up @@ -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();

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

$.append($$anchor, fragment);
$.pop();
}
}

export default Each_string_template;
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -30,4 +30,6 @@ export default function Function_prop_no_getter($$anchor, $$props) {

$.append($$anchor, fragment);
$.pop();
}
}

export default Function_prop_no_getter;
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ 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();

var h1 = root();

$.append($$anchor, h1);
$.pop();
}
}

export default Hello_world;
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand All @@ -35,4 +35,6 @@ export default function State_proxy_literal($$anchor, $$props) {
$.pop();
}

export default State_proxy_literal;

$.delegate(["click"]);
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -13,4 +13,6 @@ export default function Svelte_element($$anchor, $$props) {
$.element(node, tag, false);
$.append($$anchor, fragment);
$.pop();
}
}

export default Svelte_element;
33 changes: 8 additions & 25 deletions playgrounds/demo/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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);
});
Expand Down
Loading
Loading