From 531e75d314075506748fb01457b396bf7765d3b0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 19 Dec 2024 23:41:33 -0500 Subject: [PATCH 01/18] parse attachments --- .../compiler/phases/1-parse/state/element.js | 20 ++- .../svelte/src/compiler/types/template.d.ts | 8 +- .../samples/attachments/input.svelte | 1 + .../samples/attachments/output.json | 141 ++++++++++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 packages/svelte/tests/parser-modern/samples/attachments/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/attachments/output.json diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 66946a8f8d22..a79ff5f206f7 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -480,7 +480,7 @@ function read_static_attribute(parser) { /** * @param {Parser} parser - * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null} + * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.UseTag | null} */ function read_attribute(parser) { const start = parser.index; @@ -488,6 +488,24 @@ function read_attribute(parser) { if (parser.eat('{')) { parser.allow_whitespace(); + if (parser.eat('@use')) { + parser.require_whitespace(); + + const expression = read_expression(parser); + parser.allow_whitespace(); + parser.eat('}', true); + + /** @type {AST.UseTag} */ + const use = { + type: 'UseTag', + start, + end: parser.index, + expression + }; + + return use; + } + if (parser.eat('...')) { const expression = read_expression(parser); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 97a25df4a758..6b4c7d6d5208 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -174,6 +174,12 @@ export namespace AST { }; } + /** A `{@use foo(...)} tag */ + export interface UseTag extends BaseNode { + type: 'UseTag'; + expression: Expression; + } + /** An `animate:` directive */ export interface AnimateDirective extends BaseNode { type: 'AnimateDirective'; @@ -273,7 +279,7 @@ export namespace AST { interface BaseElement extends BaseNode { name: string; - attributes: Array; + attributes: Array; fragment: Fragment; } diff --git a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte new file mode 100644 index 000000000000..042bcbb98b04 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte @@ -0,0 +1 @@ +
{}} {@use (node) => {}}>
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json new file mode 100644 index 000000000000..68ee77107288 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json @@ -0,0 +1,141 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 51, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "RegularElement", + "start": 0, + "end": 51, + "name": "div", + "attributes": [ + { + "type": "UseTag", + "start": 5, + "end": 24, + "expression": { + "type": "ArrowFunctionExpression", + "start": 11, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 11 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [ + { + "type": "Identifier", + "start": 12, + "end": 16, + "loc": { + "start": { + "line": 1, + "column": 12 + }, + "end": { + "line": 1, + "column": 16 + } + }, + "name": "node" + } + ], + "body": { + "type": "BlockStatement", + "start": 21, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 21 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "body": [] + } + } + }, + { + "type": "UseTag", + "start": 25, + "end": 44, + "expression": { + "type": "ArrowFunctionExpression", + "start": 31, + "end": 43, + "loc": { + "start": { + "line": 1, + "column": 31 + }, + "end": { + "line": 1, + "column": 43 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [ + { + "type": "Identifier", + "start": 32, + "end": 36, + "loc": { + "start": { + "line": 1, + "column": 32 + }, + "end": { + "line": 1, + "column": 36 + } + }, + "name": "node" + } + ], + "body": { + "type": "BlockStatement", + "start": 41, + "end": 43, + "loc": { + "start": { + "line": 1, + "column": 41 + }, + "end": { + "line": 1, + "column": 43 + } + }, + "body": [] + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + } + ] + }, + "options": null +} \ No newline at end of file From b29e1e3a6f4621feff34ffeb002c971e79354cb3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 19 Dec 2024 23:53:06 -0500 Subject: [PATCH 02/18] basic attachments working --- .../3-transform/client/transform-client.js | 2 ++ .../client/visitors/RegularElement.js | 6 +++++- .../3-transform/client/visitors/UseTag.js | 21 +++++++++++++++++++ .../svelte/src/compiler/types/template.d.ts | 1 + .../client/dom/elements/attachments.js | 11 ++++++++++ packages/svelte/src/internal/client/index.js | 1 + .../samples/attachment-basic/_config.js | 6 ++++++ .../samples/attachment-basic/main.svelte | 1 + 8 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/UseTag.js create mode 100644 packages/svelte/src/internal/client/dom/elements/attachments.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 90901d29ce7d..d6398d76051f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -56,6 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js'; import { TransitionDirective } from './visitors/TransitionDirective.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UseDirective } from './visitors/UseDirective.js'; +import { UseTag } from './visitors/UseTag.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; /** @type {Visitors} */ @@ -131,6 +132,7 @@ const visitors = { TransitionDirective, UpdateExpression, UseDirective, + UseTag, VariableDeclaration }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 2c2c287f1275..0983aeb7e7d0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -82,7 +82,7 @@ export function RegularElement(node, context) { /** @type {AST.StyleDirective[]} */ const style_directives = []; - /** @type {Array} */ + /** @type {Array} */ const other_directives = []; /** @type {ExpressionStatement[]} */ @@ -152,6 +152,10 @@ export function RegularElement(node, context) { has_use = true; other_directives.push(attribute); break; + + case 'UseTag': + other_directives.push(attribute); + break; } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseTag.js new file mode 100644 index 000000000000..76bc83279e9f --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseTag.js @@ -0,0 +1,21 @@ +/** @import { Expression } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {AST.UseTag} node + * @param {ComponentContext} context + */ +export function UseTag(node, context) { + context.state.init.push( + b.stmt( + b.call( + '$.attach', + context.state.node, + b.thunk(/** @type {Expression} */ (context.visit(node.expression))) + ) + ) + ); + context.next(); +} diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 6b4c7d6d5208..a8e32b402a62 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -553,6 +553,7 @@ export namespace AST { | AST.Attribute | AST.SpreadAttribute | Directive + | AST.UseTag | AST.Comment | Block; diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js new file mode 100644 index 000000000000..c7299c371663 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -0,0 +1,11 @@ +import { effect } from '../../reactivity/effects.js'; + +/** + * @param {Element} node + * @param {() => (node: Element) => void} get_fn + */ +export function attach(node, get_fn) { + effect(() => { + get_fn()(node); + }); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index f22c33babc52..05135dc76474 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -27,6 +27,7 @@ export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; export { append_styles } from './dom/css.js'; export { action } from './dom/elements/actions.js'; +export { attach } from './dom/elements/attachments.js'; export { remove_input_defaults, set_attribute, diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js new file mode 100644 index 000000000000..1be47370691a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
DIV
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte new file mode 100644 index 000000000000..593065e87661 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte @@ -0,0 +1 @@ +
node.textContent = node.nodeName}>
From 2329284d9265454099e183a738ec8d3666752640 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 20 Dec 2024 00:23:04 -0500 Subject: [PATCH 03/18] working --- .../phases/2-analyze/visitors/shared/component.js | 3 ++- .../client/visitors/shared/component.js | 9 +++++++++ .../src/internal/client/dom/elements/attributes.js | 5 +++++ .../samples/attachment-component/Child.svelte | 5 +++++ .../samples/attachment-component/_config.js | 6 ++++++ .../samples/attachment-component/main.svelte | 5 +++++ .../samples/attachment-reactive/_config.js | 14 ++++++++++++++ .../samples/attachment-reactive/main.svelte | 6 ++++++ .../samples/attachment-svelte-element/_config.js | 6 ++++++ .../samples/attachment-svelte-element/main.svelte | 1 + 10 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 04bf3d2ff3bf..4f54606b7f83 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -74,7 +74,8 @@ export function visit_component(node, context) { attribute.type !== 'SpreadAttribute' && attribute.type !== 'LetDirective' && attribute.type !== 'OnDirective' && - attribute.type !== 'BindDirective' + attribute.type !== 'BindDirective' && + attribute.type !== 'UseTag' ) { e.component_invalid_directive(attribute); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 068971145c6e..ef21123082ac 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -252,6 +252,15 @@ export function build_component(node, component_name, context, anchor = context. ); } } + } else if (attribute.type === 'UseTag') { + push_prop( + b.prop( + 'init', + b.call('Symbol', b.literal('@use')), + b.thunk(/** @type {Expression} */ (context.visit(attribute.expression))), + true + ) + ); } } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 9c62d684c183..dedba8103116 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -13,6 +13,7 @@ import { set_active_effect, set_active_reaction } from '../../runtime.js'; +import { attach } from './attachments.js'; /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need @@ -407,6 +408,10 @@ export function set_attributes( } } + for (let symbol of Object.getOwnPropertySymbols(next)) { + attach(element, next[symbol]); + } + return current; } diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte new file mode 100644 index 000000000000..6760da61faeb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte @@ -0,0 +1,5 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js new file mode 100644 index 000000000000..fa1c9b059904 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
set from component
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte new file mode 100644 index 000000000000..c8b918fa6fe8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte @@ -0,0 +1,5 @@ + + + node.textContent = 'set from component'} /> diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js new file mode 100644 index 000000000000..7d0502590b5d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
1
`, + + test: ({ assert, target }) => { + const btn = target.querySelector('button'); + + flushSync(() => btn?.click()); + assert.htmlEqual(target.innerHTML, `
2
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte new file mode 100644 index 000000000000..b66560ff7d82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte @@ -0,0 +1,6 @@ + + +
node.textContent = value}>
+ diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js new file mode 100644 index 000000000000..1be47370691a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
DIV
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte new file mode 100644 index 000000000000..0b615883f94b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte @@ -0,0 +1 @@ + node.textContent = node.nodeName}> From 0c914eb185f8413438f78327bd888220ea175e5c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 20 Dec 2024 15:17:07 -0500 Subject: [PATCH 04/18] rename to attach --- .../compiler/phases/1-parse/state/element.js | 8 +-- .../2-analyze/visitors/shared/component.js | 2 +- .../3-transform/client/transform-client.js | 4 +- .../visitors/{UseTag.js => AttachTag.js} | 4 +- .../client/visitors/RegularElement.js | 4 +- .../client/visitors/shared/component.js | 4 +- .../svelte/src/compiler/types/template.d.ts | 10 +-- .../samples/attachments/input.svelte | 2 +- .../samples/attachments/output.json | 64 +++++++++---------- .../samples/attachment-basic/main.svelte | 2 +- .../samples/attachment-component/main.svelte | 2 +- .../samples/attachment-reactive/main.svelte | 2 +- .../attachment-svelte-element/main.svelte | 2 +- 13 files changed, 55 insertions(+), 55 deletions(-) rename packages/svelte/src/compiler/phases/3-transform/client/visitors/{UseTag.js => AttachTag.js} (85%) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index a79ff5f206f7..99ebf870fa2e 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -480,7 +480,7 @@ function read_static_attribute(parser) { /** * @param {Parser} parser - * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.UseTag | null} + * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null} */ function read_attribute(parser) { const start = parser.index; @@ -488,16 +488,16 @@ function read_attribute(parser) { if (parser.eat('{')) { parser.allow_whitespace(); - if (parser.eat('@use')) { + if (parser.eat('@attach')) { parser.require_whitespace(); const expression = read_expression(parser); parser.allow_whitespace(); parser.eat('}', true); - /** @type {AST.UseTag} */ + /** @type {AST.AttachTag} */ const use = { - type: 'UseTag', + type: 'AttachTag', start, end: parser.index, expression diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 4f54606b7f83..88f59c0e5bc6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -75,7 +75,7 @@ export function visit_component(node, context) { attribute.type !== 'LetDirective' && attribute.type !== 'OnDirective' && attribute.type !== 'BindDirective' && - attribute.type !== 'UseTag' + attribute.type !== 'AttachTag' ) { e.component_invalid_directive(attribute); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index d6398d76051f..40792c3d5ea9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -56,7 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js'; import { TransitionDirective } from './visitors/TransitionDirective.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UseDirective } from './visitors/UseDirective.js'; -import { UseTag } from './visitors/UseTag.js'; +import { AttachTag } from './visitors/AttachTag.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; /** @type {Visitors} */ @@ -132,7 +132,7 @@ const visitors = { TransitionDirective, UpdateExpression, UseDirective, - UseTag, + AttachTag, VariableDeclaration }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js similarity index 85% rename from packages/svelte/src/compiler/phases/3-transform/client/visitors/UseTag.js rename to packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 76bc83279e9f..062604cacc16 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UseTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -4,10 +4,10 @@ import * as b from '../../../../utils/builders.js'; /** - * @param {AST.UseTag} node + * @param {AST.AttachTag} node * @param {ComponentContext} context */ -export function UseTag(node, context) { +export function AttachTag(node, context) { context.state.init.push( b.stmt( b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 0983aeb7e7d0..9ceaebc8d362 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -82,7 +82,7 @@ export function RegularElement(node, context) { /** @type {AST.StyleDirective[]} */ const style_directives = []; - /** @type {Array} */ + /** @type {Array} */ const other_directives = []; /** @type {ExpressionStatement[]} */ @@ -153,7 +153,7 @@ export function RegularElement(node, context) { other_directives.push(attribute); break; - case 'UseTag': + case 'AttachTag': other_directives.push(attribute); break; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index ef21123082ac..5596ccf70dac 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -252,11 +252,11 @@ export function build_component(node, component_name, context, anchor = context. ); } } - } else if (attribute.type === 'UseTag') { + } else if (attribute.type === 'AttachTag') { push_prop( b.prop( 'init', - b.call('Symbol', b.literal('@use')), + b.call('Symbol', b.literal('@attach')), b.thunk(/** @type {Expression} */ (context.visit(attribute.expression))), true ) diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index a8e32b402a62..78827886f88d 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -174,9 +174,9 @@ export namespace AST { }; } - /** A `{@use foo(...)} tag */ - export interface UseTag extends BaseNode { - type: 'UseTag'; + /** A `{@attach foo(...)} tag */ + export interface AttachTag extends BaseNode { + type: 'AttachTag'; expression: Expression; } @@ -279,7 +279,7 @@ export namespace AST { interface BaseElement extends BaseNode { name: string; - attributes: Array; + attributes: Array; fragment: Fragment; } @@ -553,7 +553,7 @@ export namespace AST { | AST.Attribute | AST.SpreadAttribute | Directive - | AST.UseTag + | AST.AttachTag | AST.Comment | Block; diff --git a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte index 042bcbb98b04..9faae8d1bf40 100644 --- a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte +++ b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte @@ -1 +1 @@ -
{}} {@use (node) => {}}>
+
{}} {@attach (node) => {}}>
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json index 68ee77107288..42e9880fccdd 100644 --- a/packages/svelte/tests/parser-modern/samples/attachments/output.json +++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json @@ -2,7 +2,7 @@ "css": null, "js": [], "start": 0, - "end": 51, + "end": 57, "type": "Root", "fragment": { "type": "Fragment", @@ -10,25 +10,25 @@ { "type": "RegularElement", "start": 0, - "end": 51, + "end": 57, "name": "div", "attributes": [ { - "type": "UseTag", + "type": "AttachTag", "start": 5, - "end": 24, + "end": 27, "expression": { "type": "ArrowFunctionExpression", - "start": 11, - "end": 23, + "start": 14, + "end": 26, "loc": { "start": { "line": 1, - "column": 11 + "column": 14 }, "end": { "line": 1, - "column": 23 + "column": 26 } }, "id": null, @@ -38,16 +38,16 @@ "params": [ { "type": "Identifier", - "start": 12, - "end": 16, + "start": 15, + "end": 19, "loc": { "start": { "line": 1, - "column": 12 + "column": 15 }, "end": { "line": 1, - "column": 16 + "column": 19 } }, "name": "node" @@ -55,16 +55,16 @@ ], "body": { "type": "BlockStatement", - "start": 21, - "end": 23, + "start": 24, + "end": 26, "loc": { "start": { "line": 1, - "column": 21 + "column": 24 }, "end": { "line": 1, - "column": 23 + "column": 26 } }, "body": [] @@ -72,21 +72,21 @@ } }, { - "type": "UseTag", - "start": 25, - "end": 44, + "type": "AttachTag", + "start": 28, + "end": 50, "expression": { "type": "ArrowFunctionExpression", - "start": 31, - "end": 43, + "start": 37, + "end": 49, "loc": { "start": { "line": 1, - "column": 31 + "column": 37 }, "end": { "line": 1, - "column": 43 + "column": 49 } }, "id": null, @@ -96,16 +96,16 @@ "params": [ { "type": "Identifier", - "start": 32, - "end": 36, + "start": 38, + "end": 42, "loc": { "start": { "line": 1, - "column": 32 + "column": 38 }, "end": { "line": 1, - "column": 36 + "column": 42 } }, "name": "node" @@ -113,16 +113,16 @@ ], "body": { "type": "BlockStatement", - "start": 41, - "end": 43, + "start": 47, + "end": 49, "loc": { "start": { "line": 1, - "column": 41 + "column": 47 }, "end": { "line": 1, - "column": 43 + "column": 49 } }, "body": [] @@ -138,4 +138,4 @@ ] }, "options": null -} \ No newline at end of file +} diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte index 593065e87661..1a1f74e4a94a 100644 --- a/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte @@ -1 +1 @@ -
node.textContent = node.nodeName}>
+
node.textContent = node.nodeName}>
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte index c8b918fa6fe8..3468ee50a2cd 100644 --- a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte @@ -2,4 +2,4 @@ import Child from './Child.svelte'; - node.textContent = 'set from component'} /> + node.textContent = 'set from component'} /> diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte index b66560ff7d82..9fa3cfdb6798 100644 --- a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte @@ -2,5 +2,5 @@ let value = $state(1); -
node.textContent = value}>
+
node.textContent = value}>
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte index 0b615883f94b..bd4b52342f32 100644 --- a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte @@ -1 +1 @@ - node.textContent = node.nodeName}> + node.textContent = node.nodeName}> From 1988ba4345a7d9b8b8e0649e216b4644fbae54b7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 20 Dec 2024 22:39:53 -0500 Subject: [PATCH 05/18] fix --- .../3-transform/client/visitors/shared/component.js | 5 +++-- .../svelte/src/internal/client/dom/elements/attributes.js | 2 +- .../runtime-runes/samples/attachment-spread/_config.js | 8 ++++++++ .../runtime-runes/samples/attachment-spread/main.svelte | 7 +++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 5596ccf70dac..75cd1a4466a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -253,11 +253,12 @@ export function build_component(node, component_name, context, anchor = context. } } } else if (attribute.type === 'AttachTag') { + // TODO do we need to create a derived here? push_prop( b.prop( - 'init', + 'get', b.call('Symbol', b.literal('@attach')), - b.thunk(/** @type {Expression} */ (context.visit(attribute.expression))), + /** @type {Expression} */ (context.visit(attribute.expression)), true ) ); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index dedba8103116..5cc1286662e2 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -409,7 +409,7 @@ export function set_attributes( } for (let symbol of Object.getOwnPropertySymbols(next)) { - attach(element, next[symbol]); + attach(element, () => next[symbol]); } return current; diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js new file mode 100644 index 000000000000..96fc20745025 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js @@ -0,0 +1,8 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, logs, target }) { + assert.deepEqual(logs, ['hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte new file mode 100644 index 000000000000..804c9cfede47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte @@ -0,0 +1,7 @@ + + +
From e1b940c45b106e9df1a59b5e1d1f65c027bfb005 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 23 Dec 2024 14:15:36 -0500 Subject: [PATCH 06/18] restrict which symbols are recognised as attachment keys --- packages/svelte/package.json | 4 ++++ packages/svelte/scripts/generate-types.js | 1 + packages/svelte/src/attachments/index.js | 4 ++++ .../client/visitors/shared/component.js | 2 +- .../client/dom/elements/attachments.js | 19 +++++++++++++++++++ .../client/dom/elements/attributes.js | 10 ++++++---- packages/svelte/src/internal/client/index.js | 2 +- .../samples/attachment-spread/main.svelte | 4 +++- 8 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 packages/svelte/src/attachments/index.js diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 05169a7bc2ab..83f2ae6dc296 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -33,6 +33,10 @@ "types": "./types/index.d.ts", "default": "./src/animate/index.js" }, + "./attachments": { + "types": "./types/index.d.ts", + "default": "./src/attachments/index.js" + }, "./compiler": { "types": "./types/index.d.ts", "require": "./compiler/index.js", diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index d44afe8205a8..1f718e9b5b62 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -30,6 +30,7 @@ await createBundle({ [pkg.name]: `${dir}/src/index.d.ts`, [`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`, [`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`, + [`${pkg.name}/attachments`]: `${dir}/src/attachments/public.d.ts`, [`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js new file mode 100644 index 000000000000..6a66024c863e --- /dev/null +++ b/packages/svelte/src/attachments/index.js @@ -0,0 +1,4 @@ +export { + create_attachment_key as createAttachmentKey, + is_attachment_key as isAttachmentKey +} from '../internal/client/dom/elements/attachments.js'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 75cd1a4466a8..2b9c47475bf6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -257,7 +257,7 @@ export function build_component(node, component_name, context, anchor = context. push_prop( b.prop( 'get', - b.call('Symbol', b.literal('@attach')), + b.call('$.create_attachment_key'), /** @type {Expression} */ (context.visit(attribute.expression)), true ) diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index c7299c371663..2bfa84d305b4 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -1,5 +1,24 @@ import { effect } from '../../reactivity/effects.js'; +const key = `@attach-${Math.random().toString(36).slice(2)}`; +const name = `Symbol(${key})`; + +// TODO this feels a bit belt-and-braces to me, tbh — are we sure we need it? +/** + * Creates a `Symbol` that Svelte recognises as an attachment key + */ +export function create_attachment_key() { + return Symbol(key); +} + +/** + * Returns `true` if the symbol was created with `createAttachmentKey` + * @param {string | symbol} key + */ +export function is_attachment_key(key) { + return typeof key === 'symbol' && key.toString() === name; +} + /** * @param {Element} node * @param {() => (node: Element) => void} get_fn diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 5cc1286662e2..2f3e301502a4 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -13,7 +13,7 @@ import { set_active_effect, set_active_reaction } from '../../runtime.js'; -import { attach } from './attachments.js'; +import { attach, is_attachment_key } from './attachments.js'; /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need @@ -242,8 +242,8 @@ export function set_custom_element_data(node, prop, value) { /** * Spreads attributes onto a DOM element, taking into account the currently set attributes * @param {Element & ElementCSSInlineStyle} element - * @param {Record | undefined} prev - * @param {Record} next New attributes - this function mutates this object + * @param {Record | undefined} prev + * @param {Record} next New attributes - this function mutates this object * @param {string} [css_hash] * @param {boolean} [preserve_attribute_case] * @param {boolean} [is_custom_element] @@ -409,7 +409,9 @@ export function set_attributes( } for (let symbol of Object.getOwnPropertySymbols(next)) { - attach(element, () => next[symbol]); + if (is_attachment_key(symbol)) { + attach(element, () => next[symbol]); + } } return current; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 05135dc76474..95cf6a9958da 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -27,7 +27,7 @@ export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; export { append_styles } from './dom/css.js'; export { action } from './dom/elements/actions.js'; -export { attach } from './dom/elements/attachments.js'; +export { attach, create_attachment_key, is_attachment_key } from './dom/elements/attachments.js'; export { remove_input_defaults, set_attribute, diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte index 804c9cfede47..dbd8c47ada1e 100644 --- a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte @@ -1,6 +1,8 @@ From 0690ba225af5e173f9a40d0f31d47631491a4986 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 13 Jan 2025 17:54:59 -0500 Subject: [PATCH 07/18] allow cleanup to be returned directly --- packages/svelte/src/internal/client/dom/elements/attachments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index 2bfa84d305b4..a912012aa464 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -25,6 +25,6 @@ export function is_attachment_key(key) { */ export function attach(node, get_fn) { effect(() => { - get_fn()(node); + return get_fn()(node); }); } From e0620a130b0e2d8c2c1141955ea18dec7cdd53ad Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 13 Jan 2025 18:46:14 -0500 Subject: [PATCH 08/18] changeset --- .changeset/poor-days-pay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-days-pay.md diff --git a/.changeset/poor-days-pay.md b/.changeset/poor-days-pay.md new file mode 100644 index 000000000000..8fbff1058686 --- /dev/null +++ b/.changeset/poor-days-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: attachments From 2ae3aa0993b076fded6ccc6098ad2d53a09876d4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 13 Jan 2025 18:52:01 -0500 Subject: [PATCH 09/18] fix --- packages/svelte/scripts/generate-types.js | 2 +- .../client/dom/elements/attachments.js | 2 +- packages/svelte/types/index.d.ts | 22 ++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index 1f718e9b5b62..27422272ca01 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -30,7 +30,7 @@ await createBundle({ [pkg.name]: `${dir}/src/index.d.ts`, [`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`, [`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`, - [`${pkg.name}/attachments`]: `${dir}/src/attachments/public.d.ts`, + [`${pkg.name}/attachments`]: `${dir}/src/attachments/index.js`, [`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index a912012aa464..7376a4d4e60d 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -1,6 +1,6 @@ import { effect } from '../../reactivity/effects.js'; -const key = `@attach-${Math.random().toString(36).slice(2)}`; +const key = `@attach-${/*@__PURE__*/ Math.random().toString(36).slice(2)}`; const name = `Symbol(${key})`; // TODO this feels a bit belt-and-braces to me, tbh — are we sure we need it? diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d00b2b01ed18..90e928fb7e33 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -621,6 +621,19 @@ declare module 'svelte/animate' { export {}; } +declare module 'svelte/attachments' { + /** + * Creates a `Symbol` that Svelte recognises as an attachment key + */ + export function createAttachmentKey(): symbol; + /** + * Returns `true` if the symbol was created with `createAttachmentKey` + * */ + export function isAttachmentKey(key: string | symbol): boolean; + + export {}; +} + declare module 'svelte/compiler' { import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { SourceMap } from 'magic-string'; @@ -1050,6 +1063,12 @@ declare module 'svelte/compiler' { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); } + /** A `{@attach foo(...)} tag */ + export interface AttachTag extends BaseNode { + type: 'AttachTag'; + expression: Expression; + } + /** An `animate:` directive */ export interface AnimateDirective extends BaseNode { type: 'AnimateDirective'; @@ -1132,7 +1151,7 @@ declare module 'svelte/compiler' { interface BaseElement extends BaseNode { name: string; - attributes: Array; + attributes: Array; fragment: Fragment; } @@ -1322,6 +1341,7 @@ declare module 'svelte/compiler' { | AST.Attribute | AST.SpreadAttribute | Directive + | AST.AttachTag | AST.Comment | Block; From 704642720a3f5d0ed1b9abb6e82c95cb6f5e142a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 13 Jan 2025 20:18:07 -0500 Subject: [PATCH 10/18] lint --- packages/svelte/src/compiler/phases/1-parse/read/script.js | 2 +- packages/svelte/src/compiler/phases/1-parse/read/style.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 9d9ed3a1efdf..629012781188 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module']; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.Script} */ export function read_script(parser, start, attributes) { diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 29e8a0e54143..8f01af3b7507 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.CSS.StyleSheet} */ export default function read_style(parser, start, attributes) { From 85cc9bc6139d6d64de35caa3676de01a623a7e19 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 16:14:53 -0500 Subject: [PATCH 11/18] remove createAttachmentKey/isAttachmentKey --- packages/svelte/package.json | 4 ---- packages/svelte/scripts/generate-types.js | 1 - packages/svelte/src/attachments/index.js | 4 ---- .../client/visitors/shared/component.js | 2 +- .../client/dom/elements/attachments.js | 19 ------------------- .../client/dom/elements/attributes.js | 6 ++---- packages/svelte/src/internal/client/index.js | 2 +- .../samples/attachment-spread/main.svelte | 4 +--- packages/svelte/types/index.d.ts | 13 ------------- 9 files changed, 5 insertions(+), 50 deletions(-) delete mode 100644 packages/svelte/src/attachments/index.js diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8478e47115b9..f426b97be4aa 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -33,10 +33,6 @@ "types": "./types/index.d.ts", "default": "./src/animate/index.js" }, - "./attachments": { - "types": "./types/index.d.ts", - "default": "./src/attachments/index.js" - }, "./compiler": { "types": "./types/index.d.ts", "require": "./compiler/index.js", diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index 27422272ca01..d44afe8205a8 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -30,7 +30,6 @@ await createBundle({ [pkg.name]: `${dir}/src/index.d.ts`, [`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`, [`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`, - [`${pkg.name}/attachments`]: `${dir}/src/attachments/index.js`, [`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js deleted file mode 100644 index 6a66024c863e..000000000000 --- a/packages/svelte/src/attachments/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { - create_attachment_key as createAttachmentKey, - is_attachment_key as isAttachmentKey -} from '../internal/client/dom/elements/attachments.js'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index d97cbe023546..2882da7d2f52 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -266,7 +266,7 @@ export function build_component(node, component_name, context, anchor = context. push_prop( b.prop( 'get', - b.call('$.create_attachment_key'), + b.call('Symbol'), /** @type {Expression} */ (context.visit(attribute.expression)), true ) diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index 7376a4d4e60d..e0bfc5b1d1ce 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -1,24 +1,5 @@ import { effect } from '../../reactivity/effects.js'; -const key = `@attach-${/*@__PURE__*/ Math.random().toString(36).slice(2)}`; -const name = `Symbol(${key})`; - -// TODO this feels a bit belt-and-braces to me, tbh — are we sure we need it? -/** - * Creates a `Symbol` that Svelte recognises as an attachment key - */ -export function create_attachment_key() { - return Symbol(key); -} - -/** - * Returns `true` if the symbol was created with `createAttachmentKey` - * @param {string | symbol} key - */ -export function is_attachment_key(key) { - return typeof key === 'symbol' && key.toString() === name; -} - /** * @param {Element} node * @param {() => (node: Element) => void} get_fn diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 27ae3c990bf3..17362cedea1d 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -13,7 +13,7 @@ import { set_active_effect, set_active_reaction } from '../../runtime.js'; -import { attach, is_attachment_key } from './attachments.js'; +import { attach } from './attachments.js'; import { clsx } from '../../../shared/attributes.js'; /** @@ -417,9 +417,7 @@ export function set_attributes( } for (let symbol of Object.getOwnPropertySymbols(next)) { - if (is_attachment_key(symbol)) { - attach(element, () => next[symbol]); - } + attach(element, () => next[symbol]); } return current; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 447a67304c7b..d0ad116b67bf 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -27,7 +27,7 @@ export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; export { append_styles } from './dom/css.js'; export { action } from './dom/elements/actions.js'; -export { attach, create_attachment_key, is_attachment_key } from './dom/elements/attachments.js'; +export { attach } from './dom/elements/attachments.js'; export { remove_input_defaults, set_attribute, diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte index dbd8c47ada1e..804c9cfede47 100644 --- a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte @@ -1,8 +1,6 @@ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 90e928fb7e33..7b3a77ae6fce 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -621,19 +621,6 @@ declare module 'svelte/animate' { export {}; } -declare module 'svelte/attachments' { - /** - * Creates a `Symbol` that Svelte recognises as an attachment key - */ - export function createAttachmentKey(): symbol; - /** - * Returns `true` if the symbol was created with `createAttachmentKey` - * */ - export function isAttachmentKey(key: string | symbol): boolean; - - export {}; -} - declare module 'svelte/compiler' { import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { SourceMap } from 'magic-string'; From bec570891a3bf640ddf7a5d1cb349ff7b5bf6565 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 17:19:04 -0500 Subject: [PATCH 12/18] fix spreading of symbol properties onto component --- packages/svelte/src/internal/client/reactivity/props.js | 5 +++++ .../samples/attachment-component-spread/Child.svelte | 5 +++++ .../samples/attachment-component-spread/_config.js | 6 ++++++ .../samples/attachment-component-spread/main.svelte | 9 +++++++++ 4 files changed, 25 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 3e5a0258c744..4017be56ab85 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -231,9 +231,14 @@ const spread_props_handler = { for (let p of target.props) { if (is_function(p)) p = p(); + for (const key in p) { if (!keys.includes(key)) keys.push(key); } + + for (const key of Object.getOwnPropertySymbols(p)) { + if (!keys.includes(key)) keys.push(key); + } } return keys; diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte new file mode 100644 index 000000000000..6760da61faeb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte @@ -0,0 +1,5 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js new file mode 100644 index 000000000000..fa1c9b059904 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
set from component
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte new file mode 100644 index 000000000000..cf2551a8cfa8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte @@ -0,0 +1,9 @@ + + + From afab15097c5e75b11ab666d62d98ab788d6dd950 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 17:19:41 -0500 Subject: [PATCH 13/18] types --- packages/svelte/elements.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 604403f0a261..7b67e2a07ebd 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -859,6 +859,9 @@ export interface HTMLAttributes extends AriaAttributes, D // allow any data- attribute [key: `data-${string}`]: any; + + // allow any attachment + [key: symbol]: (node: T) => void | (() => void); } export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {}); From 7e5d4d9e6cd1eee09ae85a5fdb128dc2f666853e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 17:28:08 -0500 Subject: [PATCH 14/18] fix --- packages/svelte/src/internal/client/reactivity/props.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 4017be56ab85..df384c911c4a 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -231,6 +231,7 @@ const spread_props_handler = { for (let p of target.props) { if (is_function(p)) p = p(); + if (!p) continue; for (const key in p) { if (!keys.includes(key)) keys.push(key); From c599e90d6e42210a70a532e574cbfd624454dae9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 17:39:06 -0500 Subject: [PATCH 15/18] update name --- packages/svelte/src/compiler/phases/1-parse/state/element.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 99ebf870fa2e..13d83e0e09f9 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -496,14 +496,14 @@ function read_attribute(parser) { parser.eat('}', true); /** @type {AST.AttachTag} */ - const use = { + const attachment = { type: 'AttachTag', start, end: parser.index, expression }; - return use; + return attachment; } if (parser.eat('...')) { From 869977157ec3ed618b5e36d3f6ed29a92257543b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 17:50:38 -0500 Subject: [PATCH 16/18] reserve ability to use sequence expressions in future --- .../2-analyze/visitors/shared/component.js | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 88f59c0e5bc6..aca87fab811c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -1,3 +1,4 @@ +/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { AnalysisState, Context } from '../../types' */ import * as e from '../../../../errors.js'; @@ -92,15 +93,10 @@ export function visit_component(node, context) { validate_attribute(attribute, node); if (is_expression_attribute(attribute)) { - const expression = get_attribute_expression(attribute); - if (expression.type === 'SequenceExpression') { - let i = /** @type {number} */ (expression.start); - while (--i > 0) { - const char = context.state.analysis.source[i]; - if (char === '(') break; // parenthesized sequence expressions are ok - if (char === '{') e.attribute_invalid_sequence_expression(expression); - } - } + disallow_unparenthesized_sequences( + get_attribute_expression(attribute), + context.state.analysis.source + ); } } @@ -114,6 +110,10 @@ export function visit_component(node, context) { if (attribute.type === 'BindDirective' && attribute.name !== 'this') { context.state.analysis.uses_component_bindings = true; } + + if (attribute.type === 'AttachTag') { + disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source); + } } // If the component has a slot attribute — `` — @@ -159,3 +159,18 @@ export function visit_component(node, context) { context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state); } } + +/** + * @param {Expression} expression + * @param {string} source + */ +function disallow_unparenthesized_sequences(expression, source) { + if (expression.type === 'SequenceExpression') { + let i = /** @type {number} */ (expression.start); + while (--i > 0) { + const char = source[i]; + if (char === '(') break; // parenthesized sequence expressions are ok + if (char === '{') e.attribute_invalid_sequence_expression(expression); + } + } +} From 1664fd811811029f2fe0e54b2cf778d35361dfd8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 23:04:28 -0500 Subject: [PATCH 17/18] Update packages/svelte/src/internal/client/dom/elements/attachments.js Co-authored-by: Leonidaz --- packages/svelte/src/internal/client/dom/elements/attachments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index e0bfc5b1d1ce..f52b238559fd 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -6,6 +6,6 @@ import { effect } from '../../reactivity/effects.js'; */ export function attach(node, get_fn) { effect(() => { - return get_fn()(node); + return (get_fn())?.(node); }); } From 64021619323c72aed9ab49ef2fd2856932081f45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Jan 2025 23:07:54 -0500 Subject: [PATCH 18/18] actually let's do this instead --- .../svelte/src/internal/client/dom/elements/attachments.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index f52b238559fd..6e3089a384c1 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -6,6 +6,10 @@ import { effect } from '../../reactivity/effects.js'; */ export function attach(node, get_fn) { effect(() => { - return (get_fn())?.(node); + const fn = get_fn(); + + // we use `&&` rather than `?.` so that things like + // `{@attach DEV && something_dev_only()}` work + return fn && fn(node); }); }