Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
11 changes: 6 additions & 5 deletions packages/kit/src/core/sync/write_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ export function write_root(manifest_data, output) {

let l = max_depth;

let pyramid = `<svelte:component this={components[${l}]} data={data_${l}} {form} />`;
let pyramid = `<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} {form} />`;

while (l--) {
pyramid = `
{#if components[${l + 1}]}
<svelte:component this={components[${l}]} data={data_${l}}>
{#if constructors[${l + 1}]}
<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}}>
${pyramid.replace(/\n/g, '\n\t\t\t\t\t')}
</svelte:component>
{:else}
<svelte:component this={components[${l}]} data={data_${l}} {form} />
<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} {form} />
{/if}
`
.replace(/^\t\t\t/gm, '')
Expand All @@ -49,7 +49,8 @@ export function write_root(manifest_data, output) {
export let stores;
export let page;

export let components;
export let constructors;
export let components = [];
export let form;
${levels.map((l) => `export let data_${l} = null;`).join('\n\t\t\t\t')}

Expand Down
17 changes: 17 additions & 0 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ export async function sveltekit() {
hydratable: true,
...svelte_config.compilerOptions
},
experimental: {
dynamicCompileOptions: ({ filename, code, compileOptions }) => {
const options =
svelte_config.vitePlugin?.experimental?.dynamicCompileOptions?.({
filename,
code,
compileOptions
}) ?? {};

const basename = path.basename(filename);
if (basename.startsWith('+layout') || basename.startsWith('+page')) {
options.accessors = true;
}

return options;
}
},
Comment thread
Rich-Harris marked this conversation as resolved.
Outdated
...svelte_config.vitePlugin
};

Expand Down
62 changes: 45 additions & 17 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
is_external_url,
scroll_state
} from './utils.js';
import * as storage from './session-storage.js';
import {
lock_fetch,
unlock_fetch,
Expand All @@ -30,7 +31,7 @@ import { HttpError, Redirect } from '../control.js';
import { stores } from './singletons.js';
import { unwrap_promises } from '../../utils/promises.js';
import * as devalue from 'devalue';
import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY } from './constants.js';
import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js';
import { validate_common_exports } from '../../utils/exports.js';
import { compact } from '../../utils/array.js';

Expand All @@ -51,12 +52,10 @@ default_error_loader();

/** @typedef {{ x: number, y: number }} ScrollPosition */
/** @type {Record<number, ScrollPosition>} */
let scroll_positions = {};
try {
scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]);
} catch {
// do nothing
}
const scroll_positions = storage.get(SCROLL_KEY) ?? {};

/** @type {Record<string, any[]>} */

@tomecko tomecko Jan 24, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** @type {Record<string, any[]>} */
/** @type {Record<number, any[]>} */

I see initial_history_index and current_history_index which both seem to be numbers to be used as an index for snapshots.

const snapshots = storage.get(SNAPSHOT_KEY) ?? {};

/** @param {number} index */
function update_scroll_positions(index) {
Expand All @@ -75,6 +74,9 @@ export function create_client({ target, base }) {
/** @type {Array<((url: URL) => boolean)>} */
const invalidated = [];

/** @type {import('svelte').SvelteComponent[]} */
const components = [];
Comment thread
dummdidumm marked this conversation as resolved.

/** @type {{id: string, promise: Promise<import('./types').NavigationResult>} | null} */
let load_cache = null;

Expand Down Expand Up @@ -123,6 +125,8 @@ export function create_client({ target, base }) {
);
}

const initial_history_index = current_history_index;

// if we reload the page, or Cmd-Shift-T back to it,
// recover scroll position
const scroll = scroll_positions[current_history_index];
Expand Down Expand Up @@ -238,11 +242,20 @@ export function create_client({ target, base }) {
* @param {import('./types').NavigationIntent | undefined} intent
* @param {URL} url
* @param {string[]} redirect_chain
* @param {number} [previous_history_index]
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
* @param {{}} [nav_token] To distinguish between different navigation events and determine the latest. Needed for example for redirects to keep the original token
* @param {() => void} [callback]
*/
async function update(intent, url, redirect_chain, opts, nav_token = {}, callback) {
async function update(
intent,
url,
redirect_chain,
previous_history_index,
opts,
nav_token = {},
callback
) {
token = nav_token;
let navigation_result = intent && (await load_route(intent));

Expand Down Expand Up @@ -301,6 +314,11 @@ export function create_client({ target, base }) {

updating = true;

// `previous_history_index` will be undefined for invalidation
if (previous_history_index) {
snapshots[previous_history_index] = components.map((c) => c.snapshot?.capture());
}

if (opts && opts.details) {
const { details } = opts;
const change = details.replaceState ? 0 : 1;
Expand Down Expand Up @@ -383,10 +401,14 @@ export function create_client({ target, base }) {

root = new Root({
target,
props: { ...result.props, stores },
props: { ...result.props, stores, components },
hydrate: true
});

snapshots[current_history_index]?.forEach((value, i) => {
components[i].snapshot?.restore(value);
});

/** @type {import('types').AfterNavigate} */
const navigation = {
from: null,
Expand Down Expand Up @@ -444,7 +466,7 @@ export function create_client({ target, base }) {
},
props: {
// @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up
components: compact(branch).map((branch_node) => branch_node.node.component)
constructors: compact(branch).map((branch_node) => branch_node.node.component)
}
};

Expand Down Expand Up @@ -1081,8 +1103,11 @@ export function create_client({ target, base }) {
return;
}

// TODO update scroll positions at same time as snapshot?
update_scroll_positions(current_history_index);

const previous_history_index = current_history_index;

accepted();

navigating = true;
Expand All @@ -1095,6 +1120,7 @@ export function create_client({ target, base }) {
intent,
url,
redirect_chain,
previous_history_index,
{
scroll,
keepfocus,
Expand Down Expand Up @@ -1373,12 +1399,10 @@ export function create_client({ target, base }) {
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
update_scroll_positions(current_history_index);
storage.set(SCROLL_KEY, scroll_positions);

try {
sessionStorage[SCROLL_KEY] = JSON.stringify(scroll_positions);
} catch {
// do nothing
}
snapshots[current_history_index] = components.map((c) => c.snapshot?.capture());
storage.set(SNAPSHOT_KEY, snapshots);
}
});

Expand Down Expand Up @@ -1525,15 +1549,15 @@ export function create_client({ target, base }) {
});
});

addEventListener('popstate', (event) => {
addEventListener('popstate', async (event) => {
if (event.state?.[INDEX_KEY]) {
// if a popstate-driven navigation is cancelled, we need to counteract it
// with history.go, which means we end up back here, hence this check
if (event.state[INDEX_KEY] === current_history_index) return;

const delta = event.state[INDEX_KEY] - current_history_index;

navigate({
await navigate({
url: new URL(location.href),
scroll: scroll_positions[event.state[INDEX_KEY]],
keepfocus: false,
Expand All @@ -1548,6 +1572,10 @@ export function create_client({ target, base }) {
type: 'popstate',
delta
});

snapshots[current_history_index].forEach((value, i) => {
components[i].snapshot?.restore(value);
});
}
});

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/client/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const SNAPSHOT_KEY = 'sveltekit:snapshot';
export const SCROLL_KEY = 'sveltekit:scroll';
export const INDEX_KEY = 'sveltekit:index';

Expand Down
25 changes: 25 additions & 0 deletions packages/kit/src/runtime/client/session-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Read a value from `sessionStorage`
* @param {string} key
*/
export function get(key) {
try {
return JSON.parse(sessionStorage[key]);
} catch {
// do nothing
}
}

/**
* Write a value to `sessionStorage`
* @param {string} key
* @param {any} value
*/
export function set(key, value) {
const json = JSON.stringify(value);
try {
sessionStorage[key] = json;
} catch {
// do nothing
}
}
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export async function render_response({
navigating: writable(null),
updated
},
components: await Promise.all(branch.map(({ node }) => node.component())),
constructors: await Promise.all(branch.map(({ node }) => node.component())),
form: form_value
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<a href="/snapshot/a">a</a>
<a href="/snapshot/b">b</a>
<a href="/snapshot/c" data-sveltekit-reload>c</a>

<slot />
10 changes: 10 additions & 0 deletions packages/kit/test/apps/basics/src/routes/snapshot/a/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
let message = '';

export const snapshot = {
capture: () => message,
restore: (snapshot) => (message = snapshot)
};
</script>

<input bind:value={message} />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>b</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>c</h1>
29 changes: 29 additions & 0 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -607,3 +607,32 @@ test.describe('env in app.html', () => {
expect(await page.locator('body').getAttribute('class')).toContain('groovy');
});
});

test.describe('Snapshots', () => {
test('recovers snapshotted data', async ({ page, clicknav }) => {
await page.goto('/snapshot/a');

let input = page.locator('input');
await input.type('hello');

await clicknav('[href="/snapshot/b"]');
await page.goBack();

input = page.locator('input');
expect(await input.inputValue()).toBe('hello');

await input.clear();
await input.type('works for cross-document navigations');

await clicknav('[href="/snapshot/c"]');
await page.goBack();
expect(await page.locator('input').inputValue()).toBe('works for cross-document navigations');

input = page.locator('input');
await input.clear();
await input.type('works for reloads');

await page.reload();
expect(await page.locator('input').inputValue()).toBe('works for reloads');
});
});
1 change: 0 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
"outputMode": "new-only"
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "scripts/**", "shared/**", "templates/**", "test/**"],
"outputs": ["coverage/", "test-results/**"],
"outputMode": "new-only",
Expand Down