mirror of https://github.com/sveltejs/svelte
feat: `hydratable` (#17154)
* feat: * doc comments * types * types * changeset * tests * docs * hopefully * lint * finally figured out test issues * get docs building * the easy stuff * prune errors * feat: capture clobbering better, capture unused keys, don't block on unused keys * progress on serializing nested promises * fix * idk man but the tests are passing so i'ma checkpoint this * fix tests * compare resolved serialized values * robustify * thunkify * fixes * ajsldkfjalsdfkjasd * tests * docs * ugh * ugh ugh ugh * Update documentation/docs/06-runtime/05-hydratable.md Co-authored-by: kaysef <25851116+kaysef@users.noreply.github.com> * make errors better * tweak * remove context restoration * fix * trim * simplify hydratable (#17230) * simplify hydratable * tidy up * unused --------- Co-authored-by: Elliott Johnson <sejohnson@torchcloudconsulting.com> * improve errors * include all non-internal frames, handle identical stacks * tweak example * tweak message — setTimeout is covered by ALS * cut out the middleman * unused * tidy * Apply suggestions from code review --------- Co-authored-by: Rich Harris <rich.harris@vercel.com> Co-authored-by: kaysef <25851116+kaysef@users.noreply.github.com>pull/17234/head
parent
129c4086f7
commit
c2a110cd81
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: `hydratable` API
|
||||
@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Hydratable data
|
||||
---
|
||||
|
||||
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often -- it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
|
||||
|
||||
To fix the example above:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
|
||||
|
||||
```ts
|
||||
import { hydratable } from 'svelte';
|
||||
const rand = hydratable('random', () => Math.random());
|
||||
```
|
||||
|
||||
If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.
|
||||
|
||||
## Serialization
|
||||
|
||||
All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{await promises.one}
|
||||
{await promises.two}
|
||||
```
|
||||
@ -0,0 +1,34 @@
|
||||
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
|
||||
|
||||
### unresolved_hydratable
|
||||
|
||||
```
|
||||
A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
|
||||
|
||||
The `hydratable` was initialized in:
|
||||
%stack%
|
||||
```
|
||||
|
||||
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
|
||||
the result inside a `svelte:boundary` with a `pending` snippet:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from '$lib/get-user.js';
|
||||
|
||||
const user = hydratable('user', getUser);
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<h1>{(await user).name}</h1>
|
||||
|
||||
{#snippet pending()}
|
||||
<div>Loading...</div>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
|
||||
|
||||
Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.
|
||||
@ -0,0 +1,30 @@
|
||||
## unresolved_hydratable
|
||||
|
||||
> A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
|
||||
>
|
||||
> The `hydratable` was initialized in:
|
||||
> %stack%
|
||||
|
||||
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
|
||||
the result inside a `svelte:boundary` with a `pending` snippet:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from '$lib/get-user.js';
|
||||
|
||||
const user = hydratable('user', getUser);
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<h1>{(await user).name}</h1>
|
||||
|
||||
{#snippet pending()}
|
||||
<div>Loading...</div>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
|
||||
|
||||
Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.
|
||||
@ -0,0 +1,33 @@
|
||||
import { async_mode_flag } from '../flags/index.js';
|
||||
import { hydrating } from './dom/hydration.js';
|
||||
import * as w from './warnings.js';
|
||||
import * as e from './errors.js';
|
||||
import { DEV } from 'esm-env';
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} key
|
||||
* @param {() => T} fn
|
||||
* @returns {T}
|
||||
*/
|
||||
export function hydratable(key, fn) {
|
||||
if (!async_mode_flag) {
|
||||
e.experimental_async_required('hydratable');
|
||||
}
|
||||
|
||||
if (hydrating) {
|
||||
const store = window.__svelte?.h;
|
||||
|
||||
if (store?.has(key)) {
|
||||
return /** @type {T} */ (store.get(key));
|
||||
}
|
||||
|
||||
if (DEV) {
|
||||
e.hydratable_missing_but_required(key);
|
||||
} else {
|
||||
w.hydratable_missing_but_expected(key);
|
||||
}
|
||||
}
|
||||
|
||||
return fn();
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
/** @import { HydratableLookupEntry } from '#server' */
|
||||
import { async_mode_flag } from '../flags/index.js';
|
||||
import { get_render_context } from './render-context.js';
|
||||
import * as e from './errors.js';
|
||||
import * as devalue from 'devalue';
|
||||
import { get_stack } from '../shared/dev.js';
|
||||
import { DEV } from 'esm-env';
|
||||
import { get_user_code_location } from './dev.js';
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} key
|
||||
* @param {() => T} fn
|
||||
* @returns {T}
|
||||
*/
|
||||
export function hydratable(key, fn) {
|
||||
if (!async_mode_flag) {
|
||||
e.experimental_async_required('hydratable');
|
||||
}
|
||||
|
||||
const { hydratable } = get_render_context();
|
||||
|
||||
let entry = hydratable.lookup.get(key);
|
||||
|
||||
if (entry !== undefined) {
|
||||
if (DEV) {
|
||||
const comparison = compare(key, entry, encode(key, fn()));
|
||||
comparison.catch(() => {});
|
||||
hydratable.comparisons.push(comparison);
|
||||
}
|
||||
|
||||
return /** @type {T} */ (entry.value);
|
||||
}
|
||||
|
||||
const value = fn();
|
||||
|
||||
entry = encode(key, value, hydratable.unresolved_promises);
|
||||
hydratable.lookup.set(key, entry);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @param {Map<Promise<any>, string>} [unresolved]
|
||||
*/
|
||||
function encode(key, value, unresolved) {
|
||||
/** @type {HydratableLookupEntry} */
|
||||
const entry = { value, serialized: '' };
|
||||
|
||||
if (DEV) {
|
||||
entry.stack = get_user_code_location();
|
||||
}
|
||||
|
||||
let uid = 1;
|
||||
|
||||
entry.serialized = devalue.uneval(entry.value, (value, uneval) => {
|
||||
if (value instanceof Promise) {
|
||||
const p = value
|
||||
.then((v) => `r(${uneval(v)})`)
|
||||
.catch((devalue_error) =>
|
||||
e.hydratable_serialization_failed(
|
||||
key,
|
||||
serialization_stack(entry.stack, devalue_error?.stack)
|
||||
)
|
||||
);
|
||||
|
||||
// prevent unhandled rejections from crashing the server
|
||||
p.catch(() => {});
|
||||
|
||||
// track which promises are still resolving when render is complete
|
||||
unresolved?.set(p, key);
|
||||
p.finally(() => unresolved?.delete(p));
|
||||
|
||||
// we serialize promises as `"${i}"`, because it's impossible for that string
|
||||
// to occur 'naturally' (since the quote marks would have to be escaped)
|
||||
const placeholder = `"${uid++}"`;
|
||||
|
||||
(entry.promises ??= []).push(
|
||||
p.then((s) => {
|
||||
entry.serialized = entry.serialized.replace(placeholder, s);
|
||||
})
|
||||
);
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {HydratableLookupEntry} a
|
||||
* @param {HydratableLookupEntry} b
|
||||
*/
|
||||
async function compare(key, a, b) {
|
||||
// note: these need to be loops (as opposed to Promise.all) because
|
||||
// additional promises can get pushed to them while we're awaiting
|
||||
// an earlier one
|
||||
for (const p of a?.promises ?? []) {
|
||||
await p;
|
||||
}
|
||||
|
||||
for (const p of b?.promises ?? []) {
|
||||
await p;
|
||||
}
|
||||
|
||||
if (a.serialized !== b.serialized) {
|
||||
const a_stack = /** @type {string} */ (a.stack);
|
||||
const b_stack = /** @type {string} */ (b.stack);
|
||||
|
||||
const stack =
|
||||
a_stack === b_stack
|
||||
? `Occurred at:\n${a_stack}`
|
||||
: `First occurrence at:\n${a_stack}\n\nSecond occurrence at:\n${b_stack}`;
|
||||
|
||||
e.hydratable_clobbering(key, stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | undefined} root_stack
|
||||
* @param {string | undefined} uneval_stack
|
||||
*/
|
||||
function serialization_stack(root_stack, uneval_stack) {
|
||||
let out = '';
|
||||
if (root_stack) {
|
||||
out += root_stack + '\n';
|
||||
}
|
||||
if (uneval_stack) {
|
||||
out += 'Caused by:\n' + uneval_stack + '\n';
|
||||
}
|
||||
return out || '<missing stack trace>';
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
// @ts-ignore -- we don't include node types in the production build
|
||||
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
|
||||
/** @import { RenderContext } from '#server' */
|
||||
|
||||
import { deferred } from '../shared/utils.js';
|
||||
import * as e from './errors.js';
|
||||
|
||||
/** @type {Promise<void> | null} */
|
||||
let current_render = null;
|
||||
|
||||
/** @type {RenderContext | null} */
|
||||
let context = null;
|
||||
|
||||
/** @returns {RenderContext} */
|
||||
export function get_render_context() {
|
||||
const store = context ?? als?.getStore();
|
||||
|
||||
if (!store) {
|
||||
e.server_context_required();
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {() => Promise<T>} fn
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
export async function with_render_context(fn) {
|
||||
context = {
|
||||
hydratable: {
|
||||
lookup: new Map(),
|
||||
comparisons: [],
|
||||
unresolved_promises: new Map()
|
||||
}
|
||||
};
|
||||
|
||||
if (in_webcontainer()) {
|
||||
const { promise, resolve } = deferred();
|
||||
const previous_render = current_render;
|
||||
current_render = promise;
|
||||
await previous_render;
|
||||
return fn().finally(resolve);
|
||||
}
|
||||
|
||||
try {
|
||||
if (als === null) {
|
||||
e.async_local_storage_unavailable();
|
||||
}
|
||||
return als.run(context, fn);
|
||||
} finally {
|
||||
context = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {AsyncLocalStorage<RenderContext | null> | null} */
|
||||
let als = null;
|
||||
|
||||
export async function init_render_context() {
|
||||
if (als !== null) return;
|
||||
try {
|
||||
// @ts-ignore -- we don't include node types in the production build
|
||||
const { AsyncLocalStorage } = await import('node:async_hooks');
|
||||
als = new AsyncLocalStorage();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// this has to be a function because rollup won't treeshake it if it's a constant
|
||||
function in_webcontainer() {
|
||||
// @ts-ignore -- this will fail when we run typecheck because we exclude node types
|
||||
// eslint-disable-next-line n/prefer-global/process
|
||||
return !!globalThis.process?.versions?.webcontainer;
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import { define_property } from './utils.js';
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
* @returns {Error & { stack: string } | null}
|
||||
*/
|
||||
export function get_error(label) {
|
||||
const error = new Error();
|
||||
const stack = get_stack();
|
||||
|
||||
if (stack.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
stack.unshift('\n');
|
||||
|
||||
define_property(error, 'stack', {
|
||||
value: stack.join('\n')
|
||||
});
|
||||
|
||||
define_property(error, 'name', {
|
||||
value: label
|
||||
});
|
||||
|
||||
return /** @type {Error & { stack: string }} */ (error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function get_stack() {
|
||||
// @ts-ignore - doesn't exist everywhere
|
||||
const limit = Error.stackTraceLimit;
|
||||
// @ts-ignore - doesn't exist everywhere
|
||||
Error.stackTraceLimit = Infinity;
|
||||
const stack = new Error().stack;
|
||||
// @ts-ignore - doesn't exist everywhere
|
||||
Error.stackTraceLimit = limit;
|
||||
|
||||
if (!stack) return [];
|
||||
|
||||
const lines = stack.split('\n');
|
||||
const new_lines = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const posixified = line.replaceAll('\\', '/');
|
||||
|
||||
if (line.trim() === 'Error') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.includes('validate_each_keys')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
new_lines.push(line);
|
||||
}
|
||||
|
||||
return new_lines;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { tick } from 'svelte';
|
||||
import { ok, test } from '../../test';
|
||||
|
||||
export default test({
|
||||
skip_no_async: true,
|
||||
skip_mode: ['server'],
|
||||
|
||||
server_props: { environment: 'server' },
|
||||
ssrHtml: '<p>The current environment is: server</p>',
|
||||
|
||||
props: { environment: 'browser' },
|
||||
|
||||
async test({ assert, target, variant }) {
|
||||
// make sure hydration has a chance to finish
|
||||
await tick();
|
||||
const p = target.querySelector('p');
|
||||
ok(p);
|
||||
if (variant === 'hydrate') {
|
||||
assert.htmlEqual(p.outerHTML, '<p>The current environment is: server</p>');
|
||||
} else {
|
||||
assert.htmlEqual(p.outerHTML, '<p>The current environment is: browser</p>');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from "svelte";
|
||||
|
||||
let { environment } = $props();
|
||||
|
||||
const value = hydratable("environment", () => Promise.resolve({
|
||||
nested: Promise.resolve({
|
||||
environment
|
||||
})
|
||||
}))
|
||||
</script>
|
||||
|
||||
<p>The current environment is: {await value.then(res => res.nested).then(res => res.environment)}</p>
|
||||
@ -0,0 +1,13 @@
|
||||
import { ok, test } from '../../test';
|
||||
|
||||
export default test({
|
||||
skip_no_async: true,
|
||||
mode: ['async-server', 'hydrate'],
|
||||
|
||||
server_props: { environment: 'server' },
|
||||
ssrHtml: '<p>The current environment is: server</p>',
|
||||
|
||||
props: { environment: 'browser' },
|
||||
|
||||
runtime_error: 'hydratable_missing_but_required'
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from "svelte";
|
||||
|
||||
let { environment }: { environment: 'server' | 'browser' } = $props();
|
||||
|
||||
let value;
|
||||
if (environment === 'server') {
|
||||
value = 'server';
|
||||
} else {
|
||||
value = hydratable('environment', () => environment);
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>The current environment is: {value}</p>
|
||||
@ -0,0 +1,27 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
skip_no_async: true,
|
||||
mode: ['async-server', 'hydrate'],
|
||||
|
||||
server_props: { environment: 'server' },
|
||||
ssrHtml:
|
||||
'<div>did you ever hear the tragedy of darth plagueis the wise?</div><div>Loading...</div>',
|
||||
|
||||
test_ssr({ assert, warnings }) {
|
||||
assert.strictEqual(warnings.length, 1);
|
||||
// for some strange reason we trim the error code off the beginning of warnings so I can't actually assert it
|
||||
assert.include(warnings[0], 'A `hydratable` value with key `partially_used`');
|
||||
},
|
||||
|
||||
async test({ assert, target }) {
|
||||
// make sure the hydratable promise on the client has a chance to run and reject (it shouldn't, because the server data should be used)
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<div>did you ever hear the tragedy of darth plagueis the wise?</div><div>no, sith daddy, please tell me</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from "svelte";
|
||||
|
||||
const { environment } = $props();
|
||||
|
||||
const partially_used_hydratable = hydratable(
|
||||
"partially_used",
|
||||
() => {
|
||||
return {
|
||||
used: new Promise(
|
||||
(res, rej) => environment === 'server' ? setTimeout(() => res('did you ever hear the tragedy of darth plagueis the wise?'), 0) : rej('should not run')
|
||||
),
|
||||
unused: new Promise(
|
||||
(res, rej) => environment === 'server' ? setTimeout(() => res('no, sith daddy, please tell me'), 0) : rej('should not run')
|
||||
),
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<div>{await partially_used_hydratable.used}</div>
|
||||
<svelte:boundary>
|
||||
<div>{await partially_used_hydratable.unused}</div>
|
||||
{#snippet pending()}
|
||||
<div>Loading...</div>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,26 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
skip_no_async: true,
|
||||
mode: ['async-server', 'hydrate'],
|
||||
|
||||
server_props: { environment: 'server' },
|
||||
ssrHtml: '<div>Loading...</div>',
|
||||
|
||||
test_ssr({ assert, warnings }) {
|
||||
assert.strictEqual(warnings.length, 1);
|
||||
// for some strange reason we trim the error code off the beginning of warnings so I can't actually assert it
|
||||
assert.include(warnings[0], 'A `hydratable` value with key `unused_key`');
|
||||
},
|
||||
|
||||
async test({ assert, target }) {
|
||||
// make sure the hydratable promise on the client has a chance to run and reject (it shouldn't, because the server data should be used)
|
||||
await tick();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
'<div>did you ever hear the tragedy of darth plagueis the wise?</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from "svelte";
|
||||
|
||||
const { environment } = $props();
|
||||
|
||||
const unresolved_hydratable = hydratable(
|
||||
"unused_key",
|
||||
() => new Promise(
|
||||
(res, rej) => environment === 'server' ? setTimeout(() => res('did you ever hear the tragedy of darth plagueis the wise?'), 0) : rej('should not run')
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:boundary>
|
||||
<div>{await unresolved_hydratable}</div>
|
||||
{#snippet pending()}
|
||||
<div>Loading...</div>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
@ -0,0 +1,21 @@
|
||||
import { ok, test } from '../../test';
|
||||
|
||||
export default test({
|
||||
skip_no_async: true,
|
||||
skip_mode: ['server'],
|
||||
|
||||
server_props: { environment: 'server' },
|
||||
ssrHtml: '<p>The current environment is: server</p>',
|
||||
|
||||
props: { environment: 'browser' },
|
||||
|
||||
async test({ assert, target, variant }) {
|
||||
const p = target.querySelector('p');
|
||||
ok(p);
|
||||
if (variant === 'hydrate') {
|
||||
assert.htmlEqual(p.outerHTML, '<p>The current environment is: server</p>');
|
||||
} else {
|
||||
assert.htmlEqual(p.outerHTML, '<p>The current environment is: browser</p>');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from "svelte";
|
||||
|
||||
let { environment } = $props();
|
||||
|
||||
const value = hydratable("environment", () => environment)
|
||||
</script>
|
||||
|
||||
<p>The current environment is: {value}</p>
|
||||
@ -1,6 +1,7 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
skip: true, // TODO it appears there might be an actual bug here; the promise isn't ever actually awaited in spite of being awaited in the component
|
||||
mode: ['async'],
|
||||
error: 'lifecycle_outside_component'
|
||||
});
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['async']
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from 'svelte';
|
||||
|
||||
await hydratable('key', () => 'first');
|
||||
await hydratable('key', () => 'first');
|
||||
</script>
|
||||
@ -0,0 +1,6 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['async'],
|
||||
error: 'hydratable_clobbering'
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from 'svelte';
|
||||
|
||||
hydratable('key', () => Promise.resolve({
|
||||
nested: Promise.resolve({
|
||||
one: Promise.resolve(1),
|
||||
}),
|
||||
two: Promise.resolve(2),
|
||||
}));
|
||||
hydratable('key', () => Promise.resolve({
|
||||
nested: Promise.resolve({
|
||||
one: Promise.resolve(2),
|
||||
}),
|
||||
two: Promise.resolve(2),
|
||||
}));
|
||||
</script>
|
||||
@ -0,0 +1,6 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['async'],
|
||||
error: 'hydratable_clobbering'
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { hydratable } from 'svelte';
|
||||
|
||||
hydratable('key', () => 'first');
|
||||
hydratable('key', () => 'second');
|
||||
</script>
|
||||
@ -1,9 +1,11 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
skip: true, // TODO: This test actually works, but the error message is printed, not thrown, so we need to have a way to test for that
|
||||
compileOptions: {
|
||||
dev: true
|
||||
},
|
||||
|
||||
error:
|
||||
'node_invalid_placement_ssr: `<p>` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:2:1) cannot be a child of `<p>` (packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte:1:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
|
||||
});
|
||||
|
||||
Loading…
Reference in new issue