From 79ef6451518852665c0b9e482b60e44fae4e811b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 1 Aug 2024 19:52:05 -0400 Subject: [PATCH] chore: improve constants (#12695) * chore: consistently uppercase constants * expose helpers * use helpers * tidy up * why was that there * fix * tweak * fix * tidy up * cheat, to preserve treeshakeability --- .../compiler/phases/1-parse/read/options.js | 6 +- .../src/compiler/phases/2-analyze/index.js | 17 +- .../phases/2-analyze/visitors/Attribute.js | 7 +- .../2-analyze/visitors/BindDirective.js | 6 +- .../phases/2-analyze/visitors/Identifier.js | 7 +- .../2-analyze/visitors/RegularElement.js | 9 +- .../2-analyze/visitors/SvelteElement.js | 6 +- .../phases/2-analyze/visitors/shared/a11y.js | 7 +- .../2-analyze/visitors/shared/element.js | 17 +- .../client/visitors/RegularElement.js | 19 +- .../client/visitors/shared/element.js | 19 +- .../server/visitors/shared/element.js | 32 +- .../svelte/src/compiler/phases/constants.js | 190 ----------- packages/svelte/src/compiler/phases/scope.js | 10 +- packages/svelte/src/constants.js | 91 +---- .../client/dom/blocks/svelte-element.js | 4 +- .../client/dom/elements/attributes.js | 11 +- packages/svelte/src/internal/client/render.js | 12 +- packages/svelte/src/internal/server/index.js | 12 +- packages/svelte/src/utils.js | 311 ++++++++++++++++++ 20 files changed, 421 insertions(+), 372 deletions(-) delete mode 100644 packages/svelte/src/compiler/phases/constants.js diff --git a/packages/svelte/src/compiler/phases/1-parse/read/options.js b/packages/svelte/src/compiler/phases/1-parse/read/options.js index 27527dd162..751fe5f672 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/options.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/options.js @@ -1,6 +1,6 @@ /** @import { ObjectExpression } from 'estree' */ /** @import { SvelteOptionsRaw, Root, SvelteOptions } from '#compiler' */ -import { namespace_mathml, namespace_svg } from '../../../../constants.js'; +import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js'; import * as e from '../../../errors.js'; const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/; @@ -155,9 +155,9 @@ export default function read_options(node) { case 'namespace': { const value = get_static_value(attribute); - if (value === namespace_svg) { + if (value === NAMESPACE_SVG) { component_options.namespace = 'svg'; - } else if (value === namespace_mathml) { + } else if (value === NAMESPACE_MATHML) { component_options.namespace = 'mathml'; } else if ( value === 'html' || diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2a4241a338..6d90ca1617 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -7,13 +7,12 @@ import * as e from '../../errors.js'; import * as w from '../../warnings.js'; import { is_text_attribute } from '../../utils/ast.js'; import * as b from '../../utils/builders.js'; -import { ReservedKeywords, Runes } from '../constants.js'; import { Scope, ScopeRoot, create_scopes, get_rune } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import { create_attribute } from '../nodes.js'; import { analyze_css } from './css/css-analyze.js'; import { prune } from './css/css-prune.js'; -import { hash } from '../../../utils.js'; +import { hash, is_rune } from '../../../utils.js'; import { warn_unused } from './css/css-warn.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; @@ -203,6 +202,8 @@ function get_component_name(filename) { return name[0].toUpperCase() + name.slice(1); } +const RESERVED = ['$$props', '$$restProps', '$$slots']; + /** * @param {Program} ast * @param {ValidatedModuleCompileOptions} options @@ -212,7 +213,7 @@ export function analyze_module(ast, options) { const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null); for (const [name, references] of scope.references) { - if (name[0] !== '$' || ReservedKeywords.includes(name)) continue; + if (name[0] !== '$' || RESERVED.includes(name)) continue; if (name === '$' || name[1] === '$') { e.global_reference_invalid(references[0].node, name); } @@ -257,7 +258,7 @@ export function analyze_component(root, source, options) { // create synthetic bindings for store subscriptions for (const [name, references] of module.scope.references) { - if (name[0] !== '$' || ReservedKeywords.includes(name)) continue; + if (name[0] !== '$' || RESERVED.includes(name)) continue; if (name === '$' || name[1] === '$') { e.global_reference_invalid(references[0].node, name); } @@ -269,7 +270,7 @@ export function analyze_component(root, source, options) { // is referencing a rune and not a global store. if ( options.runes === false || - !Runes.includes(/** @type {any} */ (name)) || + !is_rune(name) || (declaration !== null && // const state = $state(0) is valid (get_rune(declaration.initial, instance.scope) === null || @@ -307,7 +308,7 @@ export function analyze_component(root, source, options) { if (options.runes !== false) { if (declaration === null && /[a-z]/.test(store_name[0])) { e.global_reference_invalid(references[0].node, name); - } else if (declaration !== null && Runes.includes(/** @type {any} */ (name))) { + } else if (declaration !== null && is_rune(name)) { for (const { node, path } of references) { if (path.at(-1)?.type === 'CallExpression') { w.store_rune_conflict(node, store_name); @@ -339,9 +340,7 @@ export function analyze_component(root, source, options) { const component_name = get_component_name(options.filename ?? 'Component'); - const runes = - options.runes ?? - Array.from(module.scope.references).some(([name]) => Runes.includes(/** @type {any} */ (name))); + const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune); // TODO remove all the ?? stuff, we don't need it now that we're validating the config /** @type {ComponentAnalysis} */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index bd6cb5ba4b..824f784a6b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -1,8 +1,7 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ /** @import { Attribute, DelegatedEvent, RegularElement } from '#compiler' */ /** @import { Context } from '../types' */ -import { DelegatedEvents } from '../../../../constants.js'; -import { is_capture_event } from '../../../../utils.js'; +import { is_capture_event, is_delegated } from '../../../../utils.js'; import { get_attribute_chunks, get_attribute_expression, @@ -60,7 +59,7 @@ export function Attribute(node, context) { */ function get_delegated_event(event_name, handler, context) { // Handle delegated event handlers. Bail-out if not a delegated event. - if (!handler || !DelegatedEvents.includes(event_name)) { + if (!handler || !is_delegated(event_name)) { return null; } @@ -119,7 +118,7 @@ function get_delegated_event(event_name, handler, context) { if ( element.type !== 'RegularElement' || element.metadata.has_spread || - !DelegatedEvents.includes(event_name) + !is_delegated(event_name) ) { return non_hoistable; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 183958afa4..ad70cd2717 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -9,8 +9,8 @@ import { validate_no_const_assignment } from './shared/utils.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { binding_properties } from '../../bindings.js'; -import { ContentEditableBindings, SVGElements } from '../../constants.js'; import fuzzymatch from '../../1-parse/utils/fuzzymatch.js'; +import { is_content_editable_binding, is_svg } from '../../../../utils.js'; /** * @param {BindDirective} node @@ -197,7 +197,7 @@ export function BindDirective(node, context) { } } - if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) { + if (node.name === 'offsetWidth' && is_svg(parent.name)) { e.bind_invalid_target( node, node.name, @@ -205,7 +205,7 @@ export function BindDirective(node, context) { ); } - if (ContentEditableBindings.includes(node.name)) { + if (is_content_editable_binding(node.name)) { const contenteditable = /** @type {Attribute} */ ( parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable') ); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 2f586c9207..c00d521610 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -1,11 +1,10 @@ /** @import { Expression, Identifier } from 'estree' */ -/** @import { SvelteNode } from '#compiler' */ /** @import { Context } from '../types' */ import is_reference from 'is-reference'; -import { Runes } from '../../constants.js'; import { should_proxy_or_freeze } from '../../3-transform/client/utils.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; +import { is_rune } from '../../../../utils.js'; /** * @param {Identifier} node @@ -34,7 +33,7 @@ export function Identifier(node, context) { if (context.state.analysis.runes) { if ( - Runes.includes(/** @type {Runes[number]} */ (node.name)) && + is_rune(node.name) && context.state.scope.get(node.name) === null && context.state.scope.get(node.name.slice(1)) === null ) { @@ -49,7 +48,7 @@ export function Identifier(node, context) { current = parent; parent = /** @type {Expression} */ (context.path[--i]); - if (!Runes.includes(/** @type {Runes[number]} */ (name))) { + if (!is_rune(name)) { if (name === '$effect.active') { e.rune_renamed(parent, '$effect.active', '$effect.tracking'); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js index d398dde09a..5c15f70ba2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js @@ -1,13 +1,12 @@ /** @import { RegularElement } from '#compiler' */ /** @import { Context } from '../types' */ -import { is_void } from '../../../../utils.js'; +import { is_mathml, is_svg, is_void } from '../../../../utils.js'; import { is_tag_valid_with_ancestor, is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; -import { MathMLElements, SVGElements } from '../../constants.js'; import { create_attribute } from '../../nodes.js'; import { regex_starts_with_newline } from '../../patterns.js'; import { check_element } from './shared/a11y.js'; @@ -92,8 +91,8 @@ export function RegularElement(node, context) { ); if (context.state.options.namespace !== 'foreign') { - if (SVGElements.includes(node.name)) node.metadata.svg = true; - else if (MathMLElements.includes(node.name)) node.metadata.mathml = true; + node.metadata.svg = is_svg(node.name); + node.metadata.mathml = is_mathml(node.name); } if (context.state.parent_element) { @@ -159,7 +158,7 @@ export function RegularElement(node, context) { context.state.analysis.source[node.end - 2] === '/' && context.state.options.namespace !== 'foreign' && !is_void(node_name) && - !SVGElements.includes(node_name) + !is_svg(node_name) ) { w.element_invalid_self_closing_tag(node, node.name); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js index df9e1da3a9..bb161dcf06 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js @@ -1,6 +1,6 @@ /** @import { Attribute, SvelteElement, Text } from '#compiler' */ /** @import { Context } from '../types' */ -import { namespace_mathml, namespace_svg } from '../../../../constants.js'; +import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js'; import { is_text_attribute } from '../../../utils/ast.js'; import { check_element } from './shared/a11y.js'; import { validate_element } from './shared/element.js'; @@ -23,8 +23,8 @@ export function SvelteElement(node, context) { ); if (xmlns) { - node.metadata.svg = xmlns.value[0].data === namespace_svg; - node.metadata.mathml = xmlns.value[0].data === namespace_mathml; + node.metadata.svg = xmlns.value[0].data === NAMESPACE_SVG; + node.metadata.mathml = xmlns.value[0].data === NAMESPACE_MATHML; } else { let i = context.path.length; while (i--) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js index 0d83e12de4..2213ae102a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js @@ -14,9 +14,9 @@ import { import * as w from '../../../../warnings.js'; import fuzzymatch from '../../../1-parse/utils/fuzzymatch.js'; import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js'; -import { ContentEditableBindings } from '../../../constants.js'; import { walk } from 'zimmerframe'; import { list } from '../../../../utils/string.js'; +import { is_content_editable_binding } from '../../../../../utils.js'; const aria_roles = roles_map.keys(); const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract); @@ -720,10 +720,7 @@ export function check_element(node, state) { has_contenteditable_attr = true; } } - } else if ( - attribute.type === 'BindDirective' && - ContentEditableBindings.includes(attribute.name) - ) { + } else if (attribute.type === 'BindDirective' && is_content_editable_binding(attribute.name)) { has_contenteditable_binding = true; } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js index 2a2249080a..0771f74ccf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js @@ -4,13 +4,24 @@ import { get_attribute_expression, is_expression_attribute } from '../../../../u import { regex_illegal_attribute_character } from '../../../patterns.js'; import * as e from '../../../../errors.js'; import * as w from '../../../../warnings.js'; -import { EventModifiers } from '../../../constants.js'; import { validate_attribute, validate_attribute_name, validate_slot_attribute } from './attribute.js'; +const EVENT_MODIFIERS = [ + 'preventDefault', + 'stopPropagation', + 'stopImmediatePropagation', + 'capture', + 'once', + 'passive', + 'nonpassive', + 'self', + 'trusted' +]; + /** * @param {import('#compiler').RegularElement | SvelteElement} node * @param {Context} context @@ -122,8 +133,8 @@ export function validate_element(node, context) { let has_passive_modifier = false; let conflicting_passive_modifier = ''; for (const modifier of attribute.modifiers) { - if (!EventModifiers.includes(modifier)) { - const list = `${EventModifiers.slice(0, -1).join(', ')} or ${EventModifiers.at(-1)}`; + if (!EVENT_MODIFIERS.includes(modifier)) { + const list = `${EVENT_MODIFIERS.slice(0, -1).join(', ')} or ${EVENT_MODIFIERS.at(-1)}`; e.event_handler_invalid_modifier(attribute, list); } if (modifier === 'passive') { 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 8344c67c0a..740fb2bb4d 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 @@ -3,8 +3,12 @@ /** @import { SourceLocation } from '#shared' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { Scope } from '../../../scope' */ -import { DOMBooleanAttributes } from '../../../../../constants.js'; -import { is_void } from '../../../../../utils.js'; +import { + is_boolean_attribute, + is_dom_property, + is_load_error_element, + is_void +} from '../../../../../utils.js'; import { escape_html } from '../../../../../escaping.js'; import { dev, is_ignored, locator } from '../../../../state.js'; import { @@ -13,7 +17,6 @@ import { is_text_attribute } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; -import { DOMProperties, LoadErrorElements } from '../../../constants.js'; import { is_custom_element_node } from '../../../nodes.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { serialize_get_binding } from '../utils.js'; @@ -130,7 +133,7 @@ export function RegularElement(node, context) { attributes.push(attribute); needs_input_reset = true; needs_content_reset = true; - if (LoadErrorElements.includes(node.name)) { + if (is_load_error_element(node.name)) { might_need_event_replaying = true; } } else if (attribute.type === 'ClassDirective') { @@ -155,7 +158,7 @@ export function RegularElement(node, context) { ) { has_content_editable_binding = true; } - } else if (attribute.type === 'UseDirective' && LoadErrorElements.includes(node.name)) { + } else if (attribute.type === 'UseDirective' && is_load_error_element(node.name)) { might_need_event_replaying = true; } context.visit(attribute); @@ -211,7 +214,7 @@ export function RegularElement(node, context) { if (is_event_attribute(attribute)) { if ( (attribute.name === 'onload' || attribute.name === 'onerror') && - LoadErrorElements.includes(node.name) + is_load_error_element(node.name) ) { might_need_event_replaying = true; } @@ -238,7 +241,7 @@ export function RegularElement(node, context) { // to create the elements it needs. context.state.template.push( ` ${attribute.name}${ - DOMBooleanAttributes.includes(name) && literal_value === true + is_boolean_attribute(name) && literal_value === true ? '' : `="${literal_value === true ? '' : escape_html(literal_value, true)}"` }` @@ -599,7 +602,7 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu update = b.stmt(b.call('$.set_value', node_id, value)); } else if (name === 'checked') { update = b.stmt(b.call('$.set_checked', node_id, value)); - } else if (DOMProperties.includes(name)) { + } else if (is_dom_property(name)) { update = b.stmt(b.assignment('=', b.member(node_id, b.id(name)), value)); } else { const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 5e744d7b8d..7f1127c7c5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -1,11 +1,13 @@ /** @import { Expression, Identifier } from 'estree' */ /** @import { Attribute, ClassDirective, DelegatedEvent, ExpressionMetadata, ExpressionTag, Namespace, OnDirective, RegularElement, StyleDirective, SvelteElement, SvelteNode } from '#compiler' */ /** @import { ComponentContext } from '../../types' */ -import { AttributeAliases } from '../../../../../../constants.js'; -import { is_capture_event } from '../../../../../../utils.js'; +import { + is_capture_event, + is_passive_event, + normalize_attribute +} from '../../../../../../utils.js'; import { get_attribute_expression } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { PassiveEvents } from '../../../../constants.js'; import { serialize_get_binding } from '../../utils.js'; import { serialize_event_handler, serialize_template_literal, serialize_update } from './utils.js'; @@ -119,18 +121,15 @@ export function serialize_attribute_value(value, context) { * @param {{ state: { metadata: { namespace: Namespace }}}} context */ export function get_attribute_name(element, attribute, context) { - let name = attribute.name; if ( !element.metadata.svg && !element.metadata.mathml && context.state.metadata.namespace !== 'foreign' ) { - name = name.toLowerCase(); - if (name in AttributeAliases) { - name = AttributeAliases[name]; - } + return normalize_attribute(attribute.name); } - return name; + + return attribute.name; } /** @@ -236,7 +235,7 @@ export function serialize_event(node, metadata, context) { } else if (node.modifiers.includes('nonpassive')) { args.push(b.literal(false)); } else if ( - PassiveEvents.includes(node.name) && + is_passive_event(node.name) && /** @type {OnDirective} */ (node).type !== 'OnDirective' ) { // For on:something events we don't apply passive behaviour to match Svelte 4. diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 052b3c5d6f..51bcd605fc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -7,11 +7,6 @@ import { is_text_attribute } from '../../../../../utils/ast.js'; import { binding_properties } from '../../../../bindings.js'; -import { - ContentEditableBindings, - LoadErrorElements, - WhitespaceInsensitiveAttributes -} from '../../../../constants.js'; import { create_attribute, create_expression_metadata, @@ -20,11 +15,17 @@ import { import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '../../../../../utils/builders.js'; import { - DOMBooleanAttributes, ELEMENT_IS_NAMESPACED, ELEMENT_PRESERVE_ATTRIBUTE_CASE } from '../../../../../../constants.js'; import { serialize_attribute_value } from './utils.js'; +import { + is_boolean_attribute, + is_content_editable_binding, + is_load_error_element +} from '../../../../../../utils.js'; + +const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; /** * Writes the output to the template output. Some elements may have attributes on them that require the @@ -77,7 +78,7 @@ export function serialize_element_attributes(node, context) { } else if (is_event_attribute(attribute)) { if ( (attribute.name === 'onload' || attribute.name === 'onerror') && - LoadErrorElements.includes(node.name) + is_load_error_element(node.name) ) { events_to_capture.add(attribute.name); } @@ -108,7 +109,7 @@ export function serialize_element_attributes(node, context) { const binding = binding_properties[attribute.name]; if (binding?.omit_in_ssr) continue; - if (ContentEditableBindings.includes(attribute.name)) { + if (is_content_editable_binding(attribute.name)) { content = /** @type {Expression} */ (context.visit(attribute.expression)); } else if (attribute.name === 'value' && node.name === 'textarea') { content = b.call( @@ -170,12 +171,12 @@ export function serialize_element_attributes(node, context) { } else if (attribute.type === 'SpreadAttribute') { attributes.push(attribute); has_spread = true; - if (LoadErrorElements.includes(node.name)) { + if (is_load_error_element(node.name)) { events_to_capture.add('onload'); events_to_capture.add('onerror'); } } else if (attribute.type === 'UseDirective') { - if (LoadErrorElements.includes(node.name)) { + if (is_load_error_element(node.name)) { events_to_capture.add('onload'); events_to_capture.add('onerror'); } @@ -227,14 +228,14 @@ export function serialize_element_attributes(node, context) { serialize_attribute_value( attribute.value, context, - WhitespaceInsensitiveAttributes.includes(name) + WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ) ).value; if (name !== 'class' || literal_value) { context.state.template.push( b.literal( ` ${attribute.name}${ - DOMBooleanAttributes.includes(name) && literal_value === true + is_boolean_attribute(name) && literal_value === true ? '' : `="${literal_value === true ? '' : String(literal_value)}"` }` @@ -245,15 +246,14 @@ export function serialize_element_attributes(node, context) { } const name = get_attribute_name(node, attribute, context); - const is_boolean = DOMBooleanAttributes.includes(name); const value = serialize_attribute_value( attribute.value, context, - WhitespaceInsensitiveAttributes.includes(name) + WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ); context.state.template.push( - b.call('$.attr', b.literal(name), value, is_boolean && b.literal(is_boolean)) + b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) ); } } @@ -344,7 +344,7 @@ function serialize_element_spread_attributes( const value = serialize_attribute_value( attribute.value, context, - WhitespaceInsensitiveAttributes.includes(name) + WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ); return b.prop('init', b.key(name), value); } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js deleted file mode 100644 index 8696692e99..0000000000 --- a/packages/svelte/src/compiler/phases/constants.js +++ /dev/null @@ -1,190 +0,0 @@ -import { AttributeAliases, DOMBooleanAttributes } from '../../constants.js'; - -export const DOMProperties = [ - ...Object.values(AttributeAliases), - 'value', - 'inert', - 'volume', - ...DOMBooleanAttributes -]; - -export const PassiveEvents = ['wheel', 'touchstart', 'touchmove', 'touchend', 'touchcancel']; - -export const Runes = /** @type {const} */ ([ - '$state', - '$state.frozen', - '$state.snapshot', - '$state.is', - '$props', - '$bindable', - '$derived', - '$derived.by', - '$effect', - '$effect.pre', - '$effect.tracking', - '$effect.root', - '$inspect', - '$inspect().with', - '$host' -]); - -/** - * Whitespace inside one of these elements will not result in - * a whitespace node being created in any circumstances. (This - * list is almost certainly very incomplete) - * TODO this is currently unused - */ -export const ElementsWithoutText = ['audio', 'datalist', 'dl', 'optgroup', 'select', 'video']; - -export const ReservedKeywords = ['$$props', '$$restProps', '$$slots']; - -/** Attributes where whitespace can be compacted */ -export const WhitespaceInsensitiveAttributes = ['class', 'style']; - -export const ContentEditableBindings = ['textContent', 'innerHTML', 'innerText']; - -export const LoadErrorElements = [ - 'body', - 'embed', - 'iframe', - 'img', - 'link', - 'object', - 'script', - 'style', - 'track' -]; - -export const SVGElements = [ - 'altGlyph', - 'altGlyphDef', - 'altGlyphItem', - 'animate', - 'animateColor', - 'animateMotion', - 'animateTransform', - 'circle', - 'clipPath', - 'color-profile', - 'cursor', - 'defs', - 'desc', - 'discard', - 'ellipse', - 'feBlend', - 'feColorMatrix', - 'feComponentTransfer', - 'feComposite', - 'feConvolveMatrix', - 'feDiffuseLighting', - 'feDisplacementMap', - 'feDistantLight', - 'feDropShadow', - 'feFlood', - 'feFuncA', - 'feFuncB', - 'feFuncG', - 'feFuncR', - 'feGaussianBlur', - 'feImage', - 'feMerge', - 'feMergeNode', - 'feMorphology', - 'feOffset', - 'fePointLight', - 'feSpecularLighting', - 'feSpotLight', - 'feTile', - 'feTurbulence', - 'filter', - 'font', - 'font-face', - 'font-face-format', - 'font-face-name', - 'font-face-src', - 'font-face-uri', - 'foreignObject', - 'g', - 'glyph', - 'glyphRef', - 'hatch', - 'hatchpath', - 'hkern', - 'image', - 'line', - 'linearGradient', - 'marker', - 'mask', - 'mesh', - 'meshgradient', - 'meshpatch', - 'meshrow', - 'metadata', - 'missing-glyph', - 'mpath', - 'path', - 'pattern', - 'polygon', - 'polyline', - 'radialGradient', - 'rect', - 'set', - 'solidcolor', - 'stop', - 'svg', - 'switch', - 'symbol', - 'text', - 'textPath', - 'tref', - 'tspan', - 'unknown', - 'use', - 'view', - 'vkern' -]; - -export const MathMLElements = [ - 'annotation', - 'annotation-xml', - 'maction', - 'math', - 'merror', - 'mfrac', - 'mi', - 'mmultiscripts', - 'mn', - 'mo', - 'mover', - 'mpadded', - 'mphantom', - 'mprescripts', - 'mroot', - 'mrow', - 'ms', - 'mspace', - 'msqrt', - 'mstyle', - 'msub', - 'msubsup', - 'msup', - 'mtable', - 'mtd', - 'mtext', - 'mtr', - 'munder', - 'munderover', - 'semantics' -]; - -export const EventModifiers = [ - 'preventDefault', - 'stopPropagation', - 'stopImmediatePropagation', - 'capture', - 'once', - 'passive', - 'nonpassive', - 'self', - 'trusted' -]; diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 49ccfed4ec..82f9591a82 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,5 +1,5 @@ /** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ -/** @import { Context, Visitor, Visitors } from 'zimmerframe' */ +/** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AnimateDirective, Binding, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteNode, TransitionDirective, UseDirective } from '#compiler' */ import is_reference from 'is-reference'; import { walk } from 'zimmerframe'; @@ -12,8 +12,7 @@ import { object, unwrap_pattern } from '../utils/ast.js'; -import { Runes } from './constants.js'; -import { is_reserved } from '../../utils.js'; +import { is_reserved, is_rune } from '../../utils.js'; export class Scope { /** @type {ScopeRoot} */ @@ -752,7 +751,6 @@ export function set_scope(node, { next, state }) { * Returns the name of the rune if the given expression is a `CallExpression` using a rune. * @param {Node | EachBlock | null | undefined} node * @param {Scope} scope - * @returns {Runes[number] | null} */ export function get_rune(node, scope) { if (!node) return null; @@ -778,10 +776,10 @@ export function get_rune(node, scope) { joined = n.name + joined; - if (!Runes.includes(/** @type {any} */ (joined))) return null; + if (!is_rune(joined)) return null; const binding = scope.get(n.name); if (binding !== null) return null; // rune name, but references a variable or store - return /** @type {typeof Runes[number] | null} */ (joined); + return joined; } diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 9f6be362b9..9a4aa58193 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -34,87 +34,8 @@ export const UNINITIALIZED = Symbol(); export const FILENAME = Symbol('filename'); export const HMR = Symbol('hmr'); -/** List of elements that require raw contents and should not have SSR comments put in them */ -export const RawTextElements = ['textarea', 'script', 'style', 'title']; - -/** List of Element events that will be delegated */ -export const DelegatedEvents = [ - 'beforeinput', - 'click', - 'change', - 'dblclick', - 'contextmenu', - 'focusin', - 'focusout', - 'input', - 'keydown', - 'keyup', - 'mousedown', - 'mousemove', - 'mouseout', - 'mouseover', - 'mouseup', - 'pointerdown', - 'pointermove', - 'pointerout', - 'pointerover', - 'pointerup', - 'touchend', - 'touchmove', - 'touchstart' -]; - -/** List of Element events that will be delegated and are passive */ -export const PassiveDelegatedEvents = ['touchstart', 'touchmove', 'touchend']; - -/** - * @type {Record} - * List of attribute names that should be aliased to their property names - * because they behave differently between setting them as an attribute and - * setting them as a property. - */ -export const AttributeAliases = { - // no `class: 'className'` because we handle that separately - formnovalidate: 'formNoValidate', - ismap: 'isMap', - nomodule: 'noModule', - playsinline: 'playsInline', - readonly: 'readOnly' -}; - -/** - * Attributes that are boolean, i.e. they are present or not present. - */ -export const DOMBooleanAttributes = [ - 'allowfullscreen', - 'async', - 'autofocus', - 'autoplay', - 'checked', - 'controls', - 'default', - 'disabled', - 'formnovalidate', - 'hidden', - 'indeterminate', - 'ismap', - 'loop', - 'multiple', - 'muted', - 'nomodule', - 'novalidate', - 'open', - 'playsinline', - 'readonly', - 'required', - 'reversed', - 'seamless', - 'selected', - 'webkitdirectory' -]; - -export const namespace_svg = 'http://www.w3.org/2000/svg'; -export const namespace_mathml = 'http://www.w3.org/1998/Math/MathML'; +export const NAMESPACE_SVG = 'http://www.w3.org/2000/svg'; +export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML'; // we use a list of ignorable runtime warnings because not every runtime warning // can be ignored and we want to keep the validation for svelte-ignore in place @@ -126,3 +47,11 @@ export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([ 'ownership_invalid_binding', 'ownership_invalid_mutation' ]); + +/** + * Whitespace inside one of these elements will not result in + * a whitespace node being created in any circumstances. (This + * list is almost certainly very incomplete) + * TODO this is currently unused + */ +export const ELEMENTS_WITHOUT_TEXT = ['audio', 'datalist', 'dl', 'optgroup', 'select', 'video']; diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index e6886bb7fa..0620dde638 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -1,5 +1,5 @@ /** @import { Effect, EffectNodes, TemplateNode } from '#client' */ -import { FILENAME, namespace_svg } from '../../../../constants.js'; +import { FILENAME, NAMESPACE_SVG } from '../../../../constants.js'; import { hydrate_next, hydrate_node, @@ -68,7 +68,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio block(() => { const next_tag = get_tag() || null; - var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? namespace_svg : null; + var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null; // Assumption: Noone changes the namespace but not the tag (what would that even mean?) if (next_tag === tag) return; diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 7e9f943d53..d0e2145937 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -1,13 +1,13 @@ import { DEV } from 'esm-env'; import { hydrating } from '../hydration.js'; import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; -import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js'; +import { NAMESPACE_SVG } from '../../../../constants.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 { queue_idle_task, queue_micro_task } from '../task.js'; -import { is_capture_event } from '../../../../utils.js'; +import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js'; /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need @@ -212,7 +212,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha const opts = {}; const event_handle_key = '$$' + key; let event_name = key.slice(2); - var delegated = DelegatedEvents.includes(event_name); + var delegated = is_delegated(event_name); if (is_capture_event(event_name)) { event_name = event_name.slice(0, -7); @@ -268,8 +268,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha } else { var name = key; if (lowercase_attributes) { - name = name.toLowerCase(); - name = AttributeAliases[name] || name; + name = normalize_attribute(name); } if (setters.includes(name)) { @@ -331,7 +330,7 @@ export function set_dynamic_element_attributes(node, prev, next, css_hash) { /** @type {Element & ElementCSSInlineStyle} */ (node), prev, next, - node.namespaceURI !== namespace_svg, + node.namespaceURI !== NAMESPACE_SVG, css_hash ); } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 2d784872f7..3cc2771e33 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -2,13 +2,8 @@ /** @import { Component, ComponentType, SvelteComponent } from '../../index.js' */ import { DEV } from 'esm-env'; import { clear_text_content, empty, init_operations } from './dom/operations.js'; -import { - HYDRATION_END, - HYDRATION_ERROR, - HYDRATION_START, - PassiveDelegatedEvents -} from '../../constants.js'; -import { flush_sync, push, pop, current_component_context, current_effect } from './runtime.js'; +import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; +import { push, pop, current_component_context, current_effect } from './runtime.js'; import { effect_root, branch } from './reactivity/effects.js'; import { hydrate_next, @@ -27,6 +22,7 @@ import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; +import { is_passive_event } from '../../utils.js'; /** * This is normally true — block effects should run their intro transitions — @@ -195,7 +191,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro if (registered_events.has(event_name)) continue; registered_events.add(event_name); - var passive = PassiveDelegatedEvents.includes(event_name); + var passive = is_passive_event(event_name); // Add the event listener to both the container and the document. // The container listener ensures we catch events from within in case diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 343b9fde0d..8ab7e52162 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -6,8 +6,6 @@ import { is_promise, noop } from '../shared/utils.js'; import { subscribe_to_store } from '../../store/utils.js'; import { UNINITIALIZED, - DOMBooleanAttributes, - RawTextElements, ELEMENT_PRESERVE_ATTRIBUTE_CASE, ELEMENT_IS_NAMESPACED } from '../../constants.js'; @@ -17,13 +15,16 @@ import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { validate_store } from '../shared/validate.js'; -import { is_void } from '../../utils.js'; +import { is_boolean_attribute, is_void } from '../../utils.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter const INVALID_ATTR_NAME_CHAR_REGEX = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; +/** List of elements that require raw contents and should not have SSR comments put in them */ +const RAW_TEXT_ELEMENTS = ['textarea', 'script', 'style', 'title']; + /** * @param {Payload} to_copy * @returns {Payload} @@ -67,7 +68,7 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) if (!is_void(tag)) { children_fn(); - if (!RawTextElements.includes(tag)) { + if (!RAW_TEXT_ELEMENTS.includes(tag)) { payload.out += EMPTY_COMMENT; } payload.out += ``; @@ -225,8 +226,7 @@ export function spread_attributes(attrs, classes, styles, flags = 0) { name = name.toLowerCase(); } - const is_boolean = is_html && DOMBooleanAttributes.includes(name); - attr_str += attr(name, attrs[name], is_boolean); + attr_str += attr(name, attrs[name], is_html && is_boolean_attribute(name)); } return attr_str; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 87d9edd31f..60250075c6 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -105,3 +105,314 @@ export function is_reserved(word) { export function is_capture_event(name) { return name.endsWith('capture') && name !== 'gotpointercapture' && name !== 'lostpointercapture'; } + +/** List of Element events that will be delegated */ +export const DELEGATED_EVENTS = [ + 'beforeinput', + 'click', + 'change', + 'dblclick', + 'contextmenu', + 'focusin', + 'focusout', + 'input', + 'keydown', + 'keyup', + 'mousedown', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + 'pointerdown', + 'pointermove', + 'pointerout', + 'pointerover', + 'pointerup', + 'touchend', + 'touchmove', + 'touchstart' +]; + +/** + * Returns `true` if `event_name` is a delegated event + * @param {string} event_name + */ +export function is_delegated(event_name) { + return DELEGATED_EVENTS.includes(event_name); +} + +/** + * Attributes that are boolean, i.e. they are present or not present. + */ +export const DOM_BOOLEAN_ATTRIBUTES = [ + 'allowfullscreen', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'controls', + 'default', + 'disabled', + 'formnovalidate', + 'hidden', + 'indeterminate', + 'ismap', + 'loop', + 'multiple', + 'muted', + 'nomodule', + 'novalidate', + 'open', + 'playsinline', + 'readonly', + 'required', + 'reversed', + 'seamless', + 'selected', + 'webkitdirectory' +]; + +/** + * Returns `true` if `name` is a boolean attribute + * @param {string} name + */ +export function is_boolean_attribute(name) { + return DOM_BOOLEAN_ATTRIBUTES.includes(name); +} + +/** + * @type {Record} + * List of attribute names that should be aliased to their property names + * because they behave differently between setting them as an attribute and + * setting them as a property. + */ +const ATTRIBUTE_ALIASES = { + // no `class: 'className'` because we handle that separately + formnovalidate: 'formNoValidate', + ismap: 'isMap', + nomodule: 'noModule', + playsinline: 'playsInline', + readonly: 'readOnly' +}; + +/** + * @param {string} name + */ +export function normalize_attribute(name) { + name = name.toLowerCase(); + return ATTRIBUTE_ALIASES[name] ?? name; +} + +const DOM_PROPERTIES = [ + ...DOM_BOOLEAN_ATTRIBUTES, + 'formNoValidate', + 'isMap', + 'noModule', + 'playsInline', + 'readOnly', + 'value', + 'inert', + 'volume' +]; + +/** + * @param {string} name + */ +export function is_dom_property(name) { + return DOM_PROPERTIES.includes(name); +} + +const PASSIVE_EVENTS = ['wheel', 'touchstart', 'touchmove', 'touchend', 'touchcancel']; + +/** + * Returns `true` if `name` is a passive event + * @param {string} name + */ +export function is_passive_event(name) { + return PASSIVE_EVENTS.includes(name); +} + +const CONTENT_EDITABLE_BINDINGS = ['textContent', 'innerHTML', 'innerText']; + +/** @param {string} name */ +export function is_content_editable_binding(name) { + return CONTENT_EDITABLE_BINDINGS.includes(name); +} + +const LOAD_ERROR_ELEMENTS = [ + 'body', + 'embed', + 'iframe', + 'img', + 'link', + 'object', + 'script', + 'style', + 'track' +]; + +/** + * Returns `true` if the element emits `load` and `error` events + * @param {string} name + */ +export function is_load_error_element(name) { + return LOAD_ERROR_ELEMENTS.includes(name); +} + +const SVG_ELEMENTS = [ + 'altGlyph', + 'altGlyphDef', + 'altGlyphItem', + 'animate', + 'animateColor', + 'animateMotion', + 'animateTransform', + 'circle', + 'clipPath', + 'color-profile', + 'cursor', + 'defs', + 'desc', + 'discard', + 'ellipse', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'filter', + 'font', + 'font-face', + 'font-face-format', + 'font-face-name', + 'font-face-src', + 'font-face-uri', + 'foreignObject', + 'g', + 'glyph', + 'glyphRef', + 'hatch', + 'hatchpath', + 'hkern', + 'image', + 'line', + 'linearGradient', + 'marker', + 'mask', + 'mesh', + 'meshgradient', + 'meshpatch', + 'meshrow', + 'metadata', + 'missing-glyph', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialGradient', + 'rect', + 'set', + 'solidcolor', + 'stop', + 'svg', + 'switch', + 'symbol', + 'text', + 'textPath', + 'tref', + 'tspan', + 'unknown', + 'use', + 'view', + 'vkern' +]; + +/** @param {string} name */ +export function is_svg(name) { + return SVG_ELEMENTS.includes(name); +} + +const MATHML_ELEMENTS = [ + 'annotation', + 'annotation-xml', + 'maction', + 'math', + 'merror', + 'mfrac', + 'mi', + 'mmultiscripts', + 'mn', + 'mo', + 'mover', + 'mpadded', + 'mphantom', + 'mprescripts', + 'mroot', + 'mrow', + 'ms', + 'mspace', + 'msqrt', + 'mstyle', + 'msub', + 'msubsup', + 'msup', + 'mtable', + 'mtd', + 'mtext', + 'mtr', + 'munder', + 'munderover', + 'semantics' +]; + +/** @param {string} name */ +export function is_mathml(name) { + return MATHML_ELEMENTS.includes(name); +} + +const RUNES = /** @type {const} */ ([ + '$state', + '$state.frozen', + '$state.snapshot', + '$state.is', + '$props', + '$bindable', + '$derived', + '$derived.by', + '$effect', + '$effect.pre', + '$effect.tracking', + '$effect.root', + '$inspect', + '$inspect().with', + '$host' +]); + +/** + * @param {string} name + * @returns {name is RUNES[number]} + */ +export function is_rune(name) { + return RUNES.includes(/** @type {RUNES[number]} */ (name)); +}