Skip to content

Commit de5cc7c

Browse files
authored
Merge branch 'main' into css-warnings
2 parents 8e3d9fe + ed9bab9 commit de5cc7c

File tree

24 files changed

+277
-22
lines changed

24 files changed

+277
-22
lines changed

.changeset/chilly-rocks-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: introduce `$host` rune, deprecate `createEventDispatcher`

.changeset/popular-walls-hunt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: improve compiled output of multiple call expression in single text node

packages/svelte/src/ambient.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,24 @@ declare function $bindable<T>(t?: T): T;
211211
declare function $inspect<T extends any[]>(
212212
...values: T
213213
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
214+
215+
/**
216+
* Retrieves the `this` reference of the custom element that contains this component. Example:
217+
*
218+
* ```svelte
219+
* <svelte:options customElement="my-element" />
220+
*
221+
* <script>
222+
* function greet(greeting) {
223+
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
224+
* }
225+
* </script>
226+
*
227+
* <button onclick={() => greet('hello')}>say hello</button>
228+
* ```
229+
*
230+
* Only available inside custom element components, and only on the client-side.
231+
*
232+
* https://svelte-5-preview.vercel.app/docs/runes#$host
233+
*/
234+
declare function $host<El extends HTMLElement = HTMLElement>(): El;

packages/svelte/src/compiler/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ const runes = {
187187
'invalid-state-location': (rune) =>
188188
`${rune}(...) can only be used as a variable declaration initializer or a class field`,
189189
'invalid-effect-location': () => `$effect() can only be used as an expression statement`,
190+
'invalid-host-location': () =>
191+
`$host() can only be used inside custom element component instances`,
190192
/**
191193
* @param {boolean} is_binding
192194
* @param {boolean} show_details

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,9 @@ export const validation_runes_js = {
896896
}
897897
},
898898
CallExpression(node, { state, path }) {
899+
if (get_rune(node, state.scope) === '$host') {
900+
error(node, 'invalid-host-location');
901+
}
899902
validate_call_expression(node, state.scope, path);
900903
},
901904
VariableDeclarator(node, { state }) {
@@ -1063,9 +1066,17 @@ export const validation_runes = merge(validation, a11y_validators, {
10631066
}
10641067
},
10651068
CallExpression(node, { state, path }) {
1066-
if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) {
1069+
const rune = get_rune(node, state.scope);
1070+
if (rune === '$bindable' && node.arguments.length > 1) {
10671071
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
1072+
} else if (rune === '$host') {
1073+
if (node.arguments.length > 0) {
1074+
error(node, 'invalid-rune-args-length', '$host', [0]);
1075+
} else if (state.ast_type === 'module' || !state.analysis.custom_element) {
1076+
error(node, 'invalid-host-location');
1077+
}
10681078
}
1079+
10691080
validate_call_expression(node, state.scope, path);
10701081
},
10711082
EachBlock(node, { next, state }) {

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,12 @@ export function client_component(source, analysis, options) {
399399
}
400400

401401
if (analysis.uses_props || analysis.uses_rest_props) {
402+
const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')];
403+
if (analysis.custom_element) {
404+
to_remove.push(b.literal('$$host'));
405+
}
402406
component_block.body.unshift(
403-
b.const(
404-
'$$sanitized_props',
405-
b.call(
406-
'$.rest_props',
407-
b.id('$$props'),
408-
b.array([b.literal('children'), b.literal('$$slots'), b.literal('$$events')])
409-
)
410-
)
407+
b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove)))
411408
);
412409
}
413410

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,10 @@ export const javascript_visitors_runes = {
380380
CallExpression(node, context) {
381381
const rune = get_rune(node, context.state.scope);
382382

383+
if (rune === '$host') {
384+
return b.id('$$props.$$host');
385+
}
386+
383387
if (rune === '$effect.active') {
384388
return b.call('$.effect_active');
385389
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
13961396

13971397
state.template.push(' ');
13981398

1399-
const [contains_call_expression, value] = serialize_template_literal(sequence, visit);
1399+
const [contains_call_expression, value] = serialize_template_literal(sequence, visit, state);
14001400

14011401
const update = b.stmt(b.call('$.set_text', text_id, value));
14021402

@@ -1511,25 +1511,39 @@ function serialize_attribute_value(attribute_value, context) {
15111511
}
15121512
}
15131513

1514-
return serialize_template_literal(attribute_value, context.visit);
1514+
return serialize_template_literal(attribute_value, context.visit, context.state);
15151515
}
15161516

15171517
/**
15181518
* @param {Array<import('#compiler').Text | import('#compiler').ExpressionTag>} values
15191519
* @param {(node: import('#compiler').SvelteNode) => any} visit
1520+
* @param {import("../types.js").ComponentClientTransformState} state
15201521
* @returns {[boolean, import('estree').TemplateLiteral]}
15211522
*/
1522-
function serialize_template_literal(values, visit) {
1523+
function serialize_template_literal(values, visit, state) {
15231524
/** @type {import('estree').TemplateElement[]} */
15241525
const quasis = [];
15251526

15261527
/** @type {import('estree').Expression[]} */
15271528
const expressions = [];
15281529
let contains_call_expression = false;
1530+
let contains_multiple_call_expression = false;
15291531
quasis.push(b.quasi(''));
15301532

15311533
for (let i = 0; i < values.length; i++) {
15321534
const node = values[i];
1535+
1536+
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
1537+
if (contains_call_expression) {
1538+
contains_multiple_call_expression = true;
1539+
}
1540+
contains_call_expression = true;
1541+
}
1542+
}
1543+
1544+
for (let i = 0; i < values.length; i++) {
1545+
const node = values[i];
1546+
15331547
if (node.type === 'Text') {
15341548
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
15351549
last.value.raw += sanitize_template_string(node.data);
@@ -1539,11 +1553,23 @@ function serialize_template_literal(values, visit) {
15391553
last.value.raw += sanitize_template_string(node.expression.value + '');
15401554
}
15411555
} else {
1542-
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
1543-
contains_call_expression = true;
1544-
}
1556+
if (contains_multiple_call_expression) {
1557+
const id = b.id(state.scope.generate('stringified_text'));
15451558

1546-
expressions.push(b.call('$.stringify', visit(node.expression)));
1559+
state.init.push(
1560+
b.const(
1561+
id,
1562+
b.call(
1563+
// In runes mode, we want things to be fine-grained - but not in legacy mode
1564+
state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
1565+
b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)))
1566+
)
1567+
)
1568+
);
1569+
expressions.push(b.call('$.get', id));
1570+
} else {
1571+
expressions.push(b.call('$.stringify', visit(node.expression)));
1572+
}
15471573
quasis.push(b.quasi('', i + 1 === values.length));
15481574
}
15491575
}
@@ -1586,7 +1612,7 @@ export const template_visitors = {
15861612
declaration.id,
15871613
b.call(
15881614
// In runes mode, we want things to be fine-grained - but not in legacy mode
1589-
state.options.runes ? '$.derived' : '$.derived_safe_equal',
1615+
state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
15901616
b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
15911617
)
15921618
)
@@ -1623,7 +1649,7 @@ export const template_visitors = {
16231649

16241650
state.init.push(
16251651
// In runes mode, we want things to be fine-grained - but not in legacy mode
1626-
b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn))
1652+
b.const(tmp, b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', fn))
16271653
);
16281654

16291655
// we need to eagerly evaluate the expression in order to hit any
@@ -2972,7 +2998,7 @@ export const template_visitors = {
29722998
b.assignment(
29732999
'=',
29743000
b.member(b.id('$.document'), b.id('title')),
2975-
serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit)[1]
3001+
serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit, state)[1]
29763002
)
29773003
)
29783004
);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,10 @@ const javascript_visitors_runes = {
785785
CallExpression(node, context) {
786786
const rune = get_rune(node, context.state.scope);
787787

788+
if (rune === '$host') {
789+
return b.id('undefined');
790+
}
791+
788792
if (rune === '$effect.active') {
789793
return b.literal(false);
790794
}

packages/svelte/src/compiler/phases/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([
4040
'$effect.active',
4141
'$effect.root',
4242
'$inspect',
43-
'$inspect().with'
43+
'$inspect().with',
44+
'$host'
4445
]);
4546

4647
/**

0 commit comments

Comments
 (0)