svelte
Version:
Cybernetically enhanced web apps
140 lines (125 loc) • 4.12 kB
JavaScript
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import {
block_close,
block_open,
block_open_else,
build_attribute_value,
build_template
} from './shared/utils.js';
/**
* @param {AST.SvelteBoundary} node
* @param {ComponentContext} context
*/
export function SvelteBoundary(node, context) {
// Extract the `failed` snippet/attribute
const failed_snippet = /** @type {AST.SnippetBlock | undefined} */ (
node.fragment.nodes.find(
(node) => node.type === 'SnippetBlock' && node.expression.name === 'failed'
)
);
const failed_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'failed')
);
// Extract the `pending` snippet/attribute
const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending')
);
const is_pending_attr_nullish =
pending_attribute &&
typeof pending_attribute.value === 'object' &&
!Array.isArray(pending_attribute.value) &&
!context.state.scope.evaluate(pending_attribute.value.expression).is_defined;
const pending_snippet = /** @type {AST.SnippetBlock | undefined} */ (
node.fragment.nodes.find(
(node) => node.type === 'SnippetBlock' && node.expression.name === 'pending'
)
);
const children_nodes = node.fragment.nodes.filter(
(child) =>
!(child.type === 'SnippetBlock' && ['failed', 'pending'].includes(child.expression.name))
);
const children_fragment = { ...node.fragment, nodes: children_nodes };
const children_block = /** @type {BlockStatement} */ (
context.visit(children_fragment, {
...context.state,
scope: context.state.scopes.get(node.fragment) ?? context.state.scope
})
);
/** @type {BlockStatement} */
let children_body;
if (pending_attribute || pending_snippet) {
if (pending_attribute && is_pending_attr_nullish && !pending_snippet) {
const { callee, pending_block } = build_pending_attribute_block(pending_attribute, context);
children_body = b.block([
b.if(
callee,
pending_block,
b.block(build_template([block_open, children_block, block_close]))
)
]);
} else {
children_body = pending_attribute
? build_pending_attribute_block(pending_attribute, context).pending_block
: build_pending_snippet_block(/** @type {AST.SnippetBlock} */ (pending_snippet), context);
}
} else {
children_body = b.block(build_template([block_open, children_block, block_close]));
}
// When there's no `failed` snippet/attribute, skip the boundary wrapper entirely
// (saves bytes / more performant at runtime)
if (!failed_snippet && !failed_attribute) {
context.state.template.push(...children_body.body);
return;
}
const props = b.object([]);
if (failed_attribute && !failed_snippet) {
const failed_callee = build_attribute_value(
failed_attribute.value,
context,
(expression) => expression,
false,
true
);
props.properties.push(b.init('failed', failed_callee));
} else if (failed_snippet) {
context.visit(failed_snippet, context.state);
props.properties.push(b.init('failed', failed_snippet.expression));
}
context.state.template.push(
b.stmt(b.call('$$renderer.boundary', props, b.arrow([b.id('$$renderer')], children_body)))
);
}
/**
* @param {AST.Attribute} attribute
* @param {ComponentContext} context
*/
function build_pending_attribute_block(attribute, context) {
const callee = build_attribute_value(
attribute.value,
context,
(expression) => expression,
false,
true
);
const pending = b.call(callee, b.id('$$renderer'));
return {
callee,
pending_block: b.block(build_template([block_open_else, b.stmt(pending), block_close]))
};
}
/**
* @param {AST.SnippetBlock} snippet
* @param {ComponentContext} context
*/
function build_pending_snippet_block(snippet, context) {
return b.block(
build_template([
block_open_else,
/** @type {BlockStatement} */ (context.visit(snippet.body)),
block_close
])
);
}