diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 0d54feee11b3..84c368e0791a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -1,17 +1,112 @@ -/** @import { BlockStatement } from 'estree' */ +/** @import { BlockStatement, Statement, Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ -import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js'; import * as b from '../../../../utils/builders.js'; +import { extract_identifiers } from '../../../../utils/ast.js'; /** * @param {AST.SvelteBoundary} node * @param {ComponentContext} context */ export function SvelteBoundary(node, context) { - context.state.template.push( - b.literal(BLOCK_OPEN), - /** @type {BlockStatement} */ (context.visit(node.fragment)), - b.literal(BLOCK_CLOSE) - ); + /** @type {Statement[]} */ + const statements = []; + + /** @type {AST.SnippetBlock | null} */ + let snippet = null; + + /** @type {AST.ConstTag[]} */ + let const_tags = []; + + const nodes = []; + + const payload = b.id('$$payload'); // correct ? + + /** @type {Expression | undefined} */ + let failed; + + // Capture the `failed` explicit snippet prop + for (const attribute of node.attributes) { + if (attribute.type === 'Attribute' && attribute.name === 'failed' && attribute.value !== true) { + const chunk = Array.isArray(attribute.value) + ? /** @type {AST.ExpressionTag} */ (attribute.value[0]) + : attribute.value; + failed = /** @type {Expression} */ (context.visit(chunk.expression, context.state)); + } + } + + // Capture the `failed` implicit snippet prop + for (const child of node.fragment.nodes) { + if (child.type === 'SnippetBlock' && child.expression.name === 'failed') { + snippet = child; + + /** @type {Statement[]} */ + const init = []; + context.visit(snippet, { ...context.state, init }); + + if (init.length === 1 && init[0].type === 'FunctionDeclaration') { + failed = b.arrow(init[0].params, init[0].body); + } else { + statements.push(...init); + failed = b.id('failed'); + } + } else if (child.type === 'ConstTag') { + const_tags.push(child); + } else { + nodes.push(child); + } + } + + let max_referenced_const_tag = -1; + + if (snippet) { + const references = context.state.scopes.get(snippet)?.references; + if (references != null && references.size) { + const keys = new Set(references.keys()); + + const_tags.forEach((tag, index) => { + if (has_reference(keys, tag)) { + max_referenced_const_tag = index + 1; + } + }); + } + } + + if (max_referenced_const_tag < 0) { + nodes.unshift(...const_tags); + } else if (max_referenced_const_tag === const_tags.length) { + const_tags.forEach((tag) => context.visit(tag, { ...context.state, init: statements })); + } else { + const_tags + .slice(0, max_referenced_const_tag) + .forEach((tag) => context.visit(tag, { ...context.state, init: statements })); + nodes.unshift(...const_tags.slice(max_referenced_const_tag)); + } + + const body_block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); + + const body = b.arrow([b.id('$$payload')], body_block); + + statements.push(b.stmt(b.call('$.boundary', payload, body, failed))); + + if (statements.length === 1) { + context.state.template.push(statements[0]); + } else { + context.state.template.push(b.block([...statements])); + } +} + +/** + * @param {Set} keys + * @param {AST.ConstTag} tag + */ +function has_reference(keys, tag) { + for (const declaration of tag.declaration.declarations) { + for (const id of extract_identifiers(declaration.id)) { + if (keys.has(id.name)) { + return true; + } + } + } + return false; } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c1ca7a960034..cc4b9424def1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,5 +1,6 @@ /** @import { Effect, TemplateNode, } from '#client' */ +import { HYDRATION_START } from '../../../../constants.js'; import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; import { component_context, set_component_context } from '../../context.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; @@ -17,7 +18,8 @@ import { hydrating, next, remove_nodes, - set_hydrate_node + set_hydrate_node, + set_hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; @@ -119,12 +121,27 @@ export function boundary(node, props, boundary_fn) { } }; + let mismatch = false; + if (hydrating) { + const data = /** @type {Comment} */ (anchor).data; hydrate_next(); + if (data !== HYDRATION_START) { + anchor = remove_nodes(); + + set_hydrate_node(anchor); + set_hydrating(false); + mismatch = true; + } } boundary_effect = branch(() => boundary_fn(anchor)); reset_is_throwing_error(); + + if (mismatch) { + // continue in hydration mode + set_hydrating(true); + } }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); if (hydrating) { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 6098b496c5ac..eeb21af37c46 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -13,7 +13,7 @@ import { import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; -import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; +import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js'; import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; @@ -535,7 +535,28 @@ export function props_id(payload) { return uid; } -export { attr, clsx }; +/** + * for server-side + * @param {Payload} payload + * @param {(payload:Payload) => void} body + * @param {(payload:Payload, err: any) => void} [failed] + * @returns {void} + */ +export function boundary(payload, body, failed) { + var inner_payload = copy_payload(payload); + try { + inner_payload.out += BLOCK_OPEN; + body(inner_payload); + } catch (err) { + inner_payload = copy_payload(payload); + inner_payload.out += BLOCK_OPEN_ELSE; + failed?.(inner_payload, err); + } + inner_payload.out += BLOCK_CLOSE; + assign_payload(payload, inner_payload); +} + +export { attr, clsx, to_class }; export { html } from './blocks/html.js';