idk man but the tests are passing so i'ma checkpoint this

elliott/hydratable
Elliott Johnson 1 day ago
parent 5af28fe91b
commit 923b086215

@ -1,12 +1,15 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### unused_hydratable
### unresolved_hydratable
```
A `hydratable` value with key `%key%` was created, but not used during the render.
A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
Stack:
The `hydratable` was initialized in:
%stack%
The unresolved data is:
%unresolved_data%
```
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
@ -30,3 +33,5 @@ the result inside a `svelte:boundary` with a `pending` snippet:
```
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.

@ -1,9 +1,12 @@
## unused_hydratable
## unresolved_hydratable
> A `hydratable` value with key `%key%` was created, but not used during the render.
> A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
>
> Stack:
> The `hydratable` was initialized in:
> %stack%
>
> The unresolved data is:
> %unresolved_data%
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:
@ -26,3 +29,5 @@ the result inside a `svelte:boundary` with a `pending` snippet:
```
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.

@ -5,6 +5,7 @@ import { define_property } from '../../shared/utils.js';
import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, untrack } from '../runtime.js';
import { get_infinite_stack } from '../../shared/dev.js';
/**
* @typedef {{
@ -134,55 +135,37 @@ export function trace(label, fn) {
* @returns {Error & { stack: string } | null}
*/
export function get_stack(label) {
// @ts-ignore stackTraceLimit doesn't exist everywhere
const limit = Error.stackTraceLimit;
return get_infinite_stack(label, (stack) => {
if (!stack) return;
// @ts-ignore
Error.stackTraceLimit = Infinity;
let error = Error();
const lines = stack.split('\n');
const new_lines = ['\n'];
// @ts-ignore
Error.stackTraceLimit = limit;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const posixified = line.replaceAll('\\', '/');
const stack = error.stack;
if (line === 'Error') {
continue;
}
if (!stack) return null;
if (line.includes('validate_each_keys')) {
return undefined;
}
const lines = stack.split('\n');
const new_lines = ['\n'];
if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
continue;
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const posixified = line.replaceAll('\\', '/');
if (line === 'Error') {
continue;
}
if (line.includes('validate_each_keys')) {
return null;
new_lines.push(line);
}
if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
continue;
if (new_lines.length === 1) {
return undefined;
}
new_lines.push(line);
}
if (new_lines.length === 1) {
return null;
}
define_property(error, 'stack', {
value: new_lines.join('\n')
return new_lines.join('\n');
});
define_property(error, 'name', {
value: label
});
return /** @type {Error & { stack: string }} */ (error);
}
/**

@ -29,8 +29,7 @@ export function hydratable(key, fn) {
return fn();
}
const val = /** @type {() => T} */ (store.get(key));
return val;
return /** @type {T} */ (store.get(key));
}
/** @param {string} key */

@ -6,7 +6,7 @@ declare global {
interface Window {
__svelte?: {
/** hydratables */
h?: Map<string, () => unknown>;
h?: Map<string, unknown>;
/** unused hydratable keys */
uh?: Set<string>;
};

@ -4,6 +4,7 @@ import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
import { get_infinite_stack } from '../shared/dev.js';
import { set_ssr_context, ssr_context } from './context.js';
import * as e from './errors.js';
import { Renderer } from './renderer.js';
@ -98,3 +99,36 @@ export function validate_snippet_args(renderer) {
e.invalid_snippet_arguments();
}
}
/**
* @param {string} label
* @returns {Error & { stack: string } | null}
*/
export function get_stack(label) {
return get_infinite_stack(label, (stack) => {
if (!stack) return;
const lines = stack.split('\n');
const new_lines = [];
for (const line of lines) {
const posixified = line.replaceAll('\\', '/');
if (line.startsWith('Error:')) {
continue;
}
if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) {
continue;
}
new_lines.push(line);
}
if (new_lines.length === 1) {
return undefined;
}
return new_lines.join('\n');
});
}

@ -1,8 +1,10 @@
/** @import { HydratableContext } from '#server' */
/** @import { HydratableContext, 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 { uneval } from 'devalue';
import { get_stack } from './dev.js';
import { DEV } from 'esm-env';
/**
* @template T
@ -17,16 +19,23 @@ export function hydratable(key, fn) {
const store = get_render_context();
const entry = store.hydratable.lookup.get(key);
if (entry !== undefined) {
return /** @type {T} */ (entry.value);
const existing_entry = store.hydratable.lookup.get(key);
if (existing_entry !== undefined) {
return /** @type {T} */ (existing_entry.value);
}
const result = fn();
store.hydratable.lookup.set(key, {
/** @type {HydratableLookupEntry} */
const entry = {
value: result,
root_index: encode(result, key, store.hydratable)
});
};
if (DEV) {
entry.stack = get_stack(`hydratable"`)?.stack;
}
store.hydratable.lookup.set(key, entry);
return result;
}
@ -50,15 +59,17 @@ function encode(value, key, hydratable_context) {
function create_replacer(key, hydratable_context) {
/**
* @param {unknown} value
* @param {(value: any) => string} inner_uneval
*/
const replacer = (value, inner_uneval) => {
const replacer = (value) => {
if (value instanceof Promise) {
hydratable_context.unresolved_promises.set(value, key);
value.finally(() => hydratable_context.unresolved_promises.delete(value));
// use the root-level uneval because we need a separate, top-level entry for each promise
const index =
hydratable_context.values.push(value.then((v) => `r(${uneval(v, replacer)})`)) - 1;
/** @type {Promise<string>} */
const serialize_promise = value.then((v) => `r(${uneval(v, replacer)})`);
hydratable_context.unresolved_promises.set(serialize_promise, key);
serialize_promise.finally(() =>
hydratable_context.unresolved_promises.delete(serialize_promise)
);
const index = hydratable_context.values.push(serialize_promise) - 1;
return `d(${index})`;
}
};

@ -8,9 +8,9 @@ import * as e from './errors.js';
import * as w from './warnings.js';
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { attributes } from './index.js';
import { uneval } from 'devalue';
import { get_render_context, with_render_context, init_render_context } from './render-context.js';
import { DEV } from 'esm-env';
import { get_stack } from './dev.js';
/** @typedef {'head' | 'body'} RendererType */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
@ -578,32 +578,16 @@ export class Renderer {
async #collect_hydratables() {
const ctx = get_render_context().hydratable;
// for (const [k, v] of ctx.lookup) {
// // TODO - root-level
// // if (ctx.unresolved_promises.has(/** @type {Promise<unknown>} */ (v.value))) {
// // // this is a problem -- it means we've finished the render but somehow not consumed a hydratable, which means we've done
// // // extra work that will get serialized and sent but then not used on the client
// // w.unused_hydratable(k, v.stack ?? 'unavailable');
// // unused_keys.push(k);
// // continue;
// // }
// // TODO - nested
// // const encoded = encode(await v.value);
// // if (DEV && v.dev_competing_entries?.length) {
// // for (const competing_entry of v.dev_competing_entries) {
// // const competing_encoded = (competing_entry.encode ?? uneval)(await competing_entry.value);
// // if (encoded !== competing_encoded) {
// // e.hydratable_clobbering(
// // k,
// // v.stack ?? 'unavailable',
// // competing_entry.stack ?? 'unavailable'
// // );
// // }
// // }
// // }
// }
for (const [promise, key] of ctx.unresolved_promises) {
// this is a problem -- it means we've finished the render but we're still waiting on a promise to resolve so we can
// serialize it, so we're blocking the response on useless content.
w.unresolved_hydratable(
key,
DEV ? ctx.lookup.get(key)?.stack ?? 'unavailable' : 'unavailable in production builds',
await promise
);
}
return await Renderer.#hydratable_block(ctx, []);
}

@ -18,12 +18,13 @@ export interface SSRContext {
export interface HydratableLookupEntry {
value: unknown;
root_index: number;
stack?: string;
}
export interface HydratableContext {
lookup: Map<string, HydratableLookupEntry>;
values: MaybePromise<string>[];
unresolved_promises: Map<Promise<unknown>, string>;
unresolved_promises: Map<Promise<string>, string>;
}
export interface RenderContext {

@ -6,24 +6,31 @@ var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* A `hydratable` value with key `%key%` was created, but not used during the render.
* A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
*
* Stack:
* The `hydratable` was initialized in:
* %stack%
*
* The unresolved data is:
* %unresolved_data%
* @param {string} key
* @param {string} stack
* @param {string} unresolved_data
*/
export function unused_hydratable(key, stack) {
export function unresolved_hydratable(key, stack, unresolved_data) {
if (DEV) {
console.warn(
`%c[svelte] unused_hydratable\n%cA \`hydratable\` value with key \`${key}\` was created, but not used during the render.
`%c[svelte] unresolved_hydratable\n%cA \`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}
Stack:
${stack}\nhttps://svelte.dev/e/unused_hydratable`,
The unresolved data is:
${unresolved_data}\nhttps://svelte.dev/e/unresolved_hydratable`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/unused_hydratable`);
console.warn(`https://svelte.dev/e/unresolved_hydratable`);
}
}

@ -0,0 +1,26 @@
import { define_property } from './utils';
/**
* @param {string} label
* @param {(stack: string | undefined) => string | undefined} fn
* @returns {Error & { stack: string } | null}
*/
export function get_infinite_stack(label, fn) {
const limit = Error.stackTraceLimit;
Error.stackTraceLimit = Infinity;
let error = Error();
Error.stackTraceLimit = limit;
const stack = fn(error.stack);
if (!stack) return null;
define_property(error, 'stack', {
value: stack
});
define_property(error, 'name', {
value: label
});
return /** @type {Error & { stack: string }} */ (error);
}

@ -86,6 +86,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
}) => void | Promise<void>;
test_ssr?: (args: {
logs: any[];
warnings: any[];
assert: Assert;
variant: 'ssr' | 'async-ssr';
}) => void | Promise<void>;
@ -105,7 +106,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
declare global {
var __svelte:
| {
h?: Map<string, () => unknown>;
h?: Map<string, unknown>;
uh?: Set<string>;
}
| undefined;
@ -266,7 +267,16 @@ async function run_test_variant(
i++;
}
if (str.slice(0, i).includes('logs')) {
let ssr_str = config.test_ssr?.toString() ?? '';
let sn = 0;
let si = 0;
while (si < ssr_str.length) {
if (ssr_str[si] === '(') sn++;
if (ssr_str[si] === ')' && --sn === 0) break;
si++;
}
if (str.slice(0, i).includes('logs') || ssr_str.slice(0, si).includes('logs')) {
// eslint-disable-next-line no-console
console.log = (...args) => {
logs.push(...args);
@ -277,7 +287,11 @@ async function run_test_variant(
manual_hydrate = true;
}
if (str.slice(0, i).includes('warnings') || config.warnings) {
if (
str.slice(0, i).includes('warnings') ||
config.warnings ||
ssr_str.slice(0, si).includes('warnings')
) {
// eslint-disable-next-line no-console
console.warn = (...args) => {
if (typeof args[0] === 'string' && args[0].startsWith('%c[svelte]')) {
@ -397,6 +411,7 @@ async function run_test_variant(
if (config.test_ssr) {
await config.test_ssr({
logs,
warnings,
// @ts-expect-error
assert: {
...assert,

@ -1,21 +0,0 @@
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>');
}
}
});

@ -1,15 +0,0 @@
<script lang="ts">
import { hydratable } from "svelte";
let { environment }: { environment: 'server' | 'browser' } = $props();
const value = hydratable("environment", () => environment, {
transport: environment === 'server' ? {
encode: (val: string) => JSON.stringify([val])
} : {
decode: (val: [string]) => val[0]
}
})
</script>
<p>The current environment is: {value}</p>

@ -9,5 +9,5 @@ export default test({
props: { environment: 'browser' },
runtime_error: 'hydratable_missing_but_expected_e'
runtime_error: 'hydratable_missing_but_required'
});

@ -1,5 +1,5 @@
import { tick } from 'svelte';
import { ok, test } from '../../test';
import { test } from '../../test';
export default test({
skip_no_async: true,
@ -8,6 +8,12 @@ export default test({
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 }) {
// let it hydrate and resolve the promise on the client
await tick();

@ -6,7 +6,7 @@
const unresolved_hydratable = hydratable(
"unused_key",
() => new Promise(
(res) => environment === 'server' ? undefined : res('did you ever hear the tragedy of darth plagueis the wise?')
(res, rej) => environment === 'server' ? setTimeout(() => res('did you ever hear the tragedy of darth plagueis the wise?'), 0) : rej('should not run')
)
);
</script>

Loading…
Cancel
Save