Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: alternative single key attachments #15091

Draft
wants to merge 14 commits into
base: attachments-2
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/svelte/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/compiler/index.js

/action.d.ts
/attachments.d.ts
/animate.d.ts
/compiler.d.ts
/easing.d.ts
Expand Down
4 changes: 3 additions & 1 deletion packages/svelte/elements.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8

import { Attachments } from 'svelte/attachments';

// Note: We also allow `null` as a valid value because Svelte treats this the same as `undefined`

type Booleanish = boolean | 'true' | 'false';
Expand Down Expand Up @@ -861,7 +863,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
[key: `data-${string}`]: any;

// allow any attachment
[key: symbol]: (node: T) => void | (() => void);
[Attachments]: Array<(node: T) => void | (() => void)>;
}

export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {});
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"./action": {
"types": "./types/index.d.ts"
},
"./attachments": {
"types": "./types/index.d.ts",
"default": "./src/attachments/index.js"
},
"./animate": {
"types": "./types/index.d.ts",
"default": "./src/animate/index.js"
Expand Down
12 changes: 11 additions & 1 deletion packages/svelte/scripts/generate-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ const pkg = JSON.parse(fs.readFileSync(`${dir}/package.json`, 'utf-8'));

// For people not using moduleResolution: 'bundler', we need to generate these files. Think about removing this in Svelte 6 or 7
// It may look weird, but the imports MUST be ending with index.js to be properly resolved in all TS modes
for (const name of ['action', 'animate', 'easing', 'motion', 'store', 'transition', 'legacy']) {
for (const name of [
'action',
'attachments',
'animate',
'easing',
'motion',
'store',
'transition',
'legacy'
]) {
fs.writeFileSync(`${dir}/${name}.d.ts`, "import './types/index.js';\n");
}

Expand All @@ -29,6 +38,7 @@ await createBundle({
modules: {
[pkg.name]: `${dir}/src/index.d.ts`,
[`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`,
[`${pkg.name}/attachments`]: `${dir}/src/attachments/public.d.ts`,
[`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/attachments/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const Attachments = Symbol.for('svelte.attachments');
4 changes: 4 additions & 0 deletions packages/svelte/src/attachments/public.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* A unique symbol used for defining the attachments to be applied to an element or component.
*/
export const Attachments: unique symbol;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement, SpreadElement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
Expand All @@ -10,6 +10,39 @@ import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';

/**
* @param {Property} prop
* @param {ComponentContext} context
* @returns {boolean}
*/
function is_attachments_prop(prop, context) {
if (prop.key.type !== 'Identifier') {
return false;
}

const binding = context.state.scope?.get?.(prop.key.name) ?? undefined;
const expression = prop.computed && prop.key.type === 'Identifier' ? prop.key : binding?.initial;

if (!expression || expression.type !== 'CallExpression') {
return false;
}

if (
expression.callee.type !== 'MemberExpression' ||
expression.callee.object.type !== 'Identifier' ||
expression.callee.object.name !== 'Symbol' ||
expression.callee.property.type !== 'Identifier' ||
expression.callee.property.name !== 'for' ||
expression.arguments.length !== 1 ||
expression.arguments[0].type !== 'Literal' ||
expression.arguments[0].value !== 'svelte.attachments'
) {
return false;
}

return true;
}

/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {string} component_name
Expand Down Expand Up @@ -50,6 +83,9 @@ export function build_component(node, component_name, context, anchor = context.
/** @type {ExpressionStatement[]} */
const binding_initializers = [];

/** @type {Array<Expression | SpreadElement>} */
const all_attachments = [];

/**
* If this component has a slot property, it is a named slot within another component. In this case
* the slot scope applies to the component itself, too, and not just its children.
Expand Down Expand Up @@ -129,6 +165,14 @@ export function build_component(node, component_name, context, anchor = context.
} else {
props_and_spreads.push(expression);
}

// Handle attachments from spread attributes
const symbol = b.call('Symbol.for', b.literal('svelte.attachments'));
const member = b.member(expression, symbol, true); // computed property
const default_empty = b.array([]);
const attachments_expr = b.logical('??', member, default_empty);

all_attachments.push(b.spread(attachments_expr));
} else if (attribute.type === 'Attribute') {
if (attribute.name.startsWith('--')) {
custom_css_props.push(
Expand Down Expand Up @@ -262,24 +306,36 @@ export function build_component(node, component_name, context, anchor = context.
}
}
} else if (attribute.type === 'Attachment') {
// TODO do we need to create a derived here?
for (const attachment of attribute.attachments) {
push_prop(
b.prop(
'get',
b.call('Symbol'),
/** @type {Expression} */ (
context.visit(attachment.type === 'SpreadElement' ? attachment.argument : attachment)
),
true
)
);
if (attachment.type === 'SpreadElement') {
const visited = /** @type {ExpressionStatement} */ (context.visit(attachment.argument));
all_attachments.push(b.spread(visited.expression));
} else {
const visited = /** @type {ExpressionStatement} */ (context.visit(attachment));
all_attachments.push(visited.expression);
}
}
}
}

delayed_props.forEach((fn) => fn());

if (all_attachments.length > 0) {
const attachment_symbol = b.member(
b.id('Symbol'),
b.call('for', b.literal('svelte.attachments'))
);

push_prop(
b.prop(
'init',
attachment_symbol,
b.array(all_attachments),
true // Mark as computed property
)
);
}

if (slot_scope_applies_to_itself) {
context.state.init.push(...lets);
}
Expand Down
38 changes: 28 additions & 10 deletions packages/svelte/src/compiler/utils/builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,21 @@ export function function_declaration(id, params, body) {
}

/**
* @param {string} name
* @param {string | ESTree.Expression} name_or_expr
* @param {ESTree.Statement[]} body
* @returns {ESTree.Property & { value: ESTree.FunctionExpression}}}
*/
export function get(name, body) {
return prop('get', key(name), function_builder(null, [], block(body)));
* @returns {ESTree.Property & { value: ESTree.FunctionExpression }}
*/
export function get(name_or_expr, body) {
let key_expr;
let computed = false;
if (typeof name_or_expr === 'string') {
key_expr = key(name_or_expr);
computed = key_expr.type !== 'Identifier';
} else {
key_expr = name_or_expr;
computed = true;
}
return prop('get', key_expr, function_builder(null, [], block(body)), computed);
}

/**
Expand Down Expand Up @@ -380,12 +389,21 @@ export function sequence(expressions) {
}

/**
* @param {string} name
* @param {string | ESTree.Expression} name_or_expr
* @param {ESTree.Statement[]} body
* @returns {ESTree.Property & { value: ESTree.FunctionExpression}}
*/
export function set(name, body) {
return prop('set', key(name), function_builder(null, [id('$$value')], block(body)));
* @returns {ESTree.Property & { value: ESTree.FunctionExpression }}
*/
export function set(name_or_expr, body) {
let key_expr;
let computed = false;
if (typeof name_or_expr === 'string') {
key_expr = key(name_or_expr);
computed = key_expr.type !== 'Identifier';
} else {
key_expr = name_or_expr;
computed = true;
}
return prop('set', key_expr, function_builder(null, [id('$$value')], block(body)), computed);
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export const STATE_SYMBOL = Symbol('$state');
export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const ATTACHMENTS_SYMBOL = Symbol.for('svelte.attachments');
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '../../constants.js';
import { ATTACHMENTS_SYMBOL, LOADING_ATTR_SYMBOL } from '../../constants.js';
import { queue_idle_task } from '../task.js';
import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js';
import {
Expand Down Expand Up @@ -416,8 +416,11 @@ export function set_attributes(
}
}

for (let symbol of Object.getOwnPropertySymbols(next)) {
attach(element, () => next[symbol]);
const attachments = next[ATTACHMENTS_SYMBOL];
if (attachments) {
for (let attachment of attachments) {
attach(element, () => attachment);
}
}

return current;
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"acorn-typescript": ["./src/compiler/phases/1-parse/ambient.d.ts"],
"svelte": ["./src/index.d.ts"],
"svelte/action": ["./src/action/public.d.ts"],
"svelte/attachments": ["./src/attachments/public.d.ts"],
"svelte/compiler": ["./src/compiler/public.d.ts"],
"svelte/events": ["./src/events/public.d.ts"],
"svelte/internal/client": ["./src/internal/client/index.js"],
Expand Down
29 changes: 19 additions & 10 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,15 @@ declare module 'svelte/action' {
export {};
}

declare module 'svelte/attachments' {
/**
* A unique symbol used for defining the attachments to be applied to an element or component.
*/
export const Attachments: unique symbol;

export {};
}

declare module 'svelte/animate' {
// todo: same as Transition, should it be shared?
export interface AnimationConfig {
Expand Down Expand Up @@ -1881,10 +1890,10 @@ declare module 'svelte/motion' {
* const tween = Tween.of(() => number);
* </script>
* ```
*
*
*/
static of<U>(fn: () => U, options?: TweenedOptions<U> | undefined): Tween<U>;

constructor(value: T, options?: TweenedOptions<T>);
/**
* Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
Expand All @@ -1903,21 +1912,21 @@ declare module 'svelte/motion' {

declare module 'svelte/reactivity' {
export class SvelteDate extends Date {

constructor(...params: any[]);
#private;
}
export class SvelteSet<T> extends Set<T> {

constructor(value?: Iterable<T> | null | undefined);

add(value: T): this;
#private;
}
export class SvelteMap<K, V> extends Map<K, V> {

constructor(value?: Iterable<readonly [K, V]> | null | undefined);

set(key: K, value: V): this;
#private;
}
Expand All @@ -1927,7 +1936,7 @@ declare module 'svelte/reactivity' {
}
const REPLACE: unique symbol;
export class SvelteURLSearchParams extends URLSearchParams {

[REPLACE](params: URLSearchParams): void;
#private;
}
Expand Down Expand Up @@ -1999,7 +2008,7 @@ declare module 'svelte/reactivity' {
*/
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
class ReactiveValue<T> {

constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
Expand Down Expand Up @@ -2064,7 +2073,7 @@ declare module 'svelte/reactivity/window' {
get current(): number | undefined;
};
class ReactiveValue<T> {

constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
Expand Down
Loading