mirror of https://github.com/sveltejs/svelte
chore: modularize server code (#12596)
* start modularizing server code * more * more * more * more * alphabetize * start on JS visitors * more * more * more * more * less * more * alphabetize * lint * combine into single visitors folder * alphabetizepull/12605/head
parent
6223a7e87e
commit
75ea6da9cd
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
|
||||
/** @import { AssignmentExpression } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import { serialize_set_binding } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AssignmentExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function AssignmentExpression(node, context) {
|
||||
return serialize_set_binding(node, context, context.next);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
|
||||
/** @import { AwaitBlock } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { empty_comment } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AwaitBlock} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function AwaitBlock(node, context) {
|
||||
context.state.template.push(
|
||||
empty_comment,
|
||||
b.stmt(
|
||||
b.call(
|
||||
'$.await',
|
||||
/** @type {Expression} */ (context.visit(node.expression)),
|
||||
b.thunk(
|
||||
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
|
||||
),
|
||||
b.arrow(
|
||||
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
|
||||
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
|
||||
),
|
||||
b.arrow(
|
||||
node.error ? [/** @type {Pattern} */ (context.visit(node.error))] : [],
|
||||
node.catch ? /** @type {BlockStatement} */ (context.visit(node.catch)) : b.block([])
|
||||
)
|
||||
)
|
||||
),
|
||||
empty_comment
|
||||
);
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/** @import { CallExpression, Expression } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import { get_rune } from '../../../scope.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { transform_inspect_rune } from '../../utils.js';
|
||||
|
||||
/**
|
||||
* @param {CallExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function CallExpression(node, context) {
|
||||
const rune = get_rune(node, context.state.scope);
|
||||
|
||||
if (rune === '$host') {
|
||||
return b.id('undefined');
|
||||
}
|
||||
|
||||
if (rune === '$effect.tracking') {
|
||||
return b.literal(false);
|
||||
}
|
||||
|
||||
if (rune === '$effect.root') {
|
||||
// ignore $effect.root() calls, just return a noop which mimics the cleanup function
|
||||
return b.arrow([], b.block([]));
|
||||
}
|
||||
|
||||
if (rune === '$state.snapshot') {
|
||||
return b.call('$.snapshot', /** @type {Expression} */ (context.visit(node.arguments[0])));
|
||||
}
|
||||
|
||||
if (rune === '$state.is') {
|
||||
return b.call(
|
||||
'Object.is',
|
||||
/** @type {Expression} */ (context.visit(node.arguments[0])),
|
||||
/** @type {Expression} */ (context.visit(node.arguments[1]))
|
||||
);
|
||||
}
|
||||
|
||||
if (rune === '$inspect' || rune === '$inspect().with') {
|
||||
return transform_inspect_rune(node, context);
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/** @import { ClassBody, Expression, MethodDefinition, PropertyDefinition } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
/** @import { StateField } from '../../client/types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { get_rune } from '../../../scope.js';
|
||||
|
||||
/**
|
||||
* @param {ClassBody} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ClassBodyRunes(node, context) {
|
||||
/** @type {Map<string, StateField>} */
|
||||
const public_derived = new Map();
|
||||
|
||||
/** @type {Map<string, StateField>} */
|
||||
const private_derived = new Map();
|
||||
|
||||
/** @type {string[]} */
|
||||
const private_ids = [];
|
||||
|
||||
for (const definition of node.body) {
|
||||
if (
|
||||
definition.type === 'PropertyDefinition' &&
|
||||
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
|
||||
) {
|
||||
const { type, name } = definition.key;
|
||||
|
||||
const is_private = type === 'PrivateIdentifier';
|
||||
if (is_private) private_ids.push(name);
|
||||
|
||||
if (definition.value?.type === 'CallExpression') {
|
||||
const rune = get_rune(definition.value, context.state.scope);
|
||||
if (rune === '$derived' || rune === '$derived.by') {
|
||||
/** @type {StateField} */
|
||||
const field = {
|
||||
kind: rune === '$derived.by' ? 'derived_call' : 'derived',
|
||||
// @ts-expect-error this is set in the next pass
|
||||
id: is_private ? definition.key : null
|
||||
};
|
||||
|
||||
if (is_private) {
|
||||
private_derived.set(name, field);
|
||||
} else {
|
||||
public_derived.set(name, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// each `foo = $derived()` needs a backing `#foo` field
|
||||
for (const [name, field] of public_derived) {
|
||||
let deconflicted = name;
|
||||
while (private_ids.includes(deconflicted)) {
|
||||
deconflicted = '_' + deconflicted;
|
||||
}
|
||||
|
||||
private_ids.push(deconflicted);
|
||||
field.id = b.private_id(deconflicted);
|
||||
}
|
||||
|
||||
/** @type {Array<MethodDefinition | PropertyDefinition>} */
|
||||
const body = [];
|
||||
|
||||
const child_state = { ...context.state, private_derived };
|
||||
|
||||
// Replace parts of the class body
|
||||
for (const definition of node.body) {
|
||||
if (
|
||||
definition.type === 'PropertyDefinition' &&
|
||||
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
|
||||
) {
|
||||
const name = definition.key.name;
|
||||
|
||||
const is_private = definition.key.type === 'PrivateIdentifier';
|
||||
const field = (is_private ? private_derived : public_derived).get(name);
|
||||
|
||||
if (definition.value?.type === 'CallExpression' && field !== undefined) {
|
||||
const init = /** @type {Expression} **/ (
|
||||
context.visit(definition.value.arguments[0], child_state)
|
||||
);
|
||||
const value =
|
||||
field.kind === 'derived_call' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
|
||||
|
||||
if (is_private) {
|
||||
body.push(b.prop_def(field.id, value));
|
||||
} else {
|
||||
// #foo;
|
||||
const member = b.member(b.this, field.id);
|
||||
body.push(b.prop_def(field.id, value));
|
||||
|
||||
// get foo() { return this.#foo; }
|
||||
body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
|
||||
|
||||
if (
|
||||
(field.kind === 'derived' || field.kind === 'derived_call') &&
|
||||
context.state.options.dev
|
||||
) {
|
||||
body.push(
|
||||
b.method(
|
||||
'set',
|
||||
definition.key,
|
||||
[b.id('_')],
|
||||
[b.throw_error(`Cannot update a derived property ('${name}')`)]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
|
||||
}
|
||||
|
||||
return { ...node, body };
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
/** @import { Component } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { serialize_inline_component } from './shared/component.js';
|
||||
|
||||
/**
|
||||
* @param {Component} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function Component(node, context) {
|
||||
serialize_inline_component(node, b.id(node.name), context);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/** @import { Expression, Pattern } from 'estree' */
|
||||
/** @import { ConstTag } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {ConstTag} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function ConstTag(node, context) {
|
||||
const declaration = node.declaration.declarations[0];
|
||||
const id = /** @type {Pattern} */ (context.visit(declaration.id));
|
||||
const init = /** @type {Expression} */ (context.visit(declaration.init));
|
||||
|
||||
context.state.init.push(b.const(id, init));
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { DebugTag } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {DebugTag} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function DebugTag(node, context) {
|
||||
context.state.template.push(
|
||||
b.stmt(
|
||||
b.call(
|
||||
'console.log',
|
||||
b.object(
|
||||
node.identifiers.map((identifier) =>
|
||||
b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier)))
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
b.debugger
|
||||
);
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
|
||||
/** @import { EachBlock } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { block_close, block_open } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {EachBlock} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function EachBlock(node, context) {
|
||||
const state = context.state;
|
||||
|
||||
const each_node_meta = node.metadata;
|
||||
const collection = /** @type {Expression} */ (context.visit(node.expression));
|
||||
const item = each_node_meta.item;
|
||||
const index =
|
||||
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
|
||||
|
||||
const array_id = state.scope.root.unique('each_array');
|
||||
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
|
||||
|
||||
/** @type {Statement[]} */
|
||||
const each = [b.const(item, b.member(array_id, index, true))];
|
||||
|
||||
if (node.context.type !== 'Identifier') {
|
||||
each.push(b.const(/** @type {Pattern} */ (node.context), item));
|
||||
}
|
||||
if (index.name !== node.index && node.index != null) {
|
||||
each.push(b.let(node.index, index));
|
||||
}
|
||||
|
||||
each.push(.../** @type {BlockStatement} */ (context.visit(node.body)).body);
|
||||
|
||||
const for_loop = b.for(
|
||||
b.let(index, b.literal(0)),
|
||||
b.binary('<', index, b.member(array_id, b.id('length'))),
|
||||
b.update('++', index, false),
|
||||
b.block(each)
|
||||
);
|
||||
|
||||
if (node.fallback) {
|
||||
const open = b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open));
|
||||
|
||||
const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
|
||||
|
||||
fallback.body.unshift(
|
||||
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
|
||||
);
|
||||
|
||||
state.template.push(
|
||||
b.if(
|
||||
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
|
||||
b.block([open, for_loop]),
|
||||
fallback
|
||||
),
|
||||
block_close
|
||||
);
|
||||
} else {
|
||||
state.template.push(block_open, for_loop, block_close);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/** @import { ExpressionStatement } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {ExpressionStatement} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function ExpressionStatementRunes(node, context) {
|
||||
const expression = node.expression;
|
||||
|
||||
if (expression.type === 'CallExpression') {
|
||||
const callee = expression.callee;
|
||||
|
||||
if (callee.type === 'Identifier' && callee.name === '$effect') {
|
||||
return b.empty;
|
||||
}
|
||||
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === '$effect'
|
||||
) {
|
||||
return b.empty;
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/** @import { Fragment } from '#compiler' */
|
||||
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
|
||||
import { clean_nodes, infer_namespace } from '../../utils.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { empty_comment, process_children, serialize_template } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {Fragment} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function Fragment(node, context) {
|
||||
const parent = context.path.at(-1) ?? node;
|
||||
const namespace = infer_namespace(context.state.namespace, parent, node.nodes);
|
||||
|
||||
const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes(
|
||||
parent,
|
||||
node.nodes,
|
||||
context.path,
|
||||
namespace,
|
||||
context.state,
|
||||
context.state.preserve_whitespace,
|
||||
context.state.options.preserveComments
|
||||
);
|
||||
|
||||
/** @type {ComponentServerTransformState} */
|
||||
const state = {
|
||||
...context.state,
|
||||
init: [],
|
||||
template: [],
|
||||
namespace,
|
||||
skip_hydration_boundaries: is_standalone
|
||||
};
|
||||
|
||||
for (const node of hoisted) {
|
||||
context.visit(node, state);
|
||||
}
|
||||
|
||||
if (is_text_first) {
|
||||
// insert `<!---->` to prevent this from being glued to the previous fragment
|
||||
state.template.push(empty_comment);
|
||||
}
|
||||
|
||||
process_children(trimmed, { ...context, state });
|
||||
|
||||
return b.block([...state.init, ...serialize_template(state.template)]);
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { HtmlTag } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {HtmlTag} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function HtmlTag(node, context) {
|
||||
const expression = /** @type {Expression} */ (context.visit(node.expression));
|
||||
context.state.template.push(b.call('$.html', expression));
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/** @import { Identifier, Node } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import is_reference from 'is-reference';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { serialize_get_binding } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {Identifier} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function Identifier(node, context) {
|
||||
if (is_reference(node, /** @type {Node} */ (context.path.at(-1)))) {
|
||||
if (node.name === '$$props') {
|
||||
return b.id('$$sanitized_props');
|
||||
}
|
||||
|
||||
return serialize_get_binding(node, context.state);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/** @import { BlockStatement, Expression } from 'estree' */
|
||||
/** @import { IfBlock } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { block_close, block_open } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {IfBlock} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function IfBlock(node, context) {
|
||||
const test = /** @type {Expression} */ (context.visit(node.test));
|
||||
|
||||
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
|
||||
|
||||
const alternate = node.alternate
|
||||
? /** @type {BlockStatement} */ (context.visit(node.alternate))
|
||||
: b.block([]);
|
||||
|
||||
consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
|
||||
|
||||
alternate.body.unshift(
|
||||
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
|
||||
);
|
||||
|
||||
context.state.template.push(b.if(test, consequent, alternate), block_close);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/** @import { BlockStatement } from 'estree' */
|
||||
/** @import { KeyBlock } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import { empty_comment } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {KeyBlock} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function KeyBlock(node, context) {
|
||||
context.state.template.push(
|
||||
empty_comment,
|
||||
/** @type {BlockStatement} */ (context.visit(node.fragment)),
|
||||
empty_comment
|
||||
);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/** @import { ExpressionStatement, LabeledStatement } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {LabeledStatement} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function LabeledStatementLegacy(node, context) {
|
||||
if (context.path.length > 1) return;
|
||||
if (node.label.name !== '$') return;
|
||||
|
||||
// TODO bail out if we're in module context
|
||||
|
||||
// these statements will be topologically ordered later
|
||||
context.state.legacy_reactive_statements.set(
|
||||
node,
|
||||
// people could do "break $" inside, so we need to keep the label
|
||||
b.labeled('$', /** @type {ExpressionStatement} */ (context.visit(node.body)))
|
||||
);
|
||||
|
||||
return b.empty;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/** @import { LetDirective } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {LetDirective} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function LetDirective(node, context) {
|
||||
if (node.expression === null || node.expression.type === 'Identifier') {
|
||||
const name = node.expression === null ? node.name : node.expression.name;
|
||||
return b.const(name, b.member(b.id('$$slotProps'), b.id(node.name)));
|
||||
}
|
||||
|
||||
const name = context.state.scope.generate(node.name);
|
||||
const bindings = context.state.scope.get_bindings(node);
|
||||
|
||||
for (const binding of bindings) {
|
||||
context.state.getters[binding.node.name] = b.member(b.id(name), b.id(binding.node.name));
|
||||
}
|
||||
|
||||
return b.const(
|
||||
name,
|
||||
b.call(
|
||||
b.thunk(
|
||||
b.block([
|
||||
b.let(
|
||||
node.expression.type === 'ObjectExpression'
|
||||
? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
|
||||
b.object_pattern(node.expression.properties)
|
||||
: // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
|
||||
b.array_pattern(node.expression.elements),
|
||||
b.member(b.id('$$slotProps'), b.id(node.name))
|
||||
),
|
||||
b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/** @import { MemberExpression } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {MemberExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function MemberExpressionRunes(node, context) {
|
||||
if (node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier') {
|
||||
const field = context.state.private_derived.get(node.property.name);
|
||||
|
||||
if (field) {
|
||||
return b.call(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/** @import { Expression, PropertyDefinition } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { get_rune } from '../../../scope.js';
|
||||
|
||||
/**
|
||||
* @param {PropertyDefinition} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function PropertyDefinitionRunes(node, context) {
|
||||
if (node.value != null && node.value.type === 'CallExpression') {
|
||||
const rune = get_rune(node.value, context.state.scope);
|
||||
|
||||
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
|
||||
return {
|
||||
...node,
|
||||
value:
|
||||
node.value.arguments.length === 0
|
||||
? null
|
||||
: /** @type {Expression} */ (context.visit(node.value.arguments[0]))
|
||||
};
|
||||
}
|
||||
|
||||
if (rune === '$derived.by') {
|
||||
return {
|
||||
...node,
|
||||
value:
|
||||
node.value.arguments.length === 0
|
||||
? null
|
||||
: b.call(/** @type {Expression} */ (context.visit(node.value.arguments[0])))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/** @import { Location } from 'locate-character' */
|
||||
/** @import { RegularElement, Text } from '#compiler' */
|
||||
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
|
||||
/** @import { Scope } from '../../../scope.js' */
|
||||
import { locator } from '../../../../state.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { VoidElements } from '../../../constants.js';
|
||||
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
|
||||
import { serialize_element_attributes } from './shared/element.js';
|
||||
import { process_children, serialize_template } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {RegularElement} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function RegularElement(node, context) {
|
||||
const namespace = determine_namespace_for_children(node, context.state.namespace);
|
||||
|
||||
/** @type {ComponentServerTransformState} */
|
||||
const state = {
|
||||
...context.state,
|
||||
getters: { ...context.state.getters },
|
||||
namespace,
|
||||
preserve_whitespace:
|
||||
context.state.preserve_whitespace ||
|
||||
((node.name === 'pre' || node.name === 'textarea') && namespace !== 'foreign')
|
||||
};
|
||||
|
||||
context.state.template.push(b.literal(`<${node.name}`));
|
||||
const body = serialize_element_attributes(node, { ...context, state });
|
||||
context.state.template.push(b.literal('>'));
|
||||
|
||||
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
|
||||
context.state.template.push(
|
||||
b.literal(/** @type {Text} */ (node.fragment.nodes[0]).data),
|
||||
b.literal(`</${node.name}>`)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { hoisted, trimmed } = clean_nodes(
|
||||
node,
|
||||
node.fragment.nodes,
|
||||
context.path,
|
||||
namespace,
|
||||
{
|
||||
...state,
|
||||
scope: /** @type {Scope} */ (state.scopes.get(node.fragment))
|
||||
},
|
||||
state.preserve_whitespace,
|
||||
state.options.preserveComments
|
||||
);
|
||||
|
||||
for (const node of hoisted) {
|
||||
context.visit(node, state);
|
||||
}
|
||||
|
||||
if (state.options.dev) {
|
||||
const location = /** @type {Location} */ (locator(node.start));
|
||||
state.template.push(
|
||||
b.stmt(
|
||||
b.call(
|
||||
'$.push_element',
|
||||
b.id('$$payload'),
|
||||
b.literal(node.name),
|
||||
b.literal(location.line),
|
||||
b.literal(location.column)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (body === null) {
|
||||
process_children(trimmed, { ...context, state });
|
||||
} else {
|
||||
let id = body;
|
||||
|
||||
if (body.type !== 'Identifier') {
|
||||
id = b.id(state.scope.generate('$$body'));
|
||||
state.template.push(b.const(id, body));
|
||||
}
|
||||
|
||||
// if this is a `<textarea>` value or a contenteditable binding, we only add
|
||||
// the body if the attribute/binding is falsy
|
||||
const inner_state = { ...state, template: [], init: [] };
|
||||
process_children(trimmed, { ...context, state: inner_state });
|
||||
|
||||
// Use the body expression as the body if it's truthy, otherwise use the inner template
|
||||
state.template.push(
|
||||
b.if(
|
||||
id,
|
||||
b.block(serialize_template([id])),
|
||||
b.block([...inner_state.init, ...serialize_template(inner_state.template)])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!VoidElements.includes(node.name) && namespace !== 'foreign') {
|
||||
state.template.push(b.literal(`</${node.name}>`));
|
||||
}
|
||||
|
||||
if (state.options.dev) {
|
||||
state.template.push(b.stmt(b.call('$.pop_element')));
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { RenderTag } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import { unwrap_optional } from '../../../../utils/ast.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { empty_comment } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {RenderTag} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function RenderTag(node, context) {
|
||||
const callee = unwrap_optional(node.expression).callee;
|
||||
const raw_args = unwrap_optional(node.expression).arguments;
|
||||
|
||||
const snippet_function = /** @type {Expression} */ (context.visit(callee));
|
||||
|
||||
const snippet_args = raw_args.map((arg) => {
|
||||
return /** @type {Expression} */ (context.visit(arg));
|
||||
});
|
||||
|
||||
context.state.template.push(
|
||||
b.stmt(
|
||||
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
|
||||
snippet_function,
|
||||
b.id('$$payload'),
|
||||
...snippet_args
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (!context.state.skip_hydration_boundaries) {
|
||||
context.state.template.push(empty_comment);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/** @import { BlockStatement, Expression, ExpressionStatement, Property } from 'estree' */
|
||||
/** @import { SlotElement } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { empty_comment, serialize_attribute_value } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {SlotElement} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SlotElement(node, context) {
|
||||
/** @type {Property[]} */
|
||||
const props = [];
|
||||
|
||||
/** @type {Expression[]} */
|
||||
const spreads = [];
|
||||
|
||||
/** @type {ExpressionStatement[]} */
|
||||
const lets = [];
|
||||
|
||||
/** @type {Expression} */
|
||||
let expression = b.call('$.default_slot', b.id('$$props'));
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'SpreadAttribute') {
|
||||
spreads.push(/** @type {Expression} */ (context.visit(attribute)));
|
||||
} else if (attribute.type === 'Attribute') {
|
||||
const value = serialize_attribute_value(attribute.value, context, false, true);
|
||||
|
||||
if (attribute.name === 'name') {
|
||||
expression = b.member(b.member_id('$$props.$$slots'), value, true, true);
|
||||
} else if (attribute.name !== 'slot') {
|
||||
if (attribute.metadata.dynamic) {
|
||||
props.push(b.get(attribute.name, [b.return(value)]));
|
||||
} else {
|
||||
props.push(b.init(attribute.name, value));
|
||||
}
|
||||
}
|
||||
} else if (attribute.type === 'LetDirective') {
|
||||
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
|
||||
}
|
||||
}
|
||||
|
||||
// Let bindings first, they can be used on attributes
|
||||
context.state.init.push(...lets);
|
||||
|
||||
const props_expression =
|
||||
spreads.length === 0
|
||||
? b.object(props)
|
||||
: b.call('$.spread_props', b.array([b.object(props), ...spreads]));
|
||||
|
||||
const fallback =
|
||||
node.fragment.nodes.length === 0
|
||||
? b.literal(null)
|
||||
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
|
||||
|
||||
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
|
||||
|
||||
context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/** @import { BlockStatement } from 'estree' */
|
||||
/** @import { SnippetBlock } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {SnippetBlock} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SnippetBlock(node, context) {
|
||||
const fn = b.function_declaration(
|
||||
node.expression,
|
||||
[b.id('$$payload'), ...node.parameters],
|
||||
/** @type {BlockStatement} */ (context.visit(node.body))
|
||||
);
|
||||
|
||||
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
|
||||
fn.___snippet = true;
|
||||
|
||||
// TODO hoist where possible
|
||||
context.state.init.push(fn);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
/** @import { SpreadAttribute } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types' */
|
||||
|
||||
/**
|
||||
* @param {SpreadAttribute} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SpreadAttribute(node, context) {
|
||||
return context.visit(node.expression);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/** @import { Expression } from 'estree' */
|
||||
/** @import { SvelteComponent } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import { serialize_inline_component } from './shared/component.js';
|
||||
|
||||
/**
|
||||
* @param {SvelteComponent} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SvelteComponent(node, context) {
|
||||
serialize_inline_component(
|
||||
node,
|
||||
/** @type {Expression} */ (context.visit(node.expression)),
|
||||
context
|
||||
);
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/** @import { BlockStatement, Expression } from 'estree' */
|
||||
/** @import { SvelteElement } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { determine_namespace_for_children } from '../../utils.js';
|
||||
import { serialize_element_attributes } from './shared/element.js';
|
||||
import { serialize_template } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {SvelteElement} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SvelteElement(node, context) {
|
||||
let tag = /** @type {Expression} */ (context.visit(node.tag));
|
||||
if (tag.type !== 'Identifier') {
|
||||
const tag_id = context.state.scope.generate('$$tag');
|
||||
context.state.init.push(b.const(tag_id, tag));
|
||||
tag = b.id(tag_id);
|
||||
}
|
||||
|
||||
if (context.state.options.dev) {
|
||||
if (node.fragment.nodes.length > 0) {
|
||||
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', b.thunk(tag))));
|
||||
}
|
||||
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag))));
|
||||
}
|
||||
|
||||
const state = {
|
||||
...context.state,
|
||||
getteres: { ...context.state.getters },
|
||||
namespace: determine_namespace_for_children(node, context.state.namespace),
|
||||
template: [],
|
||||
init: []
|
||||
};
|
||||
|
||||
serialize_element_attributes(node, { ...context, state });
|
||||
|
||||
if (context.state.options.dev) {
|
||||
context.state.template.push(b.stmt(b.call('$.push_element', tag, b.id('$$payload'))));
|
||||
}
|
||||
|
||||
const attributes = b.block([...state.init, ...serialize_template(state.template)]);
|
||||
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
|
||||
|
||||
context.state.template.push(
|
||||
b.stmt(
|
||||
b.call(
|
||||
'$.element',
|
||||
b.id('$$payload'),
|
||||
tag,
|
||||
attributes.body.length > 0 && b.thunk(attributes),
|
||||
children.body.length > 0 && b.thunk(children)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (context.state.options.dev) {
|
||||
context.state.template.push(b.stmt(b.call('$.pop_element')));
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/** @import { BlockStatement, ExpressionStatement } from 'estree' */
|
||||
/** @import { SvelteFragment } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types' */
|
||||
|
||||
/**
|
||||
* @param {SvelteFragment} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SvelteFragment(node, context) {
|
||||
const child_state = {
|
||||
...context.state,
|
||||
getters: { ...context.state.getters }
|
||||
};
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'LetDirective') {
|
||||
context.state.template.push(
|
||||
/** @type {ExpressionStatement} */ (context.visit(attribute, child_state))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const block = /** @type {BlockStatement} */ (context.visit(node.fragment, child_state));
|
||||
|
||||
context.state.template.push(block);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/** @import { BlockStatement } from 'estree' */
|
||||
/** @import { SvelteHead } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {SvelteHead} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SvelteHead(node, context) {
|
||||
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
|
||||
|
||||
context.state.template.push(
|
||||
b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], block)))
|
||||
);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
/** @import { SvelteSelf } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { serialize_inline_component } from './shared/component.js';
|
||||
|
||||
/**
|
||||
* @param {SvelteSelf} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SvelteSelf(node, context) {
|
||||
serialize_inline_component(node, b.id(context.state.analysis.name), context);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/** @import { TitleElement } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { process_children, serialize_template } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {TitleElement} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function TitleElement(node, context) {
|
||||
// title is guaranteed to contain only text/expression tag children
|
||||
const template = [b.literal('<title>')];
|
||||
process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
|
||||
template.push(b.literal('</title>'));
|
||||
|
||||
context.state.init.push(...serialize_template(template, b.id('$$payload.title'), '='));
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/** @import { UpdateExpression } from 'estree' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {UpdateExpression} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function UpdateExpression(node, context) {
|
||||
const argument = node.argument;
|
||||
|
||||
if (
|
||||
argument.type === 'Identifier' &&
|
||||
context.state.scope.get(argument.name)?.kind === 'store_sub'
|
||||
) {
|
||||
return b.call(
|
||||
node.prefix ? '$.update_store_pre' : '$.update_store',
|
||||
b.assignment('??=', b.id('$$store_subs'), b.object([])),
|
||||
b.literal(argument.name),
|
||||
b.id(argument.name.slice(1)),
|
||||
node.operator === '--' && b.literal(-1)
|
||||
);
|
||||
}
|
||||
|
||||
return context.next();
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */
|
||||
/** @import { Binding } from '#compiler' */
|
||||
/** @import { Context } from '../types.js' */
|
||||
/** @import { Scope } from '../../../scope.js' */
|
||||
import { extract_paths, is_expression_async } from '../../../../utils/ast.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { get_rune } from '../../../scope.js';
|
||||
import { walk } from 'zimmerframe';
|
||||
|
||||
/**
|
||||
* @param {VariableDeclaration} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function VariableDeclarationRunes(node, context) {
|
||||
const declarations = [];
|
||||
|
||||
for (const declarator of node.declarations) {
|
||||
const init = declarator.init;
|
||||
const rune = get_rune(init, context.state.scope);
|
||||
if (!rune || rune === '$effect.tracking' || rune === '$inspect' || rune === '$effect.root') {
|
||||
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rune === '$props') {
|
||||
// remove $bindable() from props declaration
|
||||
const id = walk(declarator.id, null, {
|
||||
AssignmentPattern(node) {
|
||||
if (
|
||||
node.right.type === 'CallExpression' &&
|
||||
get_rune(node.right, context.state.scope) === '$bindable'
|
||||
) {
|
||||
const right = node.right.arguments.length
|
||||
? /** @type {Expression} */ (context.visit(node.right.arguments[0]))
|
||||
: b.id('undefined');
|
||||
return b.assignment_pattern(node.left, right);
|
||||
}
|
||||
}
|
||||
});
|
||||
declarations.push(b.declarator(id, b.id('$$props')));
|
||||
continue;
|
||||
}
|
||||
|
||||
const args = /** @type {CallExpression} */ (init).arguments;
|
||||
const value =
|
||||
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
|
||||
|
||||
if (rune === '$derived.by') {
|
||||
declarations.push(
|
||||
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (declarator.id.type === 'Identifier') {
|
||||
declarations.push(b.declarator(declarator.id, value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rune === '$derived') {
|
||||
declarations.push(b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value));
|
||||
continue;
|
||||
}
|
||||
|
||||
declarations.push(...create_state_declarators(declarator, context.state.scope, value));
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
declarations
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {VariableDeclaration} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function VariableDeclarationLegacy(node, { state, visit }) {
|
||||
/** @type {VariableDeclarator[]} */
|
||||
const declarations = [];
|
||||
|
||||
for (const declarator of node.declarations) {
|
||||
const bindings = /** @type {Binding[]} */ (state.scope.get_bindings(declarator));
|
||||
const has_state = bindings.some((binding) => binding.kind === 'state');
|
||||
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
|
||||
|
||||
if (!has_state && !has_props) {
|
||||
declarations.push(/** @type {VariableDeclarator} */ (visit(declarator)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (has_props) {
|
||||
if (declarator.id.type !== 'Identifier') {
|
||||
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
|
||||
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
|
||||
const tmp = state.scope.generate('tmp');
|
||||
const paths = extract_paths(declarator.id);
|
||||
declarations.push(
|
||||
b.declarator(
|
||||
b.id(tmp),
|
||||
/** @type {Expression} */ (visit(/** @type {Expression} */ (declarator.init)))
|
||||
)
|
||||
);
|
||||
for (const path of paths) {
|
||||
const value = path.expression?.(b.id(tmp));
|
||||
const name = /** @type {Identifier} */ (path.node).name;
|
||||
const binding = /** @type {Binding} */ (state.scope.get(name));
|
||||
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
|
||||
declarations.push(
|
||||
b.declarator(path.node, b.call('$.value_or_fallback', prop, b.thunk(value)))
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const binding = /** @type {Binding} */ (state.scope.get(declarator.id.name));
|
||||
const prop = b.member(
|
||||
b.id('$$props'),
|
||||
b.literal(binding.prop_alias ?? declarator.id.name),
|
||||
true
|
||||
);
|
||||
|
||||
/** @type {Expression} */
|
||||
let init = prop;
|
||||
if (declarator.init) {
|
||||
const default_value = /** @type {Expression} */ (visit(declarator.init));
|
||||
init = is_expression_async(default_value)
|
||||
? b.await(b.call('$.value_or_fallback_async', prop, b.thunk(default_value, true)))
|
||||
: b.call('$.value_or_fallback', prop, b.thunk(default_value));
|
||||
}
|
||||
|
||||
declarations.push(b.declarator(declarator.id, init));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
declarations.push(
|
||||
...create_state_declarators(
|
||||
declarator,
|
||||
state.scope,
|
||||
/** @type {Expression} */ (declarator.init && visit(declarator.init))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
declarations
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {VariableDeclarator} declarator
|
||||
* @param {Scope} scope
|
||||
* @param {Expression} value
|
||||
* @returns {VariableDeclarator[]}
|
||||
*/
|
||||
function create_state_declarators(declarator, scope, value) {
|
||||
if (declarator.id.type === 'Identifier') {
|
||||
return [b.declarator(declarator.id, value)];
|
||||
}
|
||||
|
||||
const tmp = scope.generate('tmp');
|
||||
const paths = extract_paths(declarator.id);
|
||||
return [
|
||||
b.declarator(b.id(tmp), value), // TODO inject declarator for opts, so we can use it below
|
||||
...paths.map((path) => {
|
||||
const value = path.expression?.(b.id(tmp));
|
||||
return b.declarator(path.node, value);
|
||||
})
|
||||
];
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
/** @import { BlockStatement, Expression, ExpressionStatement, Property, Statement } from 'estree' */
|
||||
/** @import { Attribute, Component, SvelteComponent, SvelteSelf, TemplateNode, Text } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../../types.js' */
|
||||
import { empty_comment, serialize_attribute_value } from './utils.js';
|
||||
import * as b from '../../../../../utils/builders.js';
|
||||
import { is_element_node } from '../../../../nodes.js';
|
||||
|
||||
/**
|
||||
* @param {Component | SvelteComponent | SvelteSelf} node
|
||||
* @param {Expression} expression
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function serialize_inline_component(node, expression, context) {
|
||||
/** @type {Array<Property[] | Expression>} */
|
||||
const props_and_spreads = [];
|
||||
|
||||
/** @type {Property[]} */
|
||||
const custom_css_props = [];
|
||||
|
||||
/** @type {ExpressionStatement[]} */
|
||||
const lets = [];
|
||||
|
||||
/** @type {Record<string, TemplateNode[]>} */
|
||||
const children = {};
|
||||
|
||||
/**
|
||||
* If this component has a slot property, it is a named slot within another component. In this case
|
||||
* the slot scope applies to the component itself, too, and not just its children.
|
||||
*/
|
||||
let slot_scope_applies_to_itself = false;
|
||||
|
||||
/**
|
||||
* Components may have a children prop and also have child nodes. In this case, we assume
|
||||
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
|
||||
* We're not doing it for spread attributes, as this would result in too many false positives.
|
||||
*/
|
||||
let has_children_prop = false;
|
||||
|
||||
/**
|
||||
* @param {Property} prop
|
||||
*/
|
||||
function push_prop(prop) {
|
||||
const current = props_and_spreads.at(-1);
|
||||
const current_is_props = Array.isArray(current);
|
||||
const props = current_is_props ? current : [];
|
||||
props.push(prop);
|
||||
if (!current_is_props) {
|
||||
props_and_spreads.push(props);
|
||||
}
|
||||
}
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'LetDirective') {
|
||||
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
|
||||
} else if (attribute.type === 'SpreadAttribute') {
|
||||
props_and_spreads.push(/** @type {Expression} */ (context.visit(attribute)));
|
||||
} else if (attribute.type === 'Attribute') {
|
||||
if (attribute.name.startsWith('--')) {
|
||||
const value = serialize_attribute_value(attribute.value, context, false, true);
|
||||
custom_css_props.push(b.init(attribute.name, value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attribute.name === 'slot') {
|
||||
slot_scope_applies_to_itself = true;
|
||||
}
|
||||
|
||||
if (attribute.name === 'children') {
|
||||
has_children_prop = true;
|
||||
}
|
||||
|
||||
const value = serialize_attribute_value(attribute.value, context, false, true);
|
||||
push_prop(b.prop('init', b.key(attribute.name), value));
|
||||
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
|
||||
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
|
||||
push_prop(
|
||||
b.get(attribute.name, [
|
||||
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
|
||||
])
|
||||
);
|
||||
push_prop(
|
||||
b.set(attribute.name, [
|
||||
b.stmt(
|
||||
/** @type {Expression} */ (
|
||||
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
|
||||
)
|
||||
),
|
||||
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (slot_scope_applies_to_itself) {
|
||||
context.state.init.push(...lets);
|
||||
}
|
||||
|
||||
/** @type {Statement[]} */
|
||||
const snippet_declarations = [];
|
||||
|
||||
// Group children by slot
|
||||
for (const child of node.fragment.nodes) {
|
||||
if (child.type === 'SnippetBlock') {
|
||||
// the SnippetBlock visitor adds a declaration to `init`, but if it's directly
|
||||
// inside a component then we want to hoist them into a block so that they
|
||||
// can be used as props without creating conflicts
|
||||
context.visit(child, {
|
||||
...context.state,
|
||||
init: snippet_declarations
|
||||
});
|
||||
|
||||
push_prop(b.prop('init', child.expression, child.expression));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let slot_name = 'default';
|
||||
if (is_element_node(child)) {
|
||||
const attribute = /** @type {Attribute | undefined} */ (
|
||||
child.attributes.find(
|
||||
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
|
||||
)
|
||||
);
|
||||
if (attribute !== undefined) {
|
||||
slot_name = /** @type {Text[]} */ (attribute.value)[0].data;
|
||||
}
|
||||
}
|
||||
|
||||
children[slot_name] = children[slot_name] || [];
|
||||
children[slot_name].push(child);
|
||||
}
|
||||
|
||||
// Serialize each slot
|
||||
/** @type {Property[]} */
|
||||
const serialized_slots = [];
|
||||
|
||||
for (const slot_name of Object.keys(children)) {
|
||||
const block = /** @type {BlockStatement} */ (
|
||||
context.visit(
|
||||
{
|
||||
...node.fragment,
|
||||
// @ts-expect-error
|
||||
nodes: children[slot_name]
|
||||
},
|
||||
{
|
||||
...context.state,
|
||||
scope:
|
||||
context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ??
|
||||
context.state.scope
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (block.body.length === 0) continue;
|
||||
|
||||
const slot_fn = b.arrow(
|
||||
[b.id('$$payload'), b.id('$$slotProps')],
|
||||
b.block([
|
||||
...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []),
|
||||
...block.body
|
||||
])
|
||||
);
|
||||
|
||||
if (slot_name === 'default' && !has_children_prop) {
|
||||
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
|
||||
// create `children` prop...
|
||||
push_prop(b.prop('init', b.id('children'), slot_fn));
|
||||
|
||||
// and `$$slots.default: true` so that `<slot>` on the child works
|
||||
serialized_slots.push(b.init(slot_name, b.true));
|
||||
} else {
|
||||
// create `$$slots.default`...
|
||||
serialized_slots.push(b.init(slot_name, slot_fn));
|
||||
|
||||
// and a `children` prop that errors
|
||||
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
|
||||
}
|
||||
} else {
|
||||
serialized_slots.push(b.init(slot_name, slot_fn));
|
||||
}
|
||||
}
|
||||
|
||||
if (serialized_slots.length > 0) {
|
||||
push_prop(b.prop('init', b.id('$$slots'), b.object(serialized_slots)));
|
||||
}
|
||||
|
||||
const props_expression =
|
||||
props_and_spreads.length === 0 ||
|
||||
(props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0]))
|
||||
? b.object(/** @type {Property[]} */ (props_and_spreads[0] || []))
|
||||
: b.call(
|
||||
'$.spread_props',
|
||||
b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)))
|
||||
);
|
||||
|
||||
/** @type {Statement} */
|
||||
let statement = b.stmt(
|
||||
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
|
||||
expression,
|
||||
b.id('$$payload'),
|
||||
props_expression
|
||||
)
|
||||
);
|
||||
|
||||
if (snippet_declarations.length > 0) {
|
||||
statement = b.block([...snippet_declarations, statement]);
|
||||
}
|
||||
|
||||
const dynamic =
|
||||
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
|
||||
|
||||
if (custom_css_props.length > 0) {
|
||||
context.state.template.push(
|
||||
b.stmt(
|
||||
b.call(
|
||||
'$.css_props',
|
||||
b.id('$$payload'),
|
||||
b.literal(context.state.namespace === 'svg' ? false : true),
|
||||
b.object(custom_css_props),
|
||||
b.thunk(b.block([statement])),
|
||||
dynamic && b.true
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if (dynamic) {
|
||||
context.state.template.push(empty_comment);
|
||||
}
|
||||
|
||||
context.state.template.push(statement);
|
||||
|
||||
if (!context.state.skip_hydration_boundaries) {
|
||||
context.state.template.push(empty_comment);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,442 @@
|
||||
/** @import { Expression, ExpressionStatement, Literal } from 'estree' */
|
||||
/** @import { Attribute, ClassDirective, Namespace, RegularElement, SpreadAttribute, StyleDirective, SvelteElement, SvelteNode } from '#compiler' */
|
||||
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
|
||||
import {
|
||||
get_attribute_chunks,
|
||||
is_event_attribute,
|
||||
is_text_attribute
|
||||
} from '../../../../../utils/ast.js';
|
||||
import { binding_properties } from '../../../../bindings.js';
|
||||
import {
|
||||
ContentEditableBindings,
|
||||
LoadErrorElements,
|
||||
WhitespaceInsensitiveAttributes
|
||||
} from '../../../../constants.js';
|
||||
import { create_attribute, is_custom_element_node } from '../../../../nodes.js';
|
||||
import { regex_starts_with_newline } from '../../../../patterns.js';
|
||||
import * as b from '../../../../../utils/builders.js';
|
||||
import {
|
||||
DOMBooleanAttributes,
|
||||
ELEMENT_IS_NAMESPACED,
|
||||
ELEMENT_PRESERVE_ATTRIBUTE_CASE
|
||||
} from '../../../../../../constants.js';
|
||||
import { serialize_attribute_value } from './utils.js';
|
||||
|
||||
/**
|
||||
* Writes the output to the template output. Some elements may have attributes on them that require the
|
||||
* their output to be the child content instead. In this case, an object is returned.
|
||||
* @param {RegularElement | SvelteElement} node
|
||||
* @param {import('zimmerframe').Context<SvelteNode, ComponentServerTransformState>} context
|
||||
*/
|
||||
export function serialize_element_attributes(node, context) {
|
||||
/** @type {Array<Attribute | SpreadAttribute>} */
|
||||
const attributes = [];
|
||||
|
||||
/** @type {ClassDirective[]} */
|
||||
const class_directives = [];
|
||||
|
||||
/** @type {StyleDirective[]} */
|
||||
const style_directives = [];
|
||||
|
||||
/** @type {ExpressionStatement[]} */
|
||||
const lets = [];
|
||||
|
||||
/** @type {Expression | null} */
|
||||
let content = null;
|
||||
|
||||
let has_spread = false;
|
||||
// Use the index to keep the attributes order which is important for spreading
|
||||
let class_index = -1;
|
||||
let style_index = -1;
|
||||
let events_to_capture = new Set();
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'Attribute') {
|
||||
if (attribute.name === 'value') {
|
||||
if (node.name === 'textarea') {
|
||||
if (
|
||||
attribute.value !== true &&
|
||||
Array.isArray(attribute.value) &&
|
||||
attribute.value[0].type === 'Text' &&
|
||||
regex_starts_with_newline.test(attribute.value[0].data)
|
||||
) {
|
||||
// Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`.
|
||||
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
|
||||
// also see related code in analysis phase
|
||||
attribute.value[0].data = '\n' + attribute.value[0].data;
|
||||
}
|
||||
content = b.call('$.escape', serialize_attribute_value(attribute.value, context));
|
||||
} else if (node.name !== 'select') {
|
||||
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no
|
||||
// effect on the selected value after the user interacts with the select element (the value _property_ does, but not the attribute)
|
||||
attributes.push(attribute);
|
||||
}
|
||||
|
||||
// omit event handlers except for special cases
|
||||
} else if (is_event_attribute(attribute)) {
|
||||
if (
|
||||
(attribute.name === 'onload' || attribute.name === 'onerror') &&
|
||||
LoadErrorElements.includes(node.name)
|
||||
) {
|
||||
events_to_capture.add(attribute.name);
|
||||
}
|
||||
} else {
|
||||
if (attribute.name === 'class') {
|
||||
class_index = attributes.length;
|
||||
} else if (attribute.name === 'style') {
|
||||
style_index = attributes.length;
|
||||
}
|
||||
attributes.push(attribute);
|
||||
}
|
||||
} else if (attribute.type === 'BindDirective') {
|
||||
if (attribute.name === 'value' && node.name === 'select') continue;
|
||||
if (
|
||||
attribute.name === 'value' &&
|
||||
attributes.some(
|
||||
(attr) =>
|
||||
attr.type === 'Attribute' &&
|
||||
attr.name === 'type' &&
|
||||
is_text_attribute(attr) &&
|
||||
attr.value[0].data === 'file'
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (attribute.name === 'this') continue;
|
||||
|
||||
const binding = binding_properties[attribute.name];
|
||||
if (binding?.omit_in_ssr) continue;
|
||||
|
||||
if (ContentEditableBindings.includes(attribute.name)) {
|
||||
content = /** @type {Expression} */ (context.visit(attribute.expression));
|
||||
} else if (attribute.name === 'value' && node.name === 'textarea') {
|
||||
content = b.call(
|
||||
'$.escape',
|
||||
/** @type {Expression} */ (context.visit(attribute.expression))
|
||||
);
|
||||
} else if (attribute.name === 'group') {
|
||||
const value_attribute = /** @type {Attribute | undefined} */ (
|
||||
node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value')
|
||||
);
|
||||
if (!value_attribute) continue;
|
||||
|
||||
const is_checkbox = node.attributes.some(
|
||||
(attr) =>
|
||||
attr.type === 'Attribute' &&
|
||||
attr.name === 'type' &&
|
||||
is_text_attribute(attr) &&
|
||||
attr.value[0].data === 'checkbox'
|
||||
);
|
||||
attributes.push(
|
||||
create_attribute('checked', -1, -1, [
|
||||
{
|
||||
type: 'ExpressionTag',
|
||||
start: -1,
|
||||
end: -1,
|
||||
parent: attribute,
|
||||
expression: is_checkbox
|
||||
? b.call(
|
||||
b.member(attribute.expression, b.id('includes')),
|
||||
serialize_attribute_value(value_attribute.value, context)
|
||||
)
|
||||
: b.binary(
|
||||
'===',
|
||||
attribute.expression,
|
||||
serialize_attribute_value(value_attribute.value, context)
|
||||
),
|
||||
metadata: {
|
||||
contains_call_expression: false,
|
||||
dynamic: false
|
||||
}
|
||||
}
|
||||
])
|
||||
);
|
||||
} else {
|
||||
attributes.push(
|
||||
create_attribute(attribute.name, -1, -1, [
|
||||
{
|
||||
type: 'ExpressionTag',
|
||||
start: -1,
|
||||
end: -1,
|
||||
parent: attribute,
|
||||
expression: attribute.expression,
|
||||
metadata: {
|
||||
contains_call_expression: false,
|
||||
dynamic: false
|
||||
}
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
} else if (attribute.type === 'SpreadAttribute') {
|
||||
attributes.push(attribute);
|
||||
has_spread = true;
|
||||
if (LoadErrorElements.includes(node.name)) {
|
||||
events_to_capture.add('onload');
|
||||
events_to_capture.add('onerror');
|
||||
}
|
||||
} else if (attribute.type === 'UseDirective') {
|
||||
if (LoadErrorElements.includes(node.name)) {
|
||||
events_to_capture.add('onload');
|
||||
events_to_capture.add('onerror');
|
||||
}
|
||||
} else if (attribute.type === 'ClassDirective') {
|
||||
class_directives.push(attribute);
|
||||
} else if (attribute.type === 'StyleDirective') {
|
||||
style_directives.push(attribute);
|
||||
} else if (attribute.type === 'LetDirective') {
|
||||
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
|
||||
} else {
|
||||
context.visit(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (class_directives.length > 0 && !has_spread) {
|
||||
const class_attribute = serialize_class_directives(
|
||||
class_directives,
|
||||
/** @type {Attribute | null} */ (attributes[class_index] ?? null)
|
||||
);
|
||||
if (class_index === -1) {
|
||||
attributes.push(class_attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (style_directives.length > 0 && !has_spread) {
|
||||
serialize_style_directives(
|
||||
style_directives,
|
||||
/** @type {Attribute | null} */ (attributes[style_index] ?? null),
|
||||
context
|
||||
);
|
||||
if (style_index > -1) {
|
||||
attributes.splice(style_index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Let bindings first, they can be used on attributes
|
||||
context.state.init.push(...lets);
|
||||
|
||||
if (has_spread) {
|
||||
serialize_element_spread_attributes(
|
||||
node,
|
||||
attributes,
|
||||
style_directives,
|
||||
class_directives,
|
||||
context
|
||||
);
|
||||
} else {
|
||||
for (const attribute of /** @type {Attribute[]} */ (attributes)) {
|
||||
if (attribute.value === true || is_text_attribute(attribute)) {
|
||||
const name = get_attribute_name(node, attribute, context);
|
||||
const literal_value = /** @type {Literal} */ (
|
||||
serialize_attribute_value(
|
||||
attribute.value,
|
||||
context,
|
||||
WhitespaceInsensitiveAttributes.includes(name)
|
||||
)
|
||||
).value;
|
||||
if (name !== 'class' || literal_value) {
|
||||
context.state.template.push(
|
||||
b.literal(
|
||||
` ${attribute.name}${
|
||||
DOMBooleanAttributes.includes(name) && literal_value === true
|
||||
? ''
|
||||
: `="${literal_value === true ? '' : String(literal_value)}"`
|
||||
}`
|
||||
)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = get_attribute_name(node, attribute, context);
|
||||
const is_boolean = DOMBooleanAttributes.includes(name);
|
||||
const value = serialize_attribute_value(
|
||||
attribute.value,
|
||||
context,
|
||||
WhitespaceInsensitiveAttributes.includes(name)
|
||||
);
|
||||
|
||||
context.state.template.push(
|
||||
b.call('$.attr', b.literal(name), value, is_boolean && b.literal(is_boolean))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (events_to_capture.size !== 0) {
|
||||
for (const event of events_to_capture) {
|
||||
context.state.template.push(b.literal(` ${event}="this.__e=event"`));
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RegularElement | SvelteElement} element
|
||||
* @param {Attribute} attribute
|
||||
* @param {{ state: { namespace: Namespace }}} context
|
||||
*/
|
||||
function get_attribute_name(element, attribute, context) {
|
||||
let name = attribute.name;
|
||||
if (!element.metadata.svg && !element.metadata.mathml && context.state.namespace !== 'foreign') {
|
||||
name = name.toLowerCase();
|
||||
// don't lookup boolean aliases here, the server runtime function does only
|
||||
// check for the lowercase variants of boolean attributes
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RegularElement | SvelteElement} element
|
||||
* @param {Array<Attribute | SpreadAttribute>} attributes
|
||||
* @param {StyleDirective[]} style_directives
|
||||
* @param {ClassDirective[]} class_directives
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
function serialize_element_spread_attributes(
|
||||
element,
|
||||
attributes,
|
||||
style_directives,
|
||||
class_directives,
|
||||
context
|
||||
) {
|
||||
let classes;
|
||||
let styles;
|
||||
let flags = 0;
|
||||
|
||||
if (class_directives.length > 0 || context.state.analysis.css.hash) {
|
||||
const properties = class_directives.map((directive) =>
|
||||
b.init(
|
||||
directive.name,
|
||||
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
|
||||
? b.id(directive.name)
|
||||
: /** @type {Expression} */ (context.visit(directive.expression))
|
||||
)
|
||||
);
|
||||
|
||||
if (context.state.analysis.css.hash) {
|
||||
properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true)));
|
||||
}
|
||||
|
||||
classes = b.object(properties);
|
||||
}
|
||||
|
||||
if (style_directives.length > 0) {
|
||||
const properties = style_directives.map((directive) =>
|
||||
b.init(
|
||||
directive.name,
|
||||
directive.value === true
|
||||
? b.id(directive.name)
|
||||
: serialize_attribute_value(directive.value, context, true)
|
||||
)
|
||||
);
|
||||
|
||||
styles = b.object(properties);
|
||||
}
|
||||
|
||||
if (element.metadata.svg || element.metadata.mathml) {
|
||||
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
|
||||
} else if (is_custom_element_node(element)) {
|
||||
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
|
||||
}
|
||||
|
||||
const object = b.object(
|
||||
attributes.map((attribute) => {
|
||||
if (attribute.type === 'Attribute') {
|
||||
const name = get_attribute_name(element, attribute, context);
|
||||
const value = serialize_attribute_value(
|
||||
attribute.value,
|
||||
context,
|
||||
WhitespaceInsensitiveAttributes.includes(name)
|
||||
);
|
||||
return b.prop('init', b.key(name), value);
|
||||
}
|
||||
|
||||
return b.spread(/** @type {Expression} */ (context.visit(attribute)));
|
||||
})
|
||||
);
|
||||
|
||||
const args = [object, classes, styles, flags ? b.literal(flags) : undefined];
|
||||
context.state.template.push(b.call('$.spread_attributes', ...args));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ClassDirective[]} class_directives
|
||||
* @param {Attribute | null} class_attribute
|
||||
* @returns
|
||||
*/
|
||||
function serialize_class_directives(class_directives, class_attribute) {
|
||||
const expressions = class_directives.map((directive) =>
|
||||
b.conditional(directive.expression, b.literal(directive.name), b.literal(''))
|
||||
);
|
||||
|
||||
if (class_attribute === null) {
|
||||
class_attribute = create_attribute('class', -1, -1, []);
|
||||
}
|
||||
|
||||
const chunks = get_attribute_chunks(class_attribute.value);
|
||||
const last = chunks.at(-1);
|
||||
|
||||
if (last?.type === 'Text') {
|
||||
last.data += ' ';
|
||||
last.raw += ' ';
|
||||
} else if (last) {
|
||||
chunks.push({
|
||||
type: 'Text',
|
||||
start: -1,
|
||||
end: -1,
|
||||
parent: class_attribute,
|
||||
data: ' ',
|
||||
raw: ' '
|
||||
});
|
||||
}
|
||||
|
||||
chunks.push({
|
||||
type: 'ExpressionTag',
|
||||
start: -1,
|
||||
end: -1,
|
||||
parent: class_attribute,
|
||||
expression: b.call(
|
||||
b.member(
|
||||
b.call(b.member(b.array(expressions), b.id('filter')), b.id('Boolean')),
|
||||
b.id('join')
|
||||
),
|
||||
b.literal(' ')
|
||||
),
|
||||
metadata: { contains_call_expression: false, dynamic: false }
|
||||
});
|
||||
|
||||
class_attribute.value = chunks;
|
||||
return class_attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StyleDirective[]} style_directives
|
||||
* @param {Attribute | null} style_attribute
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
function serialize_style_directives(style_directives, style_attribute, context) {
|
||||
const styles = style_directives.map((directive) => {
|
||||
let value =
|
||||
directive.value === true
|
||||
? b.id(directive.name)
|
||||
: serialize_attribute_value(directive.value, context, true);
|
||||
if (directive.modifiers.includes('important')) {
|
||||
value = b.binary('+', value, b.literal(' !important'));
|
||||
}
|
||||
return b.init(directive.name, value);
|
||||
});
|
||||
|
||||
const arg =
|
||||
style_attribute === null
|
||||
? b.object(styles)
|
||||
: b.call(
|
||||
'$.merge_styles',
|
||||
serialize_attribute_value(style_attribute.value, context, true),
|
||||
b.object(styles)
|
||||
);
|
||||
|
||||
context.state.template.push(b.call('$.add_styles', arg));
|
||||
}
|
@ -0,0 +1,365 @@
|
||||
/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Identifier, Node, Pattern, Statement, TemplateElement } from 'estree' */
|
||||
/** @import { Attribute, Comment, ExpressionTag, SvelteNode, Text } from '#compiler' */
|
||||
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
|
||||
import { extract_paths } from '../../../../../utils/ast.js';
|
||||
import { escape_html } from '../../../../../../escaping.js';
|
||||
import {
|
||||
BLOCK_CLOSE,
|
||||
BLOCK_OPEN,
|
||||
EMPTY_COMMENT
|
||||
} from '../../../../../../internal/server/hydration.js';
|
||||
import * as b from '../../../../../utils/builders.js';
|
||||
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
|
||||
import { regex_whitespaces_strict } from '../../../../patterns.js';
|
||||
|
||||
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
|
||||
export const block_open = b.literal(BLOCK_OPEN);
|
||||
|
||||
/** Closes an if/each block, so that we can remove nodes in the case of a mismatch. Also serves as an anchor for these blocks */
|
||||
export const block_close = b.literal(BLOCK_CLOSE);
|
||||
|
||||
/** Empty comment to keep text nodes separate, or provide an anchor node for blocks */
|
||||
export const empty_comment = b.literal(EMPTY_COMMENT);
|
||||
|
||||
/**
|
||||
* Processes an array of template nodes, joining sibling text/expression nodes and
|
||||
* recursing into child nodes.
|
||||
* @param {Array<SvelteNode>} nodes
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function process_children(nodes, { visit, state }) {
|
||||
/** @type {Array<Text | Comment | ExpressionTag>} */
|
||||
let sequence = [];
|
||||
|
||||
function flush() {
|
||||
let quasi = b.quasi('', false);
|
||||
const quasis = [quasi];
|
||||
|
||||
/** @type {Expression[]} */
|
||||
const expressions = [];
|
||||
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const node = sequence[i];
|
||||
|
||||
if (node.type === 'Text' || node.type === 'Comment') {
|
||||
quasi.value.raw += sanitize_template_string(
|
||||
node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data)
|
||||
);
|
||||
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
|
||||
if (node.expression.value != null) {
|
||||
quasi.value.raw += sanitize_template_string(escape_html(node.expression.value + ''));
|
||||
}
|
||||
} else {
|
||||
expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
|
||||
|
||||
quasi = b.quasi('', i + 1 === sequence.length);
|
||||
quasis.push(quasi);
|
||||
}
|
||||
}
|
||||
|
||||
state.template.push(b.template(quasis, expressions));
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
|
||||
sequence.push(node);
|
||||
} else {
|
||||
if (sequence.length > 0) {
|
||||
flush();
|
||||
sequence = [];
|
||||
}
|
||||
|
||||
visit(node, { ...state });
|
||||
}
|
||||
}
|
||||
|
||||
if (sequence.length > 0) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
* @returns {node is Statement}
|
||||
*/
|
||||
function is_statement(node) {
|
||||
return node.type.endsWith('Statement') || node.type.endsWith('Declaration');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<Statement | Expression>} template
|
||||
* @param {Identifier} out
|
||||
* @param {AssignmentOperator} operator
|
||||
* @returns {Statement[]}
|
||||
*/
|
||||
export function serialize_template(template, out = b.id('$$payload.out'), operator = '+=') {
|
||||
/** @type {TemplateElement[]} */
|
||||
let quasis = [];
|
||||
|
||||
/** @type {Expression[]} */
|
||||
let expressions = [];
|
||||
|
||||
/** @type {Statement[]} */
|
||||
const statements = [];
|
||||
|
||||
const flush = () => {
|
||||
statements.push(b.stmt(b.assignment(operator, out, b.template(quasis, expressions))));
|
||||
quasis = [];
|
||||
expressions = [];
|
||||
};
|
||||
|
||||
for (let i = 0; i < template.length; i++) {
|
||||
const node = template[i];
|
||||
|
||||
if (is_statement(node)) {
|
||||
if (quasis.length !== 0) {
|
||||
flush();
|
||||
}
|
||||
|
||||
statements.push(node);
|
||||
} else {
|
||||
let last = quasis.at(-1);
|
||||
if (!last) quasis.push((last = b.quasi('', false)));
|
||||
|
||||
if (node.type === 'Literal') {
|
||||
last.value.raw +=
|
||||
typeof node.value === 'string' ? sanitize_template_string(node.value) : node.value;
|
||||
} else if (node.type === 'TemplateLiteral') {
|
||||
last.value.raw += node.quasis[0].value.raw;
|
||||
quasis.push(...node.quasis.slice(1));
|
||||
expressions.push(...node.expressions);
|
||||
} else {
|
||||
expressions.push(node);
|
||||
quasis.push(b.quasi('', i + 1 === template.length || is_statement(template[i + 1])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (quasis.length !== 0) {
|
||||
flush();
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Attribute['value']} value
|
||||
* @param {ComponentContext} context
|
||||
* @param {boolean} trim_whitespace
|
||||
* @param {boolean} is_component
|
||||
* @returns {Expression}
|
||||
*/
|
||||
export function serialize_attribute_value(
|
||||
value,
|
||||
context,
|
||||
trim_whitespace = false,
|
||||
is_component = false
|
||||
) {
|
||||
if (value === true) {
|
||||
return b.true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value) || value.length === 1) {
|
||||
const chunk = Array.isArray(value) ? value[0] : value;
|
||||
|
||||
if (chunk.type === 'Text') {
|
||||
const data = trim_whitespace
|
||||
? chunk.data.replace(regex_whitespaces_strict, ' ').trim()
|
||||
: chunk.data;
|
||||
|
||||
return b.literal(is_component ? data : escape_html(data, true));
|
||||
}
|
||||
|
||||
return /** @type {Expression} */ (context.visit(chunk.expression));
|
||||
}
|
||||
|
||||
let quasi = b.quasi('', false);
|
||||
const quasis = [quasi];
|
||||
|
||||
/** @type {Expression[]} */
|
||||
const expressions = [];
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const node = value[i];
|
||||
|
||||
if (node.type === 'Text') {
|
||||
quasi.value.raw += trim_whitespace
|
||||
? node.data.replace(regex_whitespaces_strict, ' ')
|
||||
: node.data;
|
||||
} else {
|
||||
expressions.push(
|
||||
b.call('$.stringify', /** @type {Expression} */ (context.visit(node.expression)))
|
||||
);
|
||||
|
||||
quasi = b.quasi('', i + 1 === value.length);
|
||||
quasis.push(quasi);
|
||||
}
|
||||
}
|
||||
|
||||
return b.template(quasis, expressions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier} node
|
||||
* @param {ServerTransformState} state
|
||||
* @returns {Expression}
|
||||
*/
|
||||
export function serialize_get_binding(node, state) {
|
||||
const binding = state.scope.get(node.name);
|
||||
|
||||
if (binding === null || node === binding.node) {
|
||||
// No associated binding or the declaration itself which shouldn't be transformed
|
||||
return node;
|
||||
}
|
||||
|
||||
if (binding.kind === 'store_sub') {
|
||||
const store_id = b.id(node.name.slice(1));
|
||||
return b.call(
|
||||
'$.store_get',
|
||||
b.assignment('??=', b.id('$$store_subs'), b.object([])),
|
||||
b.literal(node.name),
|
||||
serialize_get_binding(store_id, state)
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.hasOwn(state.getters, node.name)) {
|
||||
const getter = state.getters[node.name];
|
||||
return typeof getter === 'function' ? getter(node) : getter;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AssignmentExpression} node
|
||||
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context
|
||||
* @param {() => any} fallback
|
||||
* @returns {Expression}
|
||||
*/
|
||||
export function serialize_set_binding(node, context, fallback) {
|
||||
const { state, visit } = context;
|
||||
|
||||
if (
|
||||
node.left.type === 'ArrayPattern' ||
|
||||
node.left.type === 'ObjectPattern' ||
|
||||
node.left.type === 'RestElement'
|
||||
) {
|
||||
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
|
||||
const tmp_id = context.state.scope.generate('tmp');
|
||||
|
||||
/** @type {AssignmentExpression[]} */
|
||||
const original_assignments = [];
|
||||
|
||||
/** @type {Expression[]} */
|
||||
const assignments = [];
|
||||
|
||||
const paths = extract_paths(node.left);
|
||||
|
||||
for (const path of paths) {
|
||||
const value = path.expression?.(b.id(tmp_id));
|
||||
const assignment = b.assignment('=', path.node, value);
|
||||
original_assignments.push(assignment);
|
||||
assignments.push(serialize_set_binding(assignment, context, () => assignment));
|
||||
}
|
||||
|
||||
if (assignments.every((assignment, i) => assignment === original_assignments[i])) {
|
||||
// No change to output -> nothing to transform -> we can keep the original assignment
|
||||
return fallback();
|
||||
}
|
||||
|
||||
return b.call(
|
||||
b.thunk(
|
||||
b.block([
|
||||
b.const(tmp_id, /** @type {Expression} */ (visit(node.right))),
|
||||
b.stmt(b.sequence(assignments)),
|
||||
b.return(b.id(tmp_id))
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
|
||||
throw new Error(`Unexpected assignment type ${node.left.type}`);
|
||||
}
|
||||
|
||||
let left = node.left;
|
||||
|
||||
while (left.type === 'MemberExpression') {
|
||||
// @ts-expect-error
|
||||
left = left.object;
|
||||
}
|
||||
|
||||
if (left.type !== 'Identifier') {
|
||||
return fallback();
|
||||
}
|
||||
|
||||
const is_store = is_store_name(left.name);
|
||||
const left_name = is_store ? left.name.slice(1) : left.name;
|
||||
const binding = state.scope.get(left_name);
|
||||
|
||||
if (!binding) return fallback();
|
||||
|
||||
if (binding.mutation !== null) {
|
||||
return binding.mutation(node, context);
|
||||
}
|
||||
|
||||
if (
|
||||
binding.kind !== 'state' &&
|
||||
binding.kind !== 'frozen_state' &&
|
||||
binding.kind !== 'prop' &&
|
||||
binding.kind !== 'bindable_prop' &&
|
||||
binding.kind !== 'each' &&
|
||||
binding.kind !== 'legacy_reactive' &&
|
||||
!is_store
|
||||
) {
|
||||
// TODO error if it's a computed (or rest prop)? or does that already happen elsewhere?
|
||||
return fallback();
|
||||
}
|
||||
|
||||
const value = get_assignment_value(node, context);
|
||||
if (left === node.left) {
|
||||
if (is_store) {
|
||||
return b.call('$.store_set', b.id(left_name), /** @type {Expression} */ (visit(node.right)));
|
||||
}
|
||||
return fallback();
|
||||
} else if (is_store) {
|
||||
return b.call(
|
||||
'$.mutate_store',
|
||||
b.assignment('??=', b.id('$$store_subs'), b.object([])),
|
||||
b.literal(left.name),
|
||||
b.id(left_name),
|
||||
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value)
|
||||
);
|
||||
}
|
||||
return fallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AssignmentExpression} node
|
||||
* @param {Pick<import('zimmerframe').Context<SvelteNode, ServerTransformState>, 'visit' | 'state'>} context
|
||||
*/
|
||||
function get_assignment_value(node, { state, visit }) {
|
||||
if (node.left.type === 'Identifier') {
|
||||
const operator = node.operator;
|
||||
return operator === '='
|
||||
? /** @type {Expression} */ (visit(node.right))
|
||||
: // turn something like x += 1 into x = x + 1
|
||||
b.binary(
|
||||
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
|
||||
serialize_get_binding(node.left, state),
|
||||
/** @type {Expression} */ (visit(node.right))
|
||||
);
|
||||
}
|
||||
|
||||
return /** @type {Expression} */ (visit(node.right));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
function is_store_name(name) {
|
||||
return name[0] === '$' && /[A-Za-z_]/.test(name[1]);
|
||||
}
|
Loading…
Reference in new issue