Skip to content

Commit 313673f

Browse files
committed
hmr: proxy accessors both ways
1 parent 6afa2c3 commit 313673f

File tree

6 files changed

+113
-64
lines changed

6 files changed

+113
-64
lines changed

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

Lines changed: 95 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { key } from './render.js';
2-
import { source, set, get } from './runtime.js';
2+
import { source, set, get, push, pop, user_effect } from './runtime.js';
33
import { current_hydration_fragment } from './hydration.js';
44
import { child_frag } from './operations.js';
5-
import { proxy } from './proxy/proxy.js';
5+
import { STATE_SYMBOL, proxy } from './proxy/proxy.js';
66

77
/**
88
* @typedef {Record<string | symbol, any> | undefined} ComponentReturn
@@ -17,41 +17,96 @@ import { proxy } from './proxy/proxy.js';
1717
* }} HotData<Component>
1818
*/
1919

20-
function find_surrounding_ssr_commments() {
21-
if (!current_hydration_fragment?.[0]) return null;
22-
23-
/** @type {Comment | undefined} */
24-
let before;
25-
/** @type {Comment | undefined} */
26-
let after;
27-
/** @type {Node | null | undefined} */
28-
let node;
29-
30-
node = current_hydration_fragment[0].previousSibling;
31-
while (node) {
32-
const comment = /** @type {Comment} */ (node);
33-
if (node.nodeType === 8 && comment.data.startsWith('ssr:')) {
34-
before = comment;
35-
break;
20+
function get_hydration_root() {
21+
function find_surrounding_ssr_commments() {
22+
if (!current_hydration_fragment?.[0]) return null;
23+
24+
/** @type {Comment | undefined} */
25+
let before;
26+
/** @type {Comment | undefined} */
27+
let after;
28+
/** @type {Node | null | undefined} */
29+
let node;
30+
31+
node = current_hydration_fragment[0].previousSibling;
32+
while (node) {
33+
const comment = /** @type {Comment} */ (node);
34+
if (node.nodeType === 8 && comment.data.startsWith('ssr:')) {
35+
before = comment;
36+
break;
37+
}
38+
node = node.previousSibling;
39+
}
40+
41+
node = current_hydration_fragment.at(-1)?.nextSibling;
42+
while (node) {
43+
const comment = /** @type {Comment} */ (node);
44+
if (node.nodeType === 8 && comment.data.startsWith('ssr:')) {
45+
after = comment;
46+
break;
47+
}
48+
node = node.nextSibling;
49+
}
50+
51+
if (before && after && before.data === after.data) {
52+
return [before, after];
3653
}
37-
node = node.previousSibling;
54+
55+
return null;
3856
}
3957

40-
node = current_hydration_fragment.at(-1)?.nextSibling;
41-
while (node) {
42-
const comment = /** @type {Comment} */ (node);
43-
if (node.nodeType === 8 && comment.data.startsWith('ssr:')) {
44-
after = comment;
45-
break;
58+
if (current_hydration_fragment) {
59+
const ssr0 = find_surrounding_ssr_commments();
60+
if (ssr0) {
61+
const [before, after] = ssr0;
62+
current_hydration_fragment.unshift(before);
63+
current_hydration_fragment.push(after);
64+
return child_frag(current_hydration_fragment);
4665
}
47-
node = node.nextSibling;
4866
}
67+
}
68+
69+
function create_accessors_proxy() {
70+
const accessors_proxy = proxy(/** @type {import('./proxy/proxy.js').StateObject} */ ({}));
71+
/** @type {Set<string>} */
72+
const accessors_keys = new Set();
4973

50-
if (before && after && before.data === after.data) {
51-
return [before, after];
74+
/**
75+
* @param {ComponentReturn} new_accessors
76+
*/
77+
function sync_accessors_proxy(new_accessors) {
78+
const removed_keys = new Set(accessors_keys);
79+
80+
if (new_accessors) {
81+
for (const key in new_accessors) {
82+
accessors_keys.add(key);
83+
removed_keys.delete(key);
84+
85+
// current -> proxy
86+
user_effect(() => {
87+
accessors_proxy[key] = new_accessors[key];
88+
})
89+
90+
// proxy -> current
91+
const descriptor = Object.getOwnPropertyDescriptor(new_accessors, key);
92+
if (descriptor?.set || descriptor?.writable) {
93+
user_effect(() => {
94+
const s = accessors_proxy[STATE_SYMBOL].s.get(key);
95+
if (s) {
96+
new_accessors[key] = get(s);
97+
}
98+
});
99+
}
100+
}
101+
}
102+
103+
for (const key of removed_keys) {
104+
accessors_keys.delete(key);
105+
accessors_proxy[key] = undefined;
106+
}
52107
}
53108

54-
return null;
109+
return { accessors_proxy, sync_accessors_proxy };
55110
}
56111

57112
/**
@@ -71,10 +126,10 @@ function create_proxy_component(new_component) {
71126
}
72127

73128
// @ts-ignore
74-
function proxy_component($$anchor, ...args) {
75-
const accessors_proxy = proxy(/** @type {import('./proxy/proxy.js').StateObject} */ ({}));
76-
/** @type {Set<string>} */
77-
const accessors_keys = new Set();
129+
function proxy_component($$anchor, $$props) {
130+
push($$props);
131+
132+
const { accessors_proxy, sync_accessors_proxy } = create_accessors_proxy();
78133

79134
// During hydration the root component will receive a null $$anchor. The
80135
// following is a hack to get our `key` a node to render to, all while
@@ -86,14 +141,8 @@ function create_proxy_component(new_component) {
86141
// still work after that... Maybe we can show a more specific error message than
87142
// the generic hydration failure one (that could be misleading in this case).
88143
//
89-
if (!$$anchor && current_hydration_fragment?.[0]) {
90-
const ssr0 = find_surrounding_ssr_commments();
91-
if (ssr0) {
92-
const [before, after] = ssr0;
93-
current_hydration_fragment.unshift(before);
94-
current_hydration_fragment.push(after);
95-
$$anchor = child_frag(current_hydration_fragment);
96-
}
144+
if (!$$anchor) {
145+
$$anchor = get_hydration_root() || $$anchor;
97146
}
98147

99148
key(
@@ -103,25 +152,14 @@ function create_proxy_component(new_component) {
103152
const component = get(component_signal);
104153

105154
// @ts-ignore
106-
const new_accessors = component($$anchor, ...args);
155+
const new_accessors = component($$anchor, $$props);
107156

108-
const removed_keys = new Set(accessors_keys);
109-
110-
if (new_accessors) {
111-
for (const [key, value] of Object.entries(new_accessors)) {
112-
accessors_proxy[key] = value;
113-
accessors_keys.add(key);
114-
removed_keys.delete(key);
115-
}
116-
}
117-
118-
for (const key of removed_keys) {
119-
accessors_keys.delete(key);
120-
accessors_proxy[key] = undefined;
121-
}
157+
sync_accessors_proxy(new_accessors);
122158
}
123159
);
124160

161+
pop(accessors_proxy);
162+
125163
return accessors_proxy;
126164
}
127165

@@ -132,6 +170,7 @@ function create_proxy_component(new_component) {
132170
}
133171
});
134172
} catch (err) {
173+
// eslint-disable-next-line no-console
135174
console.warn("[Svelte HMR] Failed to proxy component function's name", err);
136175
}
137176

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";
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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as $ from "svelte/internal";
55

66
var frag = $.template(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, true);
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
@@ -46,3 +46,5 @@ export default function Main($$anchor, $$props) {
4646
$.close_frag($$anchor, fragment);
4747
$.pop();
4848
}
49+
50+
export default Main;

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";
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);
@@ -33,4 +33,6 @@ export default function Function_prop_no_getter($$anchor, $$props) {
3333

3434
$.close_frag($$anchor, fragment);
3535
$.pop();
36-
}
36+
}
37+
38+
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";
55

66
var frag = $.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

1111
/* Init */
1212
var h1 = $.open($$anchor, true, frag);
1313

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

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import "svelte/internal/disclose-version";
44
import * as $ from "svelte/internal";
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');
@@ -15,3 +15,5 @@ export default function Svelte_element($$anchor, $$props) {
1515
$.close_frag($$anchor, fragment);
1616
$.pop();
1717
}
18+
19+
export default Svelte_element;

0 commit comments

Comments
 (0)