From 2ebb277be70e9333a24f20bae01be2383b434e0e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 May 2024 09:55:26 -0400 Subject: [PATCH] feat: more information when hydration fails (#11649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the case of an invalid child element, we already get information about the parent and the child, but in other cases where a mismatch could occur you're pretty much on your own. This adds a bit more context to hydration_mismatch warnings — 'The error occurred near ...' --- .changeset/violet-mails-trade.md | 5 ++++ .../svelte/messages/client-errors/errors.md | 8 ++----- .../messages/client-warnings/warnings.md | 2 ++ .../svelte/scripts/process-messages/index.js | 1 + packages/svelte/src/constants.js | 1 + .../src/internal/client/dom/hydration.js | 18 +++++++++++--- packages/svelte/src/internal/client/errors.js | 24 ++++--------------- packages/svelte/src/internal/client/render.js | 18 +++++++------- .../svelte/src/internal/client/warnings.js | 9 +++---- 9 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 .changeset/violet-mails-trade.md diff --git a/.changeset/violet-mails-trade.md b/.changeset/violet-mails-trade.md new file mode 100644 index 0000000000..d6f2046676 --- /dev/null +++ b/.changeset/violet-mails-trade.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: more information when hydration fails diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 9333d2cfe9..bb8773a3bd 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -36,13 +36,9 @@ > Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops -## hydration_missing_marker_close +## hydration_failed -> Missing hydration closing marker - -## hydration_missing_marker_open - -> Missing hydration opening marker +> Failed to hydrate the application ## lifecycle_legacy_only diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 3131ab98f4..73dba661b3 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -6,6 +6,8 @@ > Hydration failed because the initial UI does not match what was rendered on the server +> Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location% + ## lifecycle_double_unmount > Tried to unmount a component that was not mounted diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js index c45222fa47..b8f9a8f4b5 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -163,6 +163,7 @@ function transform(name, dest) { type: 'Literal', value: text }; + prev_vars = vars; continue; } diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 0e3a1737ab..9f1974f2ce 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -22,6 +22,7 @@ export const TEMPLATE_USE_IMPORT_NODE = 1 << 1; export const HYDRATION_START = '['; export const HYDRATION_END = ']'; export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered +export const HYDRATION_ERROR = {}; export const UNINITIALIZED = Symbol(); diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 1afbbcf874..dee87b31dd 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -1,5 +1,6 @@ -import { HYDRATION_END, HYDRATION_START } from '../../../constants.js'; -import * as e from '../errors.js'; +import { DEV } from 'esm-env'; +import { HYDRATION_END, HYDRATION_START, HYDRATION_ERROR } from '../../../constants.js'; +import * as w from '../warnings.js'; /** * Use this variable to guard everything related to hydration code so it can be treeshaken out @@ -67,5 +68,16 @@ export function hydrate_anchor(node) { nodes.push(current); } - e.hydration_missing_marker_close(); + let location; + + if (DEV) { + // @ts-expect-error + const loc = node.parentNode?.__svelte_meta?.loc; + if (loc) { + location = `${loc.file}:${loc.line}:${loc.column}`; + } + } + + w.hydration_mismatch(location); + throw HYDRATION_ERROR; } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 76b715d013..2ead3963c5 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -161,34 +161,18 @@ export function effect_update_depth_exceeded() { } /** - * Missing hydration closing marker + * Failed to hydrate the application * @returns {never} */ -export function hydration_missing_marker_close() { +export function hydration_failed() { if (DEV) { - const error = new Error(`${"hydration_missing_marker_close"}\n${"Missing hydration closing marker"}`); + const error = new Error(`${"hydration_failed"}\n${"Failed to hydrate the application"}`); error.name = 'Svelte error'; throw error; } else { // TODO print a link to the documentation - throw new Error("hydration_missing_marker_close"); - } -} - -/** - * Missing hydration opening marker - * @returns {never} - */ -export function hydration_missing_marker_open() { - if (DEV) { - const error = new Error(`${"hydration_missing_marker_open"}\n${"Missing hydration opening marker"}`); - - error.name = 'Svelte error'; - throw error; - } else { - // TODO print a link to the documentation - throw new Error("hydration_missing_marker_open"); + throw new Error("hydration_failed"); } } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index ee9cfbfc30..3e2edfbea4 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -6,7 +6,7 @@ import { empty, init_operations } from './dom/operations.js'; -import { HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js'; +import { HYDRATION_ERROR, HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js'; import { flush_sync, push, pop, current_component_context } from './runtime.js'; import { effect_root, branch } from './reactivity/effects.js'; import { @@ -153,7 +153,7 @@ export function hydrate(component, options) { } if (!node) { - e.hydration_missing_marker_open(); + throw HYDRATION_ERROR; } const anchor = hydrate_anchor(node); @@ -167,12 +167,10 @@ export function hydrate(component, options) { return instance; }, false); } catch (error) { - if ( - !hydrated && - options.recover !== false && - /** @type {Error} */ (error).message.includes('hydration_missing_marker_close') - ) { - w.hydration_mismatch(); + if (error === HYDRATION_ERROR) { + if (options.recover === false) { + e.hydration_failed(); + } // If an error occured above, the operations might not yet have been initialised. init_operations(); @@ -180,9 +178,9 @@ export function hydrate(component, options) { set_hydrating(false); return mount(component, options); - } else { - throw error; } + + throw error; } finally { set_hydrating(!!previous_hydrate_nodes); set_hydrate_nodes(previous_hydrate_nodes); diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index b28b7d2059..e5befc8474 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -21,11 +21,12 @@ export function hydration_attribute_changed(attribute, html, value) { } /** - * Hydration failed because the initial UI does not match what was rendered on the server + * Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location% + * @param {string | undefined | null} [location] */ -export function hydration_mismatch() { +export function hydration_mismatch(location) { if (DEV) { - console.warn(`%c[svelte] ${"hydration_mismatch"}\n%c${"Hydration failed because the initial UI does not match what was rendered on the server"}`, bold, normal); + console.warn(`%c[svelte] ${"hydration_mismatch"}\n%c${location ? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}` : "Hydration failed because the initial UI does not match what was rendered on the server"}`, bold, normal); } else { // TODO print a link to the documentation console.warn("hydration_mismatch"); @@ -66,7 +67,7 @@ export function ownership_invalid_binding(parent, child, owner) { */ export function ownership_invalid_mutation(component, owner) { if (DEV) { - console.warn(`%c[svelte] ${"ownership_invalid_mutation"}\n%c${`${component} mutated a value owned by ${owner}. This is strongly discouraged. Consider passing values to child components with \`bind:\`, or use a callback instead`}`, bold, normal); + console.warn(`%c[svelte] ${"ownership_invalid_mutation"}\n%c${component ? `${component} mutated a value owned by ${owner}. This is strongly discouraged. Consider passing values to child components with \`bind:\`, or use a callback instead` : "Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead"}`, bold, normal); } else { // TODO print a link to the documentation console.warn("ownership_invalid_mutation");