Skip to content

Commit aa57f97

Browse files
committed
BREAKING change noPreserveState to preserveLocalState (default false), & add variable level preservation of state
1 parent 4e773d0 commit aa57f97

File tree

4 files changed

+211
-33
lines changed

4 files changed

+211
-33
lines changed

README.md

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,66 @@ By default, `svelte-hmr` will trigger a full browser reload when it detects an e
2929

3030
#### noPreserveState
3131

32+
**Deprecated: removed and default changed from version 0.12. Use `preserveState` instead.**
33+
34+
#### preserveState
35+
3236
Type: `bool`<br>
3337
Default: `false`
3438

35-
Prevent preserving the state of the component (i.e. value of props and `let` variables) across HMR updates.
36-
37-
Note that, independently of this option, the state of child components of a component that is impacted by a HMR update will never be preserved beyond what is re-injected by props, and the state of parent / sibling components will always be preserved (more accurately: parent and siblings are not affected by HMR updates). Read the HMR explanation bellow if you want to understand why.
39+
Enable [preservation of local state](#preservation-of-local-state) for all variables in all components.
3840

3941
#### noPreserveStateKey
4042

4143
Type: `string`<br>
42-
Default: `'@!hmr'`
44+
Default: `'@hmr:reset'` (also accepts legacy `'@!hmr'`)
4345

44-
Escape hatch from preservation of local state. There are some situation where preservation of state gets in the way, typically when you want to change the initial / default value of a prop or local variable. If this string appears anywhere in the component's code, then state won't be preserved for this update.
46+
Force disable preservation of local state for this component.
4547

46-
You'd generally use it with a quick comment right where you are currently editing the code, and remove it just after saving the file:
48+
This flag has priority over all other settings of state preservation. If it is present, all the state of the component will be reset on the next update, regardless of the value of all the other state preservation settings.
4749

48-
```js
49-
let answer = 42 // @!hmr
50+
```svelte
51+
<!-- @hmr:reset -->
52+
53+
<script>
54+
'@hmr:reset'
55+
56+
// @hmr:reset
57+
</script>
5058
```
5159

52-
But you can also make it more permanent if you find that some of your components don't play with state preservation. Maybe use a noop string to clearly manifest your intention?
60+
#### preserveAllLocalStateKey
61+
62+
Type: `string`<br>
63+
Default: `'@hmr:keep-all'`
64+
65+
Force preservation of all local variables of this component.
5366

5467
```svelte
68+
<!-- @hmr:keep-all -->
69+
5570
<script>
56-
'@!hmr'
57-
...
71+
'@hmr:keep-all'
72+
73+
// @hmr:keep-all
74+
</script>
75+
```
76+
77+
#### preserveLocalStateKey
78+
79+
Type: `string`<br>
80+
Default: `'@hmr:keep'`
81+
82+
Force preservation of a given local variable in this component.
83+
84+
```svelte
85+
<script>
86+
// @hmr:keep
87+
let x = 0
88+
89+
let y = 0 // @hmr:keep
90+
91+
x = 1 // @hmr:keep
5892
</script>
5993
```
6094

@@ -118,7 +152,9 @@ Now, the best way to see what it can do for you is probably to checkout the temp
118152

119153
### Preservation of local state
120154

121-
Local state is preserved by Svelte HMR, that is any state that Svelte itself tracks as reactive (basically any root scope `let` vars, exported or not).
155+
**From version 0.12** this behaviour has been deemed too confusing and hard to anticipate, so preservation of state is now disabled by default, and some escape hatches to preserve the state of some given variables have been added.
156+
157+
Local state can be preserved by Svelte HMR, that is any state that Svelte itself tracks as reactive (basically any root scope `let` vars, exported or not).
122158

123159
This means that in code like this:
124160

@@ -133,9 +169,25 @@ This means that in code like this:
133169

134170
If you replace `let x = 1` by `let x = 10` and save, the previous value of `x` will be preserved. That is, `x` will be 2 and not 10. The restoration of previous state happens _after_ the init code of the component has run, so the value will not be 11 either, despite the `x++` that is still here.
135171

136-
If you find this behaviour inconvenient, you can disable it by setting `noPreserveState` option of your HMR-enabled bundler plugin.
172+
If you want this behaviour for all the state of all your components, you can enable it by setting the `preserveLocalState` option to `true`.
173+
174+
If you then want to disable it for just one particular file, or just temporarily, you can turn it off by adding a `// @hmr:reset` comment somewhere in your component.
137175

138-
If you want to disable it for just one particular file, or just temporarily, you can turn it off by adding a `// @!hmr` comment somewhere in your component.
176+
On the contrary, if you keep the default `preserveLocalState` to `false`, you can enable preservation of all the local state of a given component by adding the following comment: `// @hmr:keep-all`. You can also preserve only the state of some specific variables, by annotating them with: `// @hmr:keep`.
177+
178+
For example:
179+
180+
```svelte
181+
<script>
182+
let x = 0 // @hmr:keep
183+
184+
// or:
185+
186+
// @hmr:keep
187+
let y = 1,
188+
z = 2
189+
</script>
190+
```
139191

140192
## Svelte HMR tools
141193

lib/make-hot.js

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ const globalName = '___SVELTE_HMR_HOT_API'
44
const globalAdapterName = '___SVELTE_HMR_HOT_API_PROXY_ADAPTER'
55

66
const defaultHotOptions = {
7-
// don't preserve local state
8-
noPreserveState: false,
9-
// escape hatch from preserve local state -- if this string appears anywhere
10-
// in the component's code, then state won't be preserved for this update
11-
noPreserveStateKey: '@!hmr',
7+
// preserve all local state
8+
preserveLocalState: false,
9+
10+
// escape hatchs from preservation of local state
11+
//
12+
// disable preservation of state for this component
13+
noPreserveStateKey: ['@hmr:reset', '@!hmr'],
14+
// enable preservation of state for all variables in this component
15+
preserveAllLocalStateKey: '@hmr:keep-all',
16+
// enable preservation of state for a given variable (must be inline or
17+
// above the target variable or variables; can be repeated)
18+
preserveLocalStateKey: '@hmr:keep',
19+
1220
// don't reload on fatal error
1321
noReload: false,
1422
// try to recover after runtime errors during component init
@@ -183,6 +191,116 @@ const isProp = v => v.export_name && !v.module
183191
// meta can be 'import.meta' or 'module'
184192
// const createMakeHot = (hotApi = defaultHotApi, options) => {
185193
const createMakeHot = ({ walk, meta = 'import.meta', hotApi, adapter }) => {
194+
const resolvePreserveLocalStateKey = ({
195+
preserveLocalStateKey,
196+
compiled,
197+
}) => {
198+
const containsKey = comments =>
199+
comments &&
200+
comments.some(({ value }) => value.includes(preserveLocalStateKey))
201+
202+
const variables = new Set()
203+
204+
const addReference = node => {
205+
if (!node.name) {
206+
// eslint-disable-next-line no-console
207+
console.warn('Incorrect identifier for preserveLocalStateKey')
208+
}
209+
variables.add(node.name)
210+
}
211+
212+
const processNodes = targets => targets.forEach(processNode)
213+
214+
const processNode = node => {
215+
switch (node.type) {
216+
case 'Identifier':
217+
variables.add(node.name)
218+
return true
219+
case 'UpdateExpression':
220+
addReference(node.argument)
221+
return true
222+
case 'VariableDeclarator':
223+
addReference(node.id)
224+
return true
225+
case 'AssignmentExpression':
226+
processNode(node.left)
227+
return true
228+
case 'ExpressionStatement':
229+
processNode(node.expression)
230+
return true
231+
232+
case 'VariableDeclaration':
233+
processNodes(node.declarations)
234+
return true
235+
case 'SequenceExpression': // ++, --
236+
processNodes(node.expressions)
237+
return true
238+
}
239+
return false
240+
}
241+
242+
const stack = []
243+
244+
if (compiled.ast.instance) {
245+
walk(compiled.ast.instance, {
246+
leave() {
247+
stack.shift()
248+
},
249+
enter(node) {
250+
stack.unshift(node)
251+
if (
252+
containsKey(node.leadingComments) ||
253+
containsKey(node.trailingComments)
254+
) {
255+
stack
256+
.slice(0, 3)
257+
.reverse()
258+
.some(processNode)
259+
}
260+
},
261+
})
262+
}
263+
264+
return [...variables]
265+
}
266+
267+
const resolvePreserveLocalState = ({
268+
hotOptions,
269+
originalCode,
270+
compiled,
271+
}) => {
272+
const {
273+
preserveLocalState,
274+
noPreserveStateKey,
275+
preserveLocalStateKey,
276+
preserveAllLocalStateKey,
277+
} = hotOptions
278+
if (originalCode) {
279+
const hasKey = key => {
280+
const test = k => originalCode.indexOf(k) !== -1
281+
return Array.isArray(key) ? key.some(test) : test(key)
282+
}
283+
// noPreserveStateKey
284+
if (noPreserveStateKey && hasKey(noPreserveStateKey)) {
285+
return false
286+
}
287+
// preserveAllLocalStateKey
288+
if (preserveAllLocalStateKey && hasKey(preserveAllLocalStateKey)) {
289+
return true
290+
}
291+
// preserveLocalStateKey
292+
if (preserveLocalStateKey && hasKey(preserveLocalStateKey)) {
293+
// returns an array of variable names to preserve
294+
return resolvePreserveLocalStateKey({ preserveLocalStateKey, compiled })
295+
}
296+
}
297+
// preserveLocalState
298+
if (preserveLocalState) {
299+
return true
300+
}
301+
return false
302+
}
303+
186304
const hasAccessorsOption = compiled => {
187305
if (!compiled.ast || !compiled.ast.html) return
188306
let accessors = false
@@ -276,13 +394,13 @@ const createMakeHot = ({ walk, meta = 'import.meta', hotApi, adapter }) => {
276394

277395
const { importAdapterName } = hotOptions
278396

279-
const noPreserveState =
280-
hotOptions.noPreserveState ||
281-
(hotOptions.noPreserveStateKey &&
282-
originalCode &&
283-
originalCode.indexOf(hotOptions.noPreserveStateKey) !== -1)
397+
const preserveLocalState = resolvePreserveLocalState({
398+
hotOptions,
399+
originalCode,
400+
compiled,
401+
})
284402

285-
const options = JSON.stringify({ ...hotOptions, noPreserveState })
403+
const options = JSON.stringify({ ...hotOptions, preserveLocalState })
286404

287405
const adapterImport = adapter || resolveAdapterImport(hotOptions)
288406

runtime/proxy.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class ProxyComponent {
126126
Adapter,
127127
id,
128128
debugName,
129-
current, // { Component, hotOptions: { noPreserveState, ... } }
129+
current, // { Component, hotOptions: { preserveLocalState, ... } }
130130
register,
131131
},
132132
options // { target, anchor, ... }
@@ -158,8 +158,8 @@ class ProxyComponent {
158158
adapter.rerender()
159159
} else {
160160
try {
161-
const noPreserveState = current.hotOptions.noPreserveState
162-
const replaceOptions = { target, anchor, noPreserveState }
161+
const preserveLocalState = current.hotOptions.preserveLocalState
162+
const replaceOptions = { target, anchor, preserveLocalState }
163163
if (conservativeDestroy) {
164164
replaceOptions.conservativeDestroy = true
165165
}

runtime/svelte-hooks.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,18 @@ export const createProxiedComponent = (
8181

8282
const isCurrent = _cmp => cmp === _cmp
8383

84-
const assignOptions = (target, anchor, restore, noPreserveState) => {
84+
const assignOptions = (target, anchor, restore, preserveLocalState) => {
8585
const props = Object.assign({}, options.props)
86-
if (!noPreserveState && restore.state) {
87-
props.$$inject = restore.state
86+
if (preserveLocalState && restore.state) {
87+
if (Array.isArray(preserveLocalState)) {
88+
// form ['a', 'b'] => preserve only 'a' and 'b'
89+
props.$$inject = {}
90+
for (const key of preserveLocalState) {
91+
props.$$inject[key] = restore.state[key]
92+
}
93+
} else {
94+
props.$$inject = restore.state
95+
}
8896
} else {
8997
delete props.$$inject
9098
}
@@ -116,12 +124,12 @@ export const createProxiedComponent = (
116124
{
117125
target = options.target,
118126
anchor = options.anchor,
119-
noPreserveState,
127+
preserveLocalState,
120128
conservative = false,
121129
}
122130
) => {
123131
const restore = captureState(targetCmp)
124-
assignOptions(target, anchor, restore, noPreserveState)
132+
assignOptions(target, anchor, restore, preserveLocalState)
125133
const previous = cmp
126134
if (conservative) {
127135
try {

0 commit comments

Comments
 (0)