Skip to content

Commit 74ca8e1

Browse files
committed
fix: unify <select> attribute handling and prevent double-await
1 parent cd37911 commit 74ca8e1

3 files changed

Lines changed: 81 additions & 129 deletions

File tree

packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import { is_void } from '../../../../../utils.js';
77
import { dev, locator } from '../../../../state.js';
88
import * as b from '#compiler/builders';
99
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
10-
import { is_custom_element_node } from '../../../nodes.js';
1110
import {
12-
ELEMENT_IS_NAMESPACED,
13-
ELEMENT_PRESERVE_ATTRIBUTE_CASE
14-
} from '../../../../../constants.js';
15-
import { build_element_attributes, build_spread_object } from './shared/element.js';
11+
build_element_attributes,
12+
build_spread_object,
13+
prepare_element_spread
14+
} from './shared/element.js';
1615
import {
1716
process_children,
1817
build_template,
@@ -43,8 +42,24 @@ export function RegularElement(node, context) {
4342
const optimiser = new PromiseOptimiser();
4443

4544
state.template.push(b.literal(`<${node.name}`));
46-
const body = build_element_attributes(node, { ...context, state }, optimiser.transform);
47-
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
45+
46+
// If this element needs special handling (like <select value>),
47+
// avoid calling build_element_attributes here to prevent evaluating/awaiting
48+
// attribute expressions twice. We'll handle attributes in the special branch.
49+
const is_select_special =
50+
node.name === 'select' &&
51+
node.attributes.some(
52+
(attribute) =>
53+
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
54+
attribute.name === 'value') ||
55+
attribute.type === 'SpreadAttribute'
56+
);
57+
58+
let body = /** @type {Expression | null} */ (null);
59+
if (!is_select_special) {
60+
body = build_element_attributes(node, { ...context, state }, optimiser.transform);
61+
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
62+
}
4863

4964
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
5065
state.template.push(
@@ -100,15 +115,7 @@ export function RegularElement(node, context) {
100115
);
101116
}
102117

103-
if (
104-
node.name === 'select' &&
105-
node.attributes.some(
106-
(attribute) =>
107-
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
108-
attribute.name === 'value') ||
109-
attribute.type === 'SpreadAttribute'
110-
)
111-
) {
118+
if (is_select_special) {
112119
/** @type {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} */
113120
const select_attributes = [];
114121
/** @type {AST.ClassDirective[]} */
@@ -130,58 +137,15 @@ export function RegularElement(node, context) {
130137
}
131138
}
132139

133-
const attributes_expression = build_spread_object(
140+
const { object, css_hash, classes, styles, flags } = prepare_element_spread(
134141
node,
135142
select_attributes,
143+
style_directives,
144+
class_directives,
136145
context,
137146
optimiser.transform
138147
);
139148

140-
/** @type {ObjectExpression | undefined} */
141-
let classes;
142-
if (class_directives.length) {
143-
const properties = class_directives.map((directive) =>
144-
b.init(
145-
directive.name,
146-
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
147-
? b.id(directive.name)
148-
: /** @type {Expression} */ (context.visit(directive.expression))
149-
)
150-
);
151-
152-
classes = b.object(properties);
153-
}
154-
155-
/** @type {ObjectExpression | undefined} */
156-
let styles;
157-
if (style_directives.length > 0) {
158-
const properties = style_directives.map((directive) =>
159-
b.init(
160-
directive.name,
161-
directive.value === true
162-
? b.id(directive.name)
163-
: build_attribute_value(directive.value, context, optimiser.transform, true)
164-
)
165-
);
166-
167-
styles = b.object(properties);
168-
}
169-
170-
let flags = 0;
171-
172-
if (node.metadata.svg || node.metadata.mathml) {
173-
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
174-
} else if (is_custom_element_node(node)) {
175-
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
176-
}
177-
178-
const css_hash =
179-
node.metadata.scoped && context.state.analysis.css.hash
180-
? b.literal(context.state.analysis.css.hash)
181-
: undefined;
182-
183-
const flag_literal = flags ? b.literal(flags) : undefined;
184-
185149
const inner_state = { ...state, template: [], init: [] };
186150
process_children(trimmed, { ...context, state: inner_state });
187151

@@ -191,15 +155,7 @@ export function RegularElement(node, context) {
191155
);
192156

193157
const statement = b.stmt(
194-
b.call(
195-
'$$renderer.select',
196-
attributes_expression,
197-
css_hash,
198-
classes,
199-
styles,
200-
flag_literal,
201-
fn
202-
)
158+
b.call('$$renderer.select', object, fn, css_hash, classes, styles, flags)
203159
);
204160

205161
if (optimiser.expressions.length > 0) {

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -358,62 +358,84 @@ function build_element_spread_attributes(
358358
context,
359359
transform
360360
) {
361+
const { object, css_hash, classes, styles, flags } = prepare_element_spread(
362+
element,
363+
/** @type {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} */ (attributes),
364+
style_directives,
365+
class_directives,
366+
context,
367+
transform
368+
);
369+
370+
let call = b.call('$.attributes', object, css_hash, classes, styles, flags);
371+
372+
context.state.template.push(call);
373+
}
374+
375+
/**
376+
* Prepare args for $.attributes(...): compute object, css_hash, classes, styles and flags.
377+
* @param {AST.RegularElement | AST.SvelteElement} element
378+
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
379+
* @param {AST.StyleDirective[]} style_directives
380+
* @param {AST.ClassDirective[]} class_directives
381+
* @param {ComponentContext} context
382+
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
383+
* @returns {{ object: ObjectExpression, css_hash: Literal | undefined, classes: ObjectExpression | undefined, styles: ObjectExpression | undefined, flags: Literal | undefined }}
384+
*/
385+
export function prepare_element_spread(
386+
element,
387+
attributes,
388+
style_directives,
389+
class_directives,
390+
context,
391+
transform
392+
) {
393+
/** @type {ObjectExpression | undefined} */
361394
let classes;
395+
/** @type {ObjectExpression | undefined} */
362396
let styles;
363-
let flags = 0;
364-
365-
let has_await = false;
397+
let flags_num = 0;
366398

367399
if (class_directives.length) {
368-
const properties = class_directives.map((directive) => {
369-
has_await ||= directive.metadata.expression.has_await;
370-
371-
return b.init(
400+
const properties = class_directives.map((directive) =>
401+
b.init(
372402
directive.name,
373403
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
374404
? b.id(directive.name)
375405
: /** @type {Expression} */ (context.visit(directive.expression))
376-
);
377-
});
378-
406+
)
407+
);
379408
classes = b.object(properties);
380409
}
381410

382411
if (style_directives.length > 0) {
383-
const properties = style_directives.map((directive) => {
384-
has_await ||= directive.metadata.expression.has_await;
385-
386-
return b.init(
412+
const properties = style_directives.map((directive) =>
413+
b.init(
387414
directive.name,
388415
directive.value === true
389416
? b.id(directive.name)
390417
: build_attribute_value(directive.value, context, transform, true)
391-
);
392-
});
393-
418+
)
419+
);
394420
styles = b.object(properties);
395421
}
396422

397423
if (element.metadata.svg || element.metadata.mathml) {
398-
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
424+
flags_num |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
399425
} else if (is_custom_element_node(element)) {
400-
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
426+
flags_num |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
401427
} else if (element.type === 'RegularElement' && element.name === 'input') {
402-
flags |= ELEMENT_IS_INPUT;
428+
flags_num |= ELEMENT_IS_INPUT;
403429
}
404430

405431
const object = build_spread_object(element, attributes, context, transform);
406-
407432
const css_hash =
408433
element.metadata.scoped && context.state.analysis.css.hash
409434
? b.literal(context.state.analysis.css.hash)
410435
: undefined;
436+
const flags = flags_num ? b.literal(flags_num) : undefined;
411437

412-
const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
413-
414-
let call = b.call('$.attributes', ...args);
415-
416-
context.state.template.push(call);
438+
return { object, css_hash, classes, styles, flags };
417439
}
418440

419441
/**

packages/svelte/src/internal/server/renderer.js

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -158,47 +158,21 @@ export class Renderer {
158158
}
159159

160160
/**
161-
* @overload
162161
* @param {Record<string, any>} attrs
163162
* @param {(renderer: Renderer) => void} fn
163+
* @param {string | undefined} [css_hash]
164+
* @param {Record<string, boolean> | undefined} [classes]
165+
* @param {Record<string, string> | undefined} [styles]
166+
* @param {number | undefined} [flags]
164167
* @returns {void}
165168
*/
166-
/**
167-
* @overload
168-
* @param {Record<string, any>} attrs
169-
* @param {string | undefined} css_hash
170-
* @param {Record<string, boolean> | undefined} classes
171-
* @param {Record<string, string> | undefined} styles
172-
* @param {number | undefined} flags
173-
* @param {(renderer: Renderer) => void} fn
174-
* @returns {void}
175-
*/
176-
/**
177-
* @param {Record<string, any>} attrs
178-
* @param {...any} rest
179-
* @returns {void}
180-
*/
181-
select(attrs, ...rest) {
182-
const callback = /** @type {(renderer: Renderer) => void} */ (rest.pop() ?? (() => {}));
183-
/** @type {[
184-
string | undefined,
185-
Record<string, boolean> | undefined,
186-
Record<string, string> | undefined,
187-
number | undefined
188-
]} */
189-
const [css_hash, classes, styles, flags] = /** @type {[
190-
string | undefined,
191-
Record<string, boolean> | undefined,
192-
Record<string, string> | undefined,
193-
number | undefined
194-
]} */ (rest);
195-
169+
select(attrs, fn, css_hash, classes, styles, flags) {
196170
const { value, ...select_attrs } = attrs;
197171

198172
this.push(`<select${attributes(select_attrs, css_hash, classes, styles, flags)}>`);
199173
this.child((renderer) => {
200174
renderer.local.select_value = value;
201-
callback(renderer);
175+
fn(renderer);
202176
});
203177
this.push('</select>');
204178
}

0 commit comments

Comments
 (0)