Skip to content

Commit 30764db

Browse files
committed
POC HMR in Svelte 5
1 parent 0c58524 commit 30764db

File tree

7 files changed

+110
-7
lines changed

7 files changed

+110
-7
lines changed

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
@@ -340,15 +340,31 @@ export function client_component(source, analysis, options) {
340340
const body = [
341341
...state.hoisted,
342342
...module.body,
343-
b.export_default(
344-
b.function_declaration(
345-
b.id(analysis.name),
346-
[b.id('$$anchor'), b.id('$$props')],
347-
component_block
348-
)
343+
b.function_declaration(
344+
b.id(analysis.name),
345+
[b.id('$$anchor'), b.id('$$props')],
346+
component_block
349347
)
350348
];
351349

350+
if (options.hmr) {
351+
body.push(
352+
b.export_default(
353+
b.conditional(
354+
b.import_meta_hot(),
355+
b.call('$.hmr', b.member(b.import_meta_hot(), b.id('data')), b.id(analysis.name)),
356+
b.id(analysis.name)
357+
)
358+
),
359+
b.if(
360+
b.import_meta_hot(),
361+
b.stmt(b.call('import.meta.hot.acceptExports', b.literal('default')))
362+
)
363+
);
364+
} else {
365+
body.push(b.export_default(b.id(analysis.name)));
366+
}
367+
352368
if (options.discloseVersion) {
353369
body.unshift(b.imports([], 'svelte/internal/disclose-version'));
354370
}

packages/svelte/src/compiler/types/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ export interface CompileOptions extends ModuleCompileOptions {
178178
* @default null
179179
*/
180180
cssOutputFilename?: string;
181+
/**
182+
* If `true`, compiles components with hot reloading support.
183+
*
184+
* @default false
185+
*/
186+
hmr?: boolean;
181187

182188
// Other Svelte 4 compiler options:
183189
// enableSourcemap?: EnableSourcemap; // TODO bring back? https://github.com/sveltejs/svelte/pull/6835
@@ -228,6 +234,7 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
228234
sourcemap: CompileOptions['sourcemap'];
229235
legacy: Required<Required<CompileOptions>['legacy']>;
230236
runes: CompileOptions['runes'];
237+
hmr: CompileOptions['hmr'];
231238
};
232239

233240
export type DeclarationKind =

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,20 @@ export function imports(parts, source) {
553553
};
554554
}
555555

556+
/**
557+
* @return {import('estree').MemberExpression}
558+
*/
559+
export function import_meta_hot() {
560+
return member(
561+
{
562+
type: 'MetaProperty',
563+
meta: id('import'),
564+
property: id('meta')
565+
},
566+
id('hot')
567+
);
568+
}
569+
556570
/**
557571
* @param {import('estree').Expression | null} argument
558572
* @returns {import('estree').ReturnStatement}

packages/svelte/src/compiler/validate-options.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ export const validate_component_options =
106106
return input;
107107
}),
108108

109+
hmr: boolean(false),
110+
109111
hydratable: warn_removed(
110112
'The hydratable option has been removed. Svelte components are always hydratable now.'
111113
),
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { key } from './render.js';
2+
import { source, set, get } from './runtime.js';
3+
4+
/**
5+
* @template {any[]} ComponentArgs
6+
* @template {Record<string, any> | undefined} ComponentReturn
7+
* @template {(...args: ComponentArgs) => ComponentReturn} Component
8+
*
9+
* @param {{
10+
* component_signal: ReturnType<typeof source<Component>>,
11+
* proxy?: (...args: ComponentArgs) => ComponentReturn
12+
* }} hot_data
13+
* @param {Component} component
14+
*/
15+
export function hmr(hot_data, component) {
16+
if (hot_data.proxy) {
17+
set(hot_data.component_signal, component);
18+
} else {
19+
const component_signal = (hot_data.component_signal = source(component));
20+
21+
// @ts-ignore
22+
hot_data.proxy = function (target, ...args) {
23+
/** @type {ComponentReturn} */
24+
let current_accessors;
25+
26+
key(
27+
target,
28+
() => get(component_signal),
29+
($$anchor) => {
30+
const current_component = get(component_signal);
31+
// @ts-ignore
32+
current_accessors = current_component($$anchor, ...args);
33+
}
34+
);
35+
36+
return new Proxy(
37+
{},
38+
{
39+
get(_, p) {
40+
// we actually want to crash if no accessors, because no HMR code would crash
41+
// @ts-ignore
42+
return current_accessors[p];
43+
},
44+
set(_, p, value) {
45+
// we actually want to crash if no accessors, because no HMR code would crash
46+
// @ts-ignore
47+
current_accessors[p] = value;
48+
return true;
49+
}
50+
}
51+
);
52+
};
53+
}
54+
55+
return hot_data.proxy;
56+
}

packages/svelte/src/internal/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ export {
5555
$window as window,
5656
$document as document
5757
} from './client/operations.js';
58+
59+
export { hmr } from './client/hmr.js';

playgrounds/demo/vite.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { defineConfig } from 'vite';
22
import { svelte } from '@sveltejs/vite-plugin-svelte';
33

44
export default defineConfig({
5-
plugins: [svelte()],
5+
plugins: [
6+
svelte({
7+
compilerOptions: {
8+
hmr: true
9+
}
10+
})
11+
],
612
optimizeDeps: {
713
// svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change
814
exclude: ['svelte']

0 commit comments

Comments
 (0)