Skip to content

draft: Smart preserve local state #41

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

Open
wants to merge 5 commits into
base: monorepo-and-tests
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -158,4 +158,4 @@ jobs:
- name: install
run: pnpm install --frozen-lockfile --offline
- name: run tests
run: pnpm test
run: pnpm test:ci
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
"scripts": {
"release": "pnpx --no changeset publish",
"lint": "pnpm --recursive lint",
"test:ci": "pnpm --recursive test:ci",
"test": "pnpm --recursive test"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const { clickButton } = require('./helpers')

describe('local state: preserveLocalStateKey', () => {
const describeIf =
process.env.PRESERVE_LOCAL_STATE === 'false' ? describe : describe.skip

describeIf('local state: preserveLocalStateKey', () => {
testHmr`
# inline annotation

368 changes: 368 additions & 0 deletions packages/svelte-hmr-spec/test/preserveLocalState-smart.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
const { clickButton } = require('./helpers')

const describeIf =
process.env.PRESERVE_LOCAL_STATE === 'smart' ? describe : describe.skip

describeIf('preserveLocalState: smart', () => {
testHmr`
# preserves 1 mutable

--- App.svelte ---

<script>
let x = 0
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 1
`

testHmr`
# preserves 2 mutables

--- App.svelte ---

<script>
let x = 0
let y = 10
const increment = () => { x++, y++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x};{y}
::1 after: {x};{y}
</x-focus>

* * * * *

::0::
before: 0;10
${clickButton()}
before: 1;11
::1::
after: 1;11
`

testHmr`
# resets if @hmr:reset

--- App.svelte ---

<script>
let x = 0
let y = 10
const increment = () => { x++, y++ }
::1 // @hmr:reset
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x};{y}
::1 after: {x};{y}
</x-focus>

* * * * *

::0::
before: 0;10
${clickButton()}
before: 1;11
::1::
after: 0;10
`

testHmr`
# resets 1 mutable of several if init change

--- App.svelte ---

<script>
::0 let x = 0
::1 let x = 100
let y = 10
const increment = () => { x++, y++ }
$: xy = x + y
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x};{y}
::1 after: {x};{y}
reactive sum = {xy}
sum = {x + y}
</x-focus>

* * * * *

::0::
before: 0;10
reactive sum = 10
sum = 10
${clickButton()}
before: 1;11
reactive sum = 12
sum = 12
::1::
after: 100;11
reactive sum = 111
sum = 111
`

testHmr`
# preserves props

--- App.svelte ---

<script>
export let x = 0
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 1
`

testHmr`
# reset props if init changes

--- App.svelte ---

<script>
::0 export let x = 0
::1 export let x = 10
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 10
`

testHmr`
# reset propagates to child

--- Child.svelte ---

<script>
export let foo = ''
</script>

{foo}

--- App.svelte ---

<script>
import Child from './Child.svelte'
::0 let x = 0
::1 let x = 10
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x} -- <Child foo="foo-{x}" />
::1 after: {x} -- <Child foo="foo-{x}" />
</x-focus>

* * * * *

::0::
before: 0 -- foo-0
${clickButton()}
before: 1 -- foo-1
::1::
after: 10 -- foo-10
`

describe('normalization: ignore non significant spaces', () => {
testHmr`
# ignores non significant spaces before =

--- App.svelte ---

<script>
::0 let x = 0
::1 let x = 0
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 1
`

testHmr`
# ignores non significant spaces in primitive values

--- App.svelte ---

<script>
::0 let x = 0
::1 let x = 0
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 1
`

testHmr`
# ignores non significant spaces in single identifier

--- App.svelte ---

<script>
const i = 0

::0 let x = i
::1 let x = i
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 1
`

testHmr`
# ignores non significant spaces in binary expression

--- App.svelte ---

<script>
const i = 0

::0 let x = i + i
::1 let x = i + i
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 1
`

testHmr`
# ignores non significant spaces in multi members expression

--- App.svelte ---

<script>
const i = 0

::0 let x = i + i + 0 + i
::1 let x = i + i + 0 + i
const increment = () => { x++ }
</script>

<button on:click={increment} />

<x-focus>
::0 before: {x}
::1 after: {x}
</x-focus>

* * * * *

::0::
before: 0
${clickButton()}
before: 1
::1::
after: 1
`
})
})
58 changes: 57 additions & 1 deletion packages/svelte-hmr/lib/make-hot.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const globalName = '___SVELTE_HMR_HOT_API'
const globalAdapterName = '___SVELTE_HMR_HOT_API_PROXY_ADAPTER'

const PRESERVE_LOCAL_STATE_SMART = 'smart'

const defaultHotOptions = {
// preserve all local state
preserveLocalState: false,
preserveLocalState: false, // true | false | 'smart'

// escape hatchs from preservation of local state
//
@@ -309,6 +311,53 @@ const createMakeHot = ({ resolveAbsoluteImport, pkg = {} }) => ({
return [...variables]
}

const resolveSmartPreserveLocalState = ({ code, compiled }) => {
if (!compiled.ast.instance) return false

const variables = {}

const reduceDeclaration = decl => {
const init = decl.init

if (!init) return undefined

if ('value' in init) return init.value

if (init.type === 'Identifier') return init.name

if (init.type === 'BinaryExpression') {
return [
reduceDeclaration(init.left),
reduceDeclaration(init.operator),
reduceDeclaration(init.right),
].join(' ')
}

return code.slice(init.start, init.end)
}

walk(compiled.ast.instance, {
enter(node) {
switch (node.type) {
case 'VariableDeclaration': {
if (node.kind !== 'let') return

const decl =
node.declarations && node.declarations[0] && node.declarations[0]

const name = decl.id && node.declarations[0].id.name

if (!name) return

variables[name] = reduceDeclaration(decl)
}
}
},
})

return variables
}

const resolvePreserveLocalState = ({
hotOptions,
originalCode,
@@ -333,6 +382,13 @@ const createMakeHot = ({ resolveAbsoluteImport, pkg = {} }) => ({
if (preserveAllLocalStateKey && hasKey(preserveAllLocalStateKey)) {
return true
}
// preserveLocalState
if (preserveLocalState === PRESERVE_LOCAL_STATE_SMART) {
return resolveSmartPreserveLocalState({ code: originalCode, compiled })
}
if (preserveLocalState) {
return true
}
// preserveLocalStateKey
if (preserveLocalStateKey && hasKey(preserveLocalStateKey)) {
// returns an array of variable names to preserve
4 changes: 4 additions & 0 deletions packages/svelte-hmr/runtime/proxy.js
Original file line number Diff line number Diff line change
@@ -251,6 +251,10 @@ class ProxyComponent {
this.$$ = comp.$$
lastProperties = copyComponentProperties(this, comp, lastProperties)
},
smartPreserveLocalState:
// TODO: use constant
current.hotOptions.preserveLocalState === 'smart' &&
current.preserveLocalState,
})
setComponent(_cmp)
} catch (err) {
30 changes: 28 additions & 2 deletions packages/svelte-hmr/runtime/svelte-hooks.js
Original file line number Diff line number Diff line change
@@ -84,11 +84,13 @@ const get_current_component_safe = () => {
export const createProxiedComponent = (
Component,
initialOptions,
{ allowLiveBinding, onInstance, onMount, onDestroy }
{ allowLiveBinding, onInstance, onMount, onDestroy, smartPreserveLocalState }
) => {
let cmp
let options = initialOptions

let lastSmartPreserveLocalState = smartPreserveLocalState

const isCurrent = _cmp => cmp === _cmp

const assignOptions = (target, anchor, restore, preserveLocalState) => {
@@ -109,18 +111,42 @@ export const createProxiedComponent = (
}

if (preserveLocalState && restore.state) {
// partial
if (Array.isArray(preserveLocalState)) {
// form ['a', 'b'] => preserve only 'a' and 'b'
props.$$inject = {}
for (const key of preserveLocalState) {
props.$$inject[key] = restore.state[key]
}
} else {
}
// smart -- preserve only if declaration has not changed
else if (typeof preserveLocalState === 'object') {
const hasChanged = key =>
lastSmartPreserveLocalState[key] !== preserveLocalState[key]
const someInitHasChanged = Object.keys(preserveLocalState).some(
hasChanged
)
if (someInitHasChanged) {
// delete props.$$inject
props.$$inject = {}
for (const key of Object.keys(preserveLocalState)) {
if (hasChanged(key)) continue
props.$$inject[key] = restore.state[key]
}
} else {
props.$$inject = restore.state
}
}
// all
else {
props.$$inject = restore.state
}
} else {
delete props.$$inject
}

lastSmartPreserveLocalState = preserveLocalState

options = Object.assign({}, initialOptions, {
target,
anchor,
14 changes: 12 additions & 2 deletions playground/kit-app/src/routes/index.svelte
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<script>
let x = 0;
const increment = () => {
x++;
};
</script>

<p>{x}</p>

<button on:click={increment}>++</button>
4 changes: 4 additions & 0 deletions playground/kit-app/svelte.config.js
Original file line number Diff line number Diff line change
@@ -9,6 +9,10 @@ const config = {
kit: {
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte'
},

hot: {
preserveLocalState: 'smart'
}
};

197 changes: 190 additions & 7 deletions pnpm-lock.yaml
6 changes: 5 additions & 1 deletion test/apps/svhs/package.json
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
"@rollup/plugin-commonjs": "^19.0.2",
"@rollup/plugin-node-resolve": "^13.0.4",
"nollup": "^0.17.0",
"npm-run-all": "^4.1.5",
"rollup": "2",
"rollup-plugin-hot": "^0.1.1",
"rollup-plugin-livereload": "^1.0.0",
@@ -27,6 +28,9 @@
"dev": "cross-env MUTE_NOLLUP=0 npm run dev:nollup",
"start": "sirv public --single",
"start:dev": "sirv public --single --dev",
"test": "pnpm svhs"
"test:noPreserveState": "cross-env PRESERVE_LOCAL_STATE=false pnpm svhs",
"test:smartPreserveState": "cross-env PRESERVE_LOCAL_STATE=smart pnpm svhs",
"test:ci": "run-s test:noPreserveState test:smartPreserveState",
"test": "run-p test:noPreserveState test:smartPreserveState"
}
}
18 changes: 10 additions & 8 deletions test/apps/svhs/rollup.config.js
Original file line number Diff line number Diff line change
@@ -17,7 +17,12 @@ const production = !dev

const hot = isNollup || (watch && !useLiveReload)

const noPreserveState = !!process.env.NO_PRESERVE_STATE
const preserveLocalState =
'PRESERVE_LOCAL_STATE' in process.env
? process.env.PRESERVE_LOCAL_STATE === 'false'
? false
: process.env.PRESERVE_LOCAL_STATE
: null

const root = fs.realpathSync(__dirname)

@@ -52,14 +57,11 @@ export default {
},
accessors: true,
hot: hot && {
// expose test hooks from the plugin
test,
// optimistic will try to recover from runtime
// errors during component init
optimistic: true,
// turn on to disable preservation of local component
// state -- i.e. non exported `let` variables
noPreserveState,
// optimistic: true,
...(preserveLocalState != null && {
preserveLocalState,
}),
},
}),