Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,9 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null)
template: /** @type {any} */ (null),
memoizer: /** @type {any} */ (null)
};

const module = /** @type {ESTree.Program} */ (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { Template } from './transform-template/template.js';
import type { Memoizer } from './visitors/shared/utils.js';

export interface ClientTransformState extends TransformState {
/**
Expand Down Expand Up @@ -49,8 +50,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Expressions used inside the render effect */
readonly expressions: Expression[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */
readonly template: Template;
readonly metadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as b from '#compiler/builders';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
import { Template } from '../transform-template/template.js';

/**
Expand Down Expand Up @@ -64,8 +64,8 @@ export function Fragment(node, context) {
...context.state,
init: [],
update: [],
expressions: [],
after_update: [],
memoizer: new Memoizer(),
template: new Template(),
transform: { ...context.state.transform },
metadata: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement, build_template_chunk, get_expression_id } from './shared/utils.js';
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';

/**
Expand Down Expand Up @@ -253,8 +253,7 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) =>
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
(value, metadata) => (metadata.has_call ? context.state.memoizer.add(value) : value)
);

const update = build_element_attribute_update(node, node_id, name, value, attributes);
Expand Down Expand Up @@ -455,11 +454,15 @@ function setup_select_synchronization(value_binding, context) {

/**
* @param {AST.ClassDirective[]} class_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(class_directives, expressions, context) {
export function build_class_directives_object(
class_directives,
context,
memoizer = context.state.memoizer
) {
let properties = [];
let has_call_or_state = false;

Expand All @@ -471,38 +474,40 @@ export function build_class_directives_object(class_directives, expressions, con

const directives = b.object(properties);

return has_call_or_state ? get_expression_id(expressions, directives) : directives;
return has_call_or_state ? memoizer.add(directives) : directives;
}

/**
* @param {AST.StyleDirective[]} style_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}}
* @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/
export function build_style_directives_object(style_directives, expressions, context) {
let normal_properties = [];
let important_properties = [];
export function build_style_directives_object(
style_directives,
context,
memoizer = context.state.memoizer
) {
const normal = b.object([]);
const important = b.object([]);

for (const directive of style_directives) {
let has_call_or_state = false;

for (const d of style_directives) {
const expression =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(expressions, value) : value
).value;
const property = b.init(directive.name, expression);

if (directive.modifiers.includes('important')) {
important_properties.push(property);
} else {
normal_properties.push(property);
}
d.value === true
? build_getter({ name: d.name, type: 'Identifier' }, context.state)
: build_attribute_value(d.value, context).value;

const object = d.modifiers.includes('important') ? important : normal;
object.properties.push(b.init(d.name, expression));

has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
}

return important_properties.length
? b.array([b.object(normal_properties), b.object(important_properties)])
: b.object(normal_properties);
const directives = important.properties.length ? b.array([normal, important]) : normal;

return has_call_or_state ? memoizer.add(directives) : directives;
}

/**
Expand Down Expand Up @@ -624,7 +629,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);

const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(state.expressions, value) : value
metadata.has_call ? state.memoizer.add(value) : value
);

const evaluated = context.state.scope.evaluate(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
build_attribute_effect,
build_set_class
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';

/**
* @param {AST.SvelteElement} node
Expand Down Expand Up @@ -46,8 +46,8 @@ export function SvelteElement(node, context) {
node: element_id,
init: [],
update: [],
expressions: [],
after_update: []
after_update: [],
memoizer: new Memoizer()
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_expression, build_template_chunk, get_expression_id } from './utils.js';
import { build_expression, build_template_chunk, Memoizer } from './utils.js';

/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
Expand All @@ -28,18 +28,12 @@ export function build_attribute_effect(
/** @type {ObjectExpression['properties']} */
const values = [];

/** @type {Expression[]} */
const expressions = [];

/** @param {Expression} value */
function memoize(value) {
return b.id(`$${expressions.push(value) - 1}`);
}
const memoizer = new Memoizer();

for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? memoize(value) : value
metadata.has_call ? memoizer.add(value) : value
);

if (
Expand All @@ -57,7 +51,7 @@ export function build_attribute_effect(
let value = /** @type {Expression} */ (context.visit(attribute));

if (attribute.metadata.expression.has_call) {
value = memoize(value);
value = memoizer.add(value);
}

values.push(b.spread(value));
Expand All @@ -69,7 +63,7 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, expressions, context)
build_class_directives_object(class_directives, context, memoizer)
)
);
}
Expand All @@ -79,21 +73,20 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.STYLE')]),
build_style_directives_object(style_directives, expressions, context)
build_style_directives_object(style_directives, context, memoizer)
)
);
}

const ids = memoizer.apply();

context.state.init.push(
b.stmt(
b.call(
'$.attribute_effect',
element_id,
b.arrow(
expressions.map((_, i) => b.id(`$${i}`)),
b.object(values)
),
expressions.length > 0 && b.array(expressions.map((expression) => b.thunk(expression))),
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
Expand Down Expand Up @@ -158,7 +151,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}

return metadata.has_call ? get_expression_id(context.state.expressions, value) : value;
return metadata.has_call ? context.state.memoizer.add(value) : value;
});

/** @type {Identifier | undefined} */
Expand All @@ -171,7 +164,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
let next;

if (class_directives.length) {
next = build_class_directives_object(class_directives, context.state.expressions, context);
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);

if (has_state) {
Expand Down Expand Up @@ -226,7 +219,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
metadata.has_call ? context.state.memoizer.add(value) : value
);

/** @type {Identifier | undefined} */
Expand All @@ -235,11 +228,11 @@ export function build_set_style(node_id, attribute, style_directives, context) {
/** @type {ObjectExpression | Identifier | undefined} */
let prev;

/** @type {ArrayExpression | ObjectExpression | undefined} */
/** @type {Expression | undefined} */
let next;

if (style_directives.length) {
next = build_style_directives_object(style_directives, context.state.expressions, context);
next = build_style_directives_object(style_directives, context);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);

if (has_state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,38 @@ export function memoize_expression(state, value) {
return b.call('$.get', id);
}

/**
* Pushes `value` into `expressions` and returns a new id
* @param {Expression[]} expressions
* @param {Expression} value
*/
export function get_expression_id(expressions, value) {
return b.id(`$${expressions.push(value) - 1}`);
export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#sync = [];

/**
* @param {Expression} expression
*/
add(expression) {
const id = b.id('#'); // filled in later

this.#sync.push({ id, expression });

return id;
}

apply() {
return this.#sync.map((memo, i) => {
memo.id.name = `$${i}`;
return memo.id;
});
}

deriveds(runes = true) {
return this.#sync.map((memo) =>
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
);
}

sync_values() {
if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
}
}

/**
Expand All @@ -40,8 +65,7 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call ? get_expression_id(state.expressions, value) : value
memoize = (value, metadata) => (metadata.has_call ? state.memoizer.add(value) : value)
) {
/** @type {Expression[]} */
const expressions = [];
Expand Down Expand Up @@ -128,18 +152,22 @@ export function build_template_chunk(
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
const { memoizer } = state;

const ids = state.memoizer.apply();
const values = memoizer.sync_values();

return b.stmt(
b.call(
'$.template_effect',
b.arrow(
state.expressions.map((_, i) => b.id(`$${i}`)),
ids,
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
? state.update[0].expression
: b.block(state.update)
),
state.expressions.length > 0 &&
b.array(state.expressions.map((expression) => b.thunk(expression))),
state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
values,
values && !state.analysis.runes && b.id('$.derived_safe_equal')
)
);
}
Expand Down
Loading