Merge remote-tracking branch 'upstream/main' into fix-spread-registering-dep

pull/11290/head
paoloricciuti 2 years ago
commit 46d8f1a79e

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: disallow binding to component exports in runes mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly interpret empty aria- attribute

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: disallow mixing on:click and onclick syntax

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: make hr, script and template valid select children

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: disallow mounting a snippet

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove `bind_prop` in runes mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: only inject push/init/pop when necessary

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: provide migration helper

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: introduce types to express bindability

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: avoid hoisting error by using 'let' instead of 'var'

@ -19,6 +19,7 @@
"beige-flies-wash",
"beige-mirrors-listen",
"beige-rabbits-shave",
"beige-seas-share",
"big-cars-serve",
"big-eggs-flash",
"big-eyes-carry",
@ -214,6 +215,7 @@
"lovely-items-turn",
"lovely-rules-eat",
"lucky-colts-remember",
"lucky-geckos-swim",
"lucky-schools-hang",
"lucky-toes-begin",
"many-rockets-give",
@ -261,7 +263,9 @@
"orange-dingos-poke",
"orange-yaks-protect",
"pink-bikes-agree",
"pink-goats-promise",
"pink-mayflies-tie",
"plenty-starfishes-dress",
"polite-dolphins-care",
"polite-pumpkins-guess",
"polite-ravens-study",
@ -429,6 +433,7 @@
"tidy-chefs-taste",
"tidy-starfishes-allow",
"tiny-kings-whisper",
"tiny-meals-deliver",
"tough-radios-punch",
"twelve-dragons-join",
"twelve-onions-juggle",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: mark `accessors` and `immutable` as deprecated

@ -2,6 +2,14 @@ packages/**/dist/*.js
packages/**/build/*.js
packages/**/npm/**/*
packages/**/config/*.js
packages/svelte/messages/**/*.md
packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js
packages/svelte/src/internal/client/errors.js
packages/svelte/src/internal/client/warnings.js
packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*
packages/svelte/tests/**/_actual*

@ -33,13 +33,19 @@ export default [
ignores: [
'**/*.d.ts',
'**/tests',
'packages/svelte/scripts/process-messages/templates/*.js',
'packages/svelte/src/compiler/errors.js',
'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js',
'packages/svelte/src/internal/shared/warnings.js',
'packages/svelte/compiler/index.js',
// documentation can contain invalid examples
'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules
'sites/svelte-5-preview/**',
// wasn't checked previously, reenable at some point
'sites/svelte.dev/**'
'sites/svelte.dev/**',
'tmp/**'
]
}
];

@ -1,5 +1,31 @@
# svelte
## 5.0.0-next.115
### Patch Changes
- fix: remove `bind_prop` in runes mode ([#11321](https://github.com/sveltejs/svelte/pull/11321))
- fix: mark `accessors` and `immutable` as deprecated ([#11277](https://github.com/sveltejs/svelte/pull/11277))
## 5.0.0-next.114
### Patch Changes
- feat: introduce types to express bindability ([#11225](https://github.com/sveltejs/svelte/pull/11225))
## 5.0.0-next.113
### Patch Changes
- breaking: disallow binding to component exports in runes mode ([#11238](https://github.com/sveltejs/svelte/pull/11238))
## 5.0.0-next.112
### Patch Changes
- fix: avoid hoisting error by using 'let' instead of 'var' ([#11291](https://github.com/sveltejs/svelte/pull/11291))
## 5.0.0-next.111
### Patch Changes

@ -0,0 +1,67 @@
## bind_invalid_checkbox_value
> Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
## bind_invalid_export
> Component %component% has an export named `%key%` that a consumer component is trying to access using `bind:%key%`, which is disallowed. Instead, use `bind:this` (e.g. `<%name% bind:this={component} />`) and then access the property on the bound component instance (e.g. `component.%key%`)
## bind_not_bindable
> A component is attempting to bind to a non-bindable property `%key%` belonging to %component% (i.e. `<%name% bind:%key%={...}>`). To mark a property as bindable: `let { %key% = $bindable() } = $props()`
## each_key_duplicate
> Keyed each block has duplicate key at indexes %a% and %b%
> Keyed each block has duplicate key `%value%` at indexes %a% and %b%
## effect_in_teardown
> `%rune%` cannot be used inside an effect cleanup function
## effect_orphan
> `%rune%` can only be used inside an effect (e.g. during component initialisation)
## effect_update_depth_exceeded
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
## hydration_missing_marker_close
> Missing hydration closing marker
## hydration_missing_marker_open
> Missing hydration opening marker
## lifecycle_legacy_only
> `%name%(...)` cannot be used in runes mode
## props_invalid_value
> Cannot do `bind:%key%={undefined}` when `%key%` has a fallback value
## props_rest_readonly
> Rest element properties of `$props()` such as `%property%` are readonly
## rune_outside_svelte
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
## state_prototype_fixed
> Cannot set prototype of `$state` object
## state_unsafe_mutation
> Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. This can lead to unexpected errors and possibly cause infinite loops.
>
> If the object is not meant to be reactive, declare it without `$state`
## svelte_component_invalid_this_value
> The `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined

@ -0,0 +1,21 @@
## hydration_attribute_changed
> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
## hydration_mismatch
> Hydration failed because the initial UI does not match what was rendered on the server
## lifecycle_double_unmount
> Tried to unmount a component that was not mounted
## ownership_invalid_binding
> %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
## ownership_invalid_mutation
> Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead

@ -0,0 +1,11 @@
## options_invalid_value
> Invalid compiler option: %details%
## options_removed
> Invalid compiler option: %details%
## options_unrecognised
> Unrecognised compiler option %keypath%

@ -0,0 +1,135 @@
## bindable_invalid_location
> `$bindable()` can only be used inside a `$props()` declaration
## constant_assignment
> Cannot assign to %thing%
## constant_binding
> Cannot bind to %thing%
## declaration_duplicate
> `%name%` has already been declared
## declaration_duplicate_module_import
> Cannot declare same variable name which is imported inside `<script context="module">`
## derived_invalid_export
> Cannot export derived state from a module. To expose the current derived value, export a function returning its value
## dollar_binding_invalid
> The $ name is reserved, and cannot be used for variables and imports
## dollar_prefix_invalid
> The $ prefix is reserved, and cannot be used for variables and imports
## each_item_invalid_assignment
> Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`)
## effect_invalid_placement
> `$effect()` can only be used as an expression statement
## global_reference_invalid
> `%name%` is an illegal variable name. To reference a global variable called `%name%`, use `globalThis.%name%`
## host_invalid_placement
> `$host()` can only be used inside custom element component instances
## legacy_export_invalid
> Cannot use `export let` in runes mode — use `$props()` instead
## legacy_props_invalid
> Cannot use `$$props` in runes mode
## legacy_reactive_statement_invalid
> `$:` is not allowed in runes mode, use `$derived` or `$effect` instead
## legacy_rest_props_invalid
> Cannot use `$$restProps` in runes mode
## module_illegal_default_export
> A component cannot have a default export
## props_duplicate
> Cannot use `$props()` more than once
## props_invalid_identifier
> `$props()` can only be used with an object destructuring pattern
## props_invalid_pattern
> `$props()` assignment must not contain nested properties or computed keys
## props_invalid_placement
> `$props()` can only be used at the top level of components as a variable declaration initializer
## reactive_declaration_cycle
> Cyclical dependency detected: %cycle%
## rune_invalid_arguments
> `%rune%` cannot be called with arguments
## rune_invalid_arguments_length
> `%rune%` must be called with %args%
## rune_invalid_computed_property
> Cannot access a computed property of a rune
## rune_invalid_name
> `%name%` is not a valid rune
## rune_invalid_usage
> Cannot use `%rune%` rune in non-runes mode
## rune_missing_parentheses
> Cannot use rune without parentheses
## runes_mode_invalid_import
> %name% cannot be used in runes mode
## snippet_parameter_assignment
> Cannot reassign or bind to snippet parameter
## state_invalid_export
> Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
## state_invalid_placement
> `%rune%(...)` can only be used as a variable declaration initializer or a class field
## store_invalid_scoped_subscription
> Cannot subscribe to stores that are not declared at the top level of the component
## store_invalid_subscription
> Cannot reference store value inside `<script context="module">`

@ -0,0 +1,47 @@
## css_empty_declaration
> Declaration cannot be empty
## css_expected_identifier
> Expected a valid CSS identifier
## css_global_block_invalid_combinator
> A :global {...} block cannot follow a %name% combinator
## css_global_block_invalid_declaration
> A :global {...} block can only contain rules, not declarations
## css_global_block_invalid_list
> A :global {...} block cannot be part of a selector list with more than one item
## css_global_block_invalid_modifier
> A :global {...} block cannot modify an existing selector
## css_global_invalid_placement
> :global(...) can be at the start or end of a selector sequence, but not in the middle
## css_global_invalid_selector
> :global(...) must contain exactly one selector
## css_global_invalid_selector_list
> :global(...) must not contain type or universal selectors when used in a compound selector
## css_nesting_selector_invalid_placement
> Nesting selectors can only be used inside a rule
## css_selector_invalid
> Invalid selector
## css_type_selector_invalid_placement
> :global(...) must not be followed with a type selector

@ -0,0 +1,369 @@
## animation_duplicate
> An element can only have one 'animate' directive
## animation_invalid_placement
> An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block
## animation_missing_key
> An element that uses the `animate:` directive must be the only child of a keyed `{#each ...}` block. Did you forget to add a key to your each block?
## attribute_contenteditable_dynamic
> 'contenteditable' attribute cannot be dynamic if element uses two-way binding
## attribute_contenteditable_missing
> 'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings
## attribute_duplicate
> Attributes need to be unique
## attribute_empty_shorthand
> Attribute shorthand cannot be empty
## attribute_invalid_event_handler
> Event attribute must be a JavaScript expression, not a string
## attribute_invalid_multiple
> 'multiple' attribute must be static if select uses two-way binding
## attribute_invalid_name
> '%name%' is not a valid attribute name
## attribute_invalid_sequence_expression
> Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses
## attribute_invalid_type
> 'type' attribute must be a static text value if input uses two-way binding
## bind_invalid_expression
> Can only bind to an Identifier or MemberExpression
## bind_invalid_name
> `bind:%name%` is not a valid binding
> `bind:%name%` is not a valid binding. %explanation%
## bind_invalid_target
> `bind:%name%` can only be used with %elements%
## bind_invalid_value
> Can only bind to state or props
## block_duplicate_clause
> %name% cannot appear more than once within a block
## block_invalid_continuation_placement
> {:...} block is invalid at this position (did you forget to close the preceeding element or block?)
## block_invalid_elseif
> 'elseif' should be 'else if'
## block_invalid_placement
> {#%name% ...} block cannot be %location%
## block_unclosed
> Block was left open
## block_unexpected_close
> Unexpected block closing tag
## component_invalid_directive
> This type of directive is not valid on components
## const_tag_invalid_expression
> {@const ...} must be an assignment
## const_tag_invalid_placement
> `{@const}` must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, `<svelte:fragment>` or `<Component>`
## debug_tag_invalid_arguments
> {@debug ...} arguments must be identifiers, not arbitrary expressions
## directive_invalid_value
> Directive value must be a JavaScript expression enclosed in curly braces
## directive_missing_name
> `%type%` name cannot be empty
## element_invalid_closing_tag
> `</%name%>` attempted to close an element that was not open
## element_invalid_closing_tag_autoclosed
> `</%name%>` attempted to close element that was already automatically closed by `<%reason%>` (cannot nest `<%reason%>` inside `<%name%>`)
## element_invalid_tag_name
> Expected valid tag name
## element_unclosed
> `<%name%>` was left open
## event_handler_invalid_component_modifier
> Event modifiers other than 'once' can only be used on DOM elements
## event_handler_invalid_modifier
> Valid event modifiers are %list%
## event_handler_invalid_modifier_combination
> The '%modifier1%' and '%modifier2%' modifiers cannot be used together
## expected_attribute_value
> Expected attribute value
## expected_block_type
> Expected 'if', 'each', 'await', 'key' or 'snippet'
## expected_identifier
> Expected an identifier
## expected_pattern
> Expected identifier or destructure pattern
## expected_token
> Expected token %token%
## expected_whitespace
> Expected whitespace
## js_parse_error
> %message%
## let_directive_invalid_placement
> `let:` directive at invalid position
## mixed_event_handler_syntaxes
> Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax.
## node_invalid_placement
> %thing% is invalid inside <%parent%>
## render_tag_invalid_call_expression
> Calling a snippet function using apply, bind or call is not allowed
## render_tag_invalid_expression
> `{@render ...}` tags can only contain call expressions
## render_tag_invalid_spread_argument
> cannot use spread arguments in `{@render ...}` tags
## script_duplicate
> A component can have a single top-level `<script>` element and/or a single top-level `<script context="module">` element
## script_invalid_context
> If the context attribute is supplied, its value must be "module"
## slot_attribute_duplicate
> Duplicate slot name '%name%' in <%component%>
## slot_attribute_invalid
> slot attribute must be a static value
## slot_attribute_invalid_placement
> Element with a slot='...' attribute must be a child of a component or a descendant of a custom element
## slot_default_duplicate
> Found default slot content alongside an explicit slot="default"
## slot_element_invalid_attribute
> `<slot>` can only receive attributes and (optionally) let directives
## slot_element_invalid_name
> slot attribute must be a static value
## slot_element_invalid_name_default
> `default` is a reserved word — it cannot be used as a slot name
## slot_snippet_conflict
> Cannot use `<slot>` syntax and `{@render ...}` tags in the same component. Migrate towards `{@render ...}` tags completely.
## snippet_conflict
> Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block
## snippet_invalid_rest_parameter
> snippets do not support rest parameters; use an array instead
## style_directive_invalid_modifier
> `style:` directive can only use the `important` modifier
## style_duplicate
> A component can have a single top-level `<style>` element
## svelte_component_invalid_this
> Invalid component definition — must be an `{expression}`
## svelte_component_missing_this
> `<svelte:component>` must have a 'this' attribute
## svelte_element_invalid_this
> Invalid element definition — must be an `{expression}`
## svelte_element_missing_this
> `<svelte:element>` must have a 'this' attribute
## svelte_fragment_invalid_attribute
> `<svelte:fragment>` can only have a slot attribute and (optionally) a let: directive
## svelte_fragment_invalid_placement
> `<svelte:fragment>` must be the direct child of a component
## svelte_fragment_invalid_slot
> `<svelte:fragment>` slot attribute must have a static value
## svelte_head_illegal_attribute
> `<svelte:head>` cannot have attributes nor directives
## svelte_meta_duplicate
> A component can only have one `<%name%>` element
## svelte_meta_invalid_content
> <%name%> cannot have children
## svelte_meta_invalid_placement
> `<%name%>` tags cannot be inside elements or blocks
## svelte_meta_invalid_tag
> Valid `<svelte:...>` tag names are %list%
## svelte_options_deprecated_tag
> "tag" option is deprecated — use "customElement" instead
## svelte_options_invalid_attribute
> `<svelte:options>` can only receive static attributes
## svelte_options_invalid_attribute_value
> Valid values are %list%
## svelte_options_invalid_customelement
> "customElement" must be a string literal defining a valid custom element name or an object of the form { tag: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
## svelte_options_invalid_customelement_props
> "props" must be a statically analyzable object literal of the form "{ [key: string]: { attribute?: string; reflect?: boolean; type?: "String" | "Boolean" | "Number" | "Array" | "Object" }"
## svelte_options_invalid_customelement_shadow
> "shadow" must be either "open" or "none"
## svelte_options_invalid_tagname
> Tag name must be two or more words joined by the "-" character
## svelte_options_unknown_attribute
> `<svelte:options>` unknown attribute '%name%'
## svelte_self_invalid_placement
> `<svelte:self>` components can only exist inside `{#if}` blocks, `{#each}` blocks, `{#snippet}` blocks or slots passed to components
## tag_invalid_placement
> {@%name% ...} tag cannot be %location%
## textarea_invalid_content
> A `<textarea>` can have either a value attribute or (equivalently) child content, but not both
## title_illegal_attribute
> `<title>` cannot have attributes nor directives
## title_invalid_content
> `<title>` can only contain text and {tags}
## transition_conflict
> Cannot use `%type%:` alongside existing `%existing%:` directive
## transition_duplicate
> Cannot use multiple `%type%:` directives on a single element
## unexpected_eof
> Unexpected end of input
## unexpected_reserved_word
> '%word%' is a reserved word in JavaScript and cannot be used here
## void_element_invalid_content
> Void elements cannot have children or closing tags

@ -0,0 +1,167 @@
## a11y_accesskey
> Avoid using accesskey
## a11y_aria_activedescendant_has_tabindex
> An element with an aria-activedescendant attribute should have a tabindex value
## a11y_aria_attributes
> `<%name%>` should not have aria-* attributes
## a11y_autocomplete_valid
> '%value%' is an invalid value for 'autocomplete' on `<input type="%type%">`
## a11y_autofocus
> Avoid using autofocus
## a11y_click_events_have_key_events
> Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.
## a11y_distracting_elements
> Avoid `<%name%>` elements
## a11y_figcaption_index
> `<figcaption>` must be first or last child of `<figure>`
## a11y_figcaption_parent
> `<figcaption>` must be an immediate child of `<figure>`
## a11y_hidden
> `<%name%>` element should not be hidden
## a11y_img_redundant_alt
> Screenreaders already announce `<img>` elements as an image.
## a11y_incorrect_aria_attribute_type
> The value of '%attribute%' must be a %type%
## a11y_incorrect_aria_attribute_type_boolean
> The value of '%attribute%' must be either 'true' or 'false'. It cannot be empty
## a11y_incorrect_aria_attribute_type_id
> The value of '%attribute%' must be a string that represents a DOM element ID
## a11y_incorrect_aria_attribute_type_idlist
> The value of '%attribute%' must be a space-separated list of strings that represent DOM element IDs
## a11y_incorrect_aria_attribute_type_integer
> The value of '%attribute%' must be an integer
## a11y_incorrect_aria_attribute_type_token
> The value of '%attribute%' must be exactly one of %values%
## a11y_incorrect_aria_attribute_type_tokenlist
> The value of '%attribute%' must be a space-separated list of one or more of %values%
## a11y_incorrect_aria_attribute_type_tristate
> The value of '%attribute%' must be exactly one of true, false, or mixed
## a11y_interactive_supports_focus
> Elements with the '%role%' interactive role must have a tabindex value.
## a11y_invalid_attribute
> '%href_value%' is not a valid %href_attribute% attribute
## a11y_label_has_associated_control
> A form label must be associated with a control.
## a11y_media_has_caption
> `<video>` elements must have a `<track kind="captions">`
## a11y_misplaced_role
> `<%name%>` should not have role attribute
## a11y_misplaced_scope
> The scope attribute should only be used with `<th>` elements
## a11y_missing_attribute
> `<%name%>` element should have %article% %sequence% attribute
## a11y_missing_content
> `<%name%>` element should have child content
## a11y_mouse_events_have_key_events
> '%event%' event must be accompanied by '%accompanied_by%' event
## a11y_no_abstract_role
> Abstract role '%role%' is forbidden
## a11y_no_interactive_element_to_noninteractive_role
> `<%element%>` cannot have role '%role%'
## a11y_no_noninteractive_element_interactions
> Non-interactive element `<%element%>` should not be assigned mouse or keyboard event listeners.
## a11y_no_noninteractive_element_to_interactive_role
> Non-interactive element `<%element%>` cannot have interactive role '%role%'
## a11y_no_noninteractive_tabindex
> noninteractive element cannot have nonnegative tabIndex value
## a11y_no_redundant_roles
> Redundant role '%role%'
## a11y_no_static_element_interactions
> `<%element%>` with a %handler% handler must have an ARIA role
## a11y_positive_tabindex
> Avoid tabindex values above zero
## a11y_role_has_required_aria_props
> Elements with the ARIA role "%role%" must have the following attributes defined: %props%
## a11y_role_supports_aria_props
> The attribute '%attribute%' is not supported by the role '%role%'
## a11y_role_supports_aria_props_implicit
> The attribute '%attribute%' is not supported by the role '%role%'. This role is implicit on the element `<%name%>`
## a11y_unknown_aria_attribute
> Unknown aria attribute 'aria-%attribute%'
> Unknown aria attribute 'aria-%attribute%'. Did you mean '%suggestion%'?
## a11y_unknown_role
> Unknown role '%role%'
> Unknown role '%role%'. Did you mean '%suggestion%'?

@ -0,0 +1,27 @@
## options_deprecated_accessors
> The `accessors` option has been deprecated. It will have no effect in runes mode
## options_deprecated_immutable
> The `immutable` option has been deprecated. It will have no effect in runes mode
## options_missing_custom_element
> The `customElement` option is used when generating a custom element. Did you forget the `customElement: true` compile option?
## options_removed_enable_sourcemap
> The `enableSourcemap` option has been removed. Source maps are always generated now, and tooling can choose to ignore them
## options_removed_hydratable
> The `hydratable` option has been removed. Svelte components are always hydratable now
## options_removed_loop_guard_timeout
> The `loopGuardTimeout` option has been removed
## options_renamed_ssr_dom
> `generate: "dom"` and `generate: "ssr"` options have been renamed to "client" and "server" respectively

@ -0,0 +1,35 @@
## derived_iife
> Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`
## export_let_unused
> Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`
## non_reactive_update
> `%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates
## perf_avoid_inline_class
> Avoid 'new class' — instead, declare the class at the top level scope
## perf_avoid_nested_class
> Avoid declaring classes below the top level scope
## reactive_declaration_invalid_placement
> Reactive declarations only exist at the top level of the instance script
## reactive_declaration_module_script
> All dependencies of the reactive declaration are declared in a module script and will not be reactive
## state_referenced_locally
> State referenced in its own scope will never update. Did you mean to reference it inside a closure?
## store_rune_conflict
> It looks like you're using the `$%name%` rune, but there is a local binding called `%name%`. Referencing a local variable with a `$` prefix will create a store subscription. Please rename `%name%` to avoid the ambiguity

@ -0,0 +1,3 @@
## css_unused_selector
> Unused CSS selector "%name%"

@ -0,0 +1,39 @@
## attribute_avoid_is
> The "is" attribute is not supported cross-browser and should be avoided
## attribute_global_event_reference
> You are referencing `globalThis.%name%`. Did you forget to declare a variable with that name?
## attribute_illegal_colon
> Attributes should not contain ':' characters to prevent ambiguity with Svelte directives
## attribute_invalid_property_name
> '%wrong%' is not a valid HTML attribute. Did you mean '%right%'?
## bind_invalid_each_rest
> The rest operator (...) will create a new object and binding '%name%' with the original object will not work
## block_empty
> Empty block
## component_name_lowercase
> `<%name%>` will be treated as an HTML element unless it begins with a capital letter
## element_invalid_self_closing_tag
> Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...></%name%>` rather than `<%name% ... />`
## event_directive_deprecated
> Using `on:%name%` to listen to the %name% event is deprecated. Use the event attribute `on%name%` instead.
## slot_element_deprecated
> Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead.

@ -0,0 +1,3 @@
## lifecycle_function_unavailable
> `%name%(...)` is not available on the server

@ -0,0 +1,19 @@
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation
## render_tag_invalid_argument
> The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`
## snippet_used_as_component
> A snippet must be rendered with `{@render ...}`
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method
## svelte_element_invalid_this_value
> The `this` prop on `<svelte:element>` must be a string, if defined

@ -0,0 +1,3 @@
## dynamic_void_element_content
> `<svelte:element this="%tag%">` is a void element — it cannot have content

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.111",
"version": "5.0.0-next.115",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -99,8 +99,8 @@
"templating"
],
"scripts": {
"build": "rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "rollup -cw",
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages && rollup -cw",
"check": "tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",

@ -0,0 +1,304 @@
// @ts-check
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
const messages = {};
const seen = new Set();
for (const category of fs.readdirSync('messages')) {
messages[category] = {};
for (const file of fs.readdirSync(`messages/${category}`)) {
if (!file.endsWith('.md')) continue;
const markdown = fs
.readFileSync(`messages/${category}/${file}`, 'utf-8')
.replace(/\r\n/g, '\n');
const sorted = [];
for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
const [_, code, text] = match;
if (seen.has(code)) {
throw new Error(`Duplicate message code ${category}/${code}`);
}
sorted.push({ code, _ });
const sections = text.trim().split('\n\n');
let details = null;
if (!sections[sections.length - 1].startsWith('> ')) {
details = /** @type {string} */ (sections.pop());
}
if (sections.length === 0) {
throw new Error('No message text');
}
seen.add(code);
messages[category][code] = {
messages: sections.map((section) => section.replace(/^> /gm, '')),
details
};
}
sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
fs.writeFileSync(
`messages/${category}/${file}`,
sorted.map((x) => x._.trim()).join('\n\n') + '\n'
);
}
}
function transform(name, dest) {
const source = fs
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
.replace(/\r\n/g, '\n');
const comments = [];
const ast = acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'module',
onComment: (block, value, start, end) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
}
});
walk(ast, null, {
_(node, { next }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = comments.shift();
// @ts-expect-error
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const slice = source.slice(node.end, comments[0].start);
if (/^[,) \t]*$/.test(slice)) {
// @ts-expect-error
node.trailingComments = [comments.shift()];
}
}
}
});
const category = messages[name];
// find the `export function CODE` node
const index = ast.body.findIndex((node) => {
if (
node.type === 'ExportNamedDeclaration' &&
node.declaration &&
node.declaration.type === 'FunctionDeclaration'
) {
return node.declaration.id.name === 'CODE';
}
});
if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
const template_node = ast.body[index];
ast.body.splice(index, 1);
for (const code in category) {
const { messages } = category[code];
const vars = [];
const group = messages.map((text, i) => {
for (const match of text.matchAll(/%(\w+)%/g)) {
const name = match[1];
if (!vars.includes(name)) {
vars.push(match[1]);
}
}
return {
text,
vars: vars.slice()
};
});
/** @type {import('estree').Expression} */
let message = { type: 'Literal', value: '' };
let prev_vars;
for (let i = 0; i < group.length; i += 1) {
const { text, vars } = group[i];
if (vars.length === 0) {
message = {
type: 'Literal',
value: text
};
continue;
}
const parts = text.split(/(%\w+%)/);
/** @type {import('estree').Expression[]} */
const expressions = [];
/** @type {import('estree').TemplateElement[]} */
const quasis = [];
for (let i = 0; i < parts.length; i += 1) {
const part = parts[i];
if (i % 2 === 0) {
const str = part.replace(/(`|\${)/g, '\\$1');
quasis.push({
type: 'TemplateElement',
value: { raw: str, cooked: str },
tail: i === parts.length - 1
});
} else {
expressions.push({
type: 'Identifier',
name: part.slice(1, -1)
});
}
}
/** @type {import('estree').Expression} */
const expression = {
type: 'TemplateLiteral',
expressions,
quasis
};
if (prev_vars) {
if (vars.length === prev_vars.length) {
throw new Error('Message overloads must have new parameters');
}
message = {
type: 'ConditionalExpression',
test: {
type: 'Identifier',
name: vars[prev_vars.length]
},
consequent: expression,
alternate: message
};
} else {
message = expression;
}
prev_vars = vars;
}
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
// @ts-expect-error Block is a block comment, which is not recognised
Block(node, context) {
if (!node.value.includes('PARAMETER')) return;
const value = node.value
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}
if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;
return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}
return line;
})
.filter((x) => x !== '')
.join('\n');
if (value !== node.value) {
return { ...node, value };
}
},
FunctionDeclaration(node, context) {
if (node.id.name !== 'CODE') return;
const params = [];
for (const param of node.params) {
if (param.type === 'Identifier' && param.name === 'PARAMETER') {
params.push(...vars.map((name) => ({ type: 'Identifier', name })));
} else {
params.push(param);
}
}
return /** @type {import('estree').FunctionDeclaration} */ ({
.../** @type {import('estree').FunctionDeclaration} */ (context.next()),
params,
id: {
...node.id,
name: code
}
});
},
Literal(node) {
if (node.value === 'CODE') {
return {
type: 'Literal',
value: code
};
}
},
Identifier(node) {
if (node.name !== 'MESSAGE') return;
return message;
}
});
// @ts-expect-error
ast.body.push(clone);
}
// @ts-expect-error
const module = esrap.print(ast);
fs.writeFileSync(
dest,
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
module.code,
'utf-8'
);
}
transform('compile-errors', 'src/compiler/errors.js');
transform('compile-warnings', 'src/compiler/warnings.js');
transform('client-warnings', 'src/internal/client/warnings.js');
transform('client-errors', 'src/internal/client/errors.js');
transform('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.js');

@ -0,0 +1,17 @@
import { DEV } from 'esm-env';
/**
* MESSAGE
* @param {string} PARAMETER
* @returns {never}
*/
export function CODE(PARAMETER) {
if (DEV) {
const error = new Error(`${'CODE'}\n${MESSAGE}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error('CODE');
}
}

@ -0,0 +1,17 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* MESSAGE
* @param {string} PARAMETER
*/
export function CODE(PARAMETER) {
if (DEV) {
console.warn(`%c[svelte] ${'CODE'}\n%c${MESSAGE}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn('CODE');
}
}

@ -0,0 +1,74 @@
/** @typedef {{ start?: number, end?: number }} NodeLike */
// interface is duplicated between here (used internally) and ./interfaces.js
// (exposed publicly), and I'm not sure how to avoid that
export class CompileError extends Error {
name = 'CompileError';
/** @type {import('#compiler').CompileError['filename']} */
filename = undefined;
/** @type {import('#compiler').CompileError['position']} */
position = undefined;
/** @type {import('#compiler').CompileError['start']} */
start = undefined;
/** @type {import('#compiler').CompileError['end']} */
end = undefined;
/**
*
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
this.position = position;
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
}
}
/**
* @param {null | number | NodeLike} node
* @param {string} code
* @param {string} message
* @returns {never}
*/
function e(node, code, message) {
const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end;
throw new CompileError(
code,
message,
start !== undefined && end !== undefined ? [start, end] : undefined
);
}
/**
* MESSAGE
* @param {null | number | NodeLike} node
* @param {string} PARAMETER
* @returns {never}
*/
export function CODE(node, PARAMETER) {
e(node, 'CODE', MESSAGE);
}

@ -0,0 +1,52 @@
import { getLocator } from 'locate-character';
/** @typedef {{ start?: number, end?: number }} NodeLike */
/** @type {import('#compiler').Warning[]} */
let warnings = [];
/** @type {string | undefined} */
let filename;
let locator = getLocator('', { offsetLine: 1 });
/**
* @param {{
* source: string;
* filename: string | undefined;
* }} options
* @returns {import('#compiler').Warning[]}
*/
export function reset_warnings(options) {
filename = options.filename;
locator = getLocator(options.source, { offsetLine: 1 });
return (warnings = []);
}
/**
* @param {null | NodeLike} node
* @param {string} code
* @param {string} message
*/
function w(node, code, message) {
// @ts-expect-error
if (node?.ignores?.has(code)) return;
warnings.push({
code,
message,
filename,
start: node?.start !== undefined ? locator(node.start) : undefined,
end: node?.end !== undefined ? locator(node.end) : undefined
});
}
/**
* MESSAGE
* @param {null | NodeLike} node
* @param {string} PARAMETER
*/
export function CODE(node, PARAMETER) {
w(node, 'CODE', MESSAGE);
}

@ -0,0 +1,10 @@
/**
* MESSAGE
* @param {string} PARAMETER
* @returns {never}
*/
export function CODE(PARAMETER) {
const error = new Error(`${'CODE'}\n${MESSAGE}`);
error.name = 'Svelte error';
throw error;
}

@ -0,0 +1,17 @@
import { DEV } from 'esm-env';
/**
* MESSAGE
* @param {string} PARAMETER
* @returns {never}
*/
export function CODE(PARAMETER) {
if (DEV) {
const error = new Error(`${'CODE'}\n${MESSAGE}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error('CODE');
}
}

@ -0,0 +1,19 @@
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* MESSAGE
* @param {boolean} trace
* @param {string} PARAMETER
*/
export function CODE(trace, PARAMETER) {
if (DEV) {
console.warn(`%c[svelte] ${'CODE'}\n%c${MESSAGE}`, bold, normal);
if (trace) console.trace('stack trace');
} else {
// TODO print a link to the documentation
console.warn('CODE');
}
}

File diff suppressed because it is too large Load Diff

@ -8,6 +8,7 @@ import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_node
import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
import { transform_component, transform_module } from './phases/3-transform/index.js';
import { validate_component_options, validate_module_options } from './validate-options.js';
import { reset_warnings } from './warnings.js';
export { default as preprocess } from './preprocess/index.js';
/**
@ -20,6 +21,7 @@ export { default as preprocess } from './preprocess/index.js';
*/
export function compile(source, options) {
try {
const warnings = reset_warnings({ source, filename: options.filename });
const validated = validate_component_options(options, '');
let parsed = _parse(source);
@ -44,6 +46,7 @@ export function compile(source, options) {
const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options);
result.warnings = warnings;
result.ast = to_public_ast(source, parsed, options.modernAst);
return result;
} catch (e) {
@ -65,9 +68,12 @@ export function compile(source, options) {
*/
export function compileModule(source, options) {
try {
const warnings = reset_warnings({ source, filename: options.filename });
const validated = validate_module_options(options, '');
const analysis = analyze_module(parse_acorn(source, false), validated);
return transform_module(analysis, source, validated);
const result = transform_module(analysis, source, validated);
result.warnings = warnings;
return result;
} catch (e) {
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
@ -98,6 +104,32 @@ function handle_compile_error(error, filename, source) {
throw error;
}
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* https://svelte.dev/docs/svelte-compiler#svelte-parse
* @overload
* @param {string} source
* @param {{ filename?: string; modern: true }} options
* @returns {import('#compiler').Root}
*/
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* https://svelte.dev/docs/svelte-compiler#svelte-parse
* @overload
* @param {string} source
* @param {{ filename?: string; modern?: false }} [options]
* @returns {import('./types/legacy-nodes.js').LegacyRoot}
*/
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
@ -158,5 +190,5 @@ export function walk() {
}
export { CompileError } from './errors.js';
export { VERSION } from '../version.js';
export { migrate } from './migrate/index.js';

@ -4,6 +4,7 @@ import {
regex_not_whitespace,
regex_starts_with_whitespaces
} from './phases/patterns.js';
import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js';
/**
* Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children.
@ -197,7 +198,13 @@ export function convert(source, ast) {
ClassDirective(node) {
return { ...node, type: 'Class' };
},
ComplexSelector(node, { visit }) {
Comment(node) {
return {
...node,
ignores: extract_svelte_ignore(node.data)
};
},
ComplexSelector(node) {
const children = [];
for (const child of node.children) {

@ -0,0 +1,770 @@
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { validate_component_options } from '../validate-options.js';
import { get_rune } from '../phases/scope.js';
import { reset_warnings } from '../warnings.js';
import { extract_identifiers } from '../utils/ast.js';
import { regex_is_valid_identifier } from '../phases/patterns.js';
/**
* Does a best-effort migration of Svelte code towards using runes, event attributes and render tags.
* May throw an error if the code is too complex to migrate automatically.
*
* @param {string} source
* @returns {{ code: string; }}
*/
export function migrate(source) {
try {
reset_warnings({ source, filename: 'migrate.svelte' });
let parsed = parse(source);
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
/** @type {import('#compiler').ValidatedCompileOptions} */
const combined_options = {
...validate_component_options({}, ''),
...parsed_options,
customElementOptions
};
const str = new MagicString(source);
const analysis = analyze_component(parsed, source, combined_options);
const indent = guess_indent(source);
/** @type {State} */
let state = {
scope: analysis.instance.scope,
analysis,
str,
indent,
props: [],
props_insertion_point: parsed.instance?.content.start ?? 0,
has_props_rune: false,
props_name: analysis.root.unique('props').name,
rest_props_name: analysis.root.unique('rest').name,
end: source.length,
run_name: analysis.root.unique('run').name,
needs_run: false
};
if (parsed.instance) {
walk(parsed.instance.content, state, instance_script);
}
state = { ...state, scope: analysis.template.scope };
walk(parsed.fragment, state, template);
const run_import = `import { run${state.run_name === 'run' ? '' : `as ${state.run_name}`} } from 'svelte/legacy';`;
let added_legacy_import = false;
if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) {
const has_many_props = state.props.length > 3;
const props_separator = has_many_props ? `\n${indent}${indent}` : ' ';
let props = '';
if (analysis.uses_props) {
props = `...${state.props_name}`;
} else {
props = state.props
.map((prop) => {
let prop_str =
prop.local === prop.exported ? prop.local : `${prop.exported}: ${prop.local}`;
if (prop.bindable) {
prop_str += ` = $bindable(${prop.init})`;
} else if (prop.init) {
prop_str += ` = ${prop.init}`;
}
return prop_str;
})
.join(`,${props_separator}`);
if (analysis.uses_rest_props) {
props += `,${props_separator}...${state.rest_props_name}`;
}
}
if (state.has_props_rune) {
// some render tags or forwarded event attributes to add
str.appendRight(state.props_insertion_point, ` ${props},`);
} else {
const uses_ts = parsed.instance?.attributes.some(
(attr) => attr.name === 'lang' && /** @type {any} */ (attr).value[0].data === 'ts'
);
const type_name = state.scope.root.unique('Props').name;
let type = '';
if (uses_ts) {
if (analysis.uses_props || analysis.uses_rest_props) {
type = `interface ${type_name} { [key: string]: any }`;
} else {
type = `interface ${type_name} {${props_separator}${state.props
.map((prop) => {
return `${prop.exported}${prop.optional ? '?' : ''}: ${prop.type}`;
})
.join(`,${props_separator}`)}${has_many_props ? `\n${indent}` : ' '}}`;
}
} else {
if (analysis.uses_props || analysis.uses_rest_props) {
type = `{Record<string, any>}`;
} else {
type = `{${state.props
.map((prop) => {
return `${prop.exported}${prop.optional ? '?' : ''}: ${prop.type}`;
})
.join(`, `)}}`;
}
}
let props_declaration = `let {${props_separator}${props}${has_many_props ? `\n${indent}` : ' '}}`;
if (uses_ts) {
props_declaration = `${type}\n\n${indent}${props_declaration}`;
props_declaration = `${props_declaration}${type ? `: ${type_name}` : ''} = $props();`;
} else {
props_declaration = `/** @type {${type}} */\n${indent}${props_declaration}`;
props_declaration = `${props_declaration} = $props();`;
}
if (parsed.instance) {
props_declaration = `\n${indent}${props_declaration}`;
str.appendRight(state.props_insertion_point, props_declaration);
} else {
const imports = state.needs_run ? `${indent}${run_import}\n` : '';
str.prepend(`<script>\n${imports}${indent}${props_declaration}\n</script>\n\n`);
added_legacy_import = true;
}
}
}
if (state.needs_run && !added_legacy_import) {
if (parsed.instance) {
str.appendRight(
/** @type {number} */ (parsed.instance.content.start),
`\n${indent}${run_import}\n`
);
} else {
str.prepend(`<script>\n${indent}${run_import}\n</script>\n\n`);
}
}
return { code: str.toString() };
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error while migrating Svelte code');
throw e;
}
}
/**
* @typedef {{
* scope: import('../phases/scope.js').Scope;
* str: MagicString;
* analysis: import('../phases/types.js').ComponentAnalysis;
* indent: string;
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; optional: boolean; type: string }>;
* props_insertion_point: number;
* has_props_rune: boolean;
* props_name: string;
* rest_props_name: string;
* end: number;
* run_name: string;
* needs_run: boolean;
* }} State
*/
/** @type {import('zimmerframe').Visitors<import('../types/template.js').SvelteNode, State>} */
const instance_script = {
Identifier(node, { state }) {
handle_identifier(node, state);
},
ExportNamedDeclaration(node, { state, next }) {
if (node.declaration) {
next();
return;
}
let count_removed = 0;
for (const specifier of node.specifiers) {
const binding = state.scope.get(specifier.local.name);
if (binding?.kind === 'bindable_prop') {
state.str.remove(
/** @type {number} */ (specifier.start),
/** @type {number} */ (specifier.end)
);
count_removed++;
}
}
if (count_removed === node.specifiers.length) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
}
},
VariableDeclaration(node, { state, path }) {
if (state.scope !== state.analysis.instance.scope) {
return;
}
let nr_of_props = 0;
for (const declarator of node.declarations) {
if (state.analysis.runes) {
if (get_rune(declarator.init, state.scope) === '$props') {
state.props_insertion_point = /** @type {number} */ (declarator.id.start) + 1;
state.has_props_rune = true;
}
continue;
}
let bindings;
try {
bindings = state.scope.get_bindings(declarator);
} catch (e) {
// no bindings, so we can skip this
continue;
}
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
continue;
}
if (has_props) {
nr_of_props++;
if (declarator.id.type !== 'Identifier') {
// TODO invest time in this?
throw new Error(
'Encountered an export declaration pattern that is not supported for automigration.'
);
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// const tmp = state.scope.generate('tmp');
// const paths = extract_paths(declarator.id);
// state.props_pre.push(
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression)
// );
// for (const path of paths) {
// const name = (path.node as Identifier).name;
// const binding = state.scope.get(name)!;
// const value = path.expression!(b.id(tmp));
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
// state.props.push({
// local: name,
// exported: binding.prop_alias ? binding.prop_alias : name,
// init: value
// });
// state.props_insertion_point = /** @type {number} */(declarator.end);
// } else {
// declarations.push(b.declarator(path.node, value));
// }
// }
}
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (
state.analysis.uses_props &&
(declarator.init || binding.mutated || binding.reassigned)
) {
throw new Error(
'$$props is used together with named props in a way that cannot be automatically migrated.'
);
}
state.props.push({
local: declarator.id.name,
exported: binding.prop_alias ? binding.prop_alias : declarator.id.name,
init: declarator.init
? state.str.original.substring(
/** @type {number} */ (declarator.init.start),
/** @type {number} */ (declarator.init.end)
)
: '',
optional: !!declarator.init,
type: extract_type(declarator, state.str, path),
bindable: binding.mutated || binding.reassigned
});
state.props_insertion_point = /** @type {number} */ (declarator.end);
state.str.update(
/** @type {number} */ (declarator.start),
/** @type {number} */ (declarator.end),
''
);
continue;
}
// state
if (declarator.init) {
state.str.prependLeft(/** @type {number} */ (declarator.init.start), '$state(');
state.str.appendRight(/** @type {number} */ (declarator.init.end), ')');
} else {
state.str.prependLeft(/** @type {number} */ (declarator.id.end), ' = $state()');
}
}
if (nr_of_props === node.declarations.length) {
let start = /** @type {number} */ (node.start);
let end = /** @type {number} */ (node.end);
const parent = path.at(-1);
if (parent?.type === 'ExportNamedDeclaration') {
start = /** @type {number} */ (parent.start);
end = /** @type {number} */ (parent.end);
}
while (state.str.original[start] !== '\n') start--;
while (state.str.original[end] !== '\n') end++;
state.str.update(start, end, '');
}
},
BreakStatement(node, { state, path }) {
if (path[1].type !== 'LabeledStatement') return;
if (node.label?.name !== '$') return;
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
'return;'
);
},
LabeledStatement(node, { path, state, next }) {
if (state.analysis.runes) return;
if (path.length > 1) return;
if (node.label.name !== '$') return;
next();
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
const ids = extract_identifiers(node.body.expression.left);
const bindings = ids.map((id) => state.scope.get(id.name));
const reassigned_bindings = bindings.filter((b) => b?.reassigned);
if (reassigned_bindings.length === 0 && !bindings.some((b) => b?.kind === 'store_sub')) {
// $derived
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.body.expression.start),
'let '
);
state.str.prependLeft(
/** @type {number} */ (node.body.expression.right.start),
'$derived('
);
state.str.update(
/** @type {number} */ (node.body.expression.right.end),
/** @type {number} */ (node.end),
');'
);
return;
} else {
for (const binding of reassigned_bindings) {
if (binding && ids.includes(binding.node)) {
// implicitly-declared variable which we need to make explicit
state.str.prependLeft(
/** @type {number} */ (node.start),
`let ${binding.node.name}${binding.kind === 'state' ? ' = $state()' : ''};\n${state.indent}`
);
}
}
}
}
state.needs_run = true;
const is_block_stmt = node.body.type === 'BlockStatement';
const start_end = /** @type {number} */ (node.body.start);
// TODO try to find out if we can use $derived.by instead?
if (is_block_stmt) {
state.str.update(
/** @type {number} */ (node.start),
start_end + 1,
`${state.run_name}(() => {`
);
const end = /** @type {number} */ (node.body.end);
state.str.update(end - 1, end, '});');
} else {
state.str.update(
/** @type {number} */ (node.start),
start_end,
`${state.run_name}(() => {\n${state.indent}`
);
state.str.indent(state.indent, {
exclude: [
[0, /** @type {number} */ (node.body.start)],
[/** @type {number} */ (node.body.end), state.end]
]
});
state.str.appendRight(/** @type {number} */ (node.end), `\n${state.indent}});`);
}
}
};
/** @type {import('zimmerframe').Visitors<import('../types/template.js').SvelteNode, State>} */
const template = {
Identifier(node, { state }) {
handle_identifier(node, state);
},
RegularElement(node, { state, next }) {
handle_events(node, state);
next();
},
SvelteElement(node, { state, next }) {
handle_events(node, state);
next();
},
SvelteWindow(node, { state, next }) {
handle_events(node, state);
next();
},
SvelteBody(node, { state, next }) {
handle_events(node, state);
next();
},
SvelteDocument(node, { state, next }) {
handle_events(node, state);
next();
},
SlotElement(node, { state, next }) {
let name = 'children';
let slot_props = '{ ';
for (const attr of node.attributes) {
if (attr.type === 'SpreadAttribute') {
slot_props += `...${state.str.original.substring(/** @type {number} */ (attr.expression.start), attr.expression.end)}, `;
} else if (attr.type === 'Attribute') {
if (attr.name === 'name') {
name = state.scope.generate(/** @type {any} */ (attr.value)[0].data);
} else {
const value =
attr.value !== true
? state.str.original.substring(
attr.value[0].start,
attr.value[attr.value.length - 1].end
)
: 'true';
slot_props += value === attr.name ? `${value}, ` : `${attr.name}: ${value}, `;
}
}
}
slot_props += '}';
if (slot_props === '{ }') {
slot_props = '';
}
state.props.push({
local: name,
exported: name,
init: '',
bindable: false,
optional: true,
type: `import('svelte').${slot_props ? 'Snippet<[any]>' : 'Snippet'}`
});
if (node.fragment.nodes.length > 0) {
next();
state.str.update(
node.start,
node.fragment.nodes[0].start,
`{#if ${name}}{@render ${name}(${slot_props})}{:else}`
);
state.str.update(node.fragment.nodes[node.fragment.nodes.length - 1].end, node.end, '{/if}');
} else {
state.str.update(node.start, node.end, `{@render ${name}?.(${slot_props})}`);
}
}
};
/**
* @param {import('estree').VariableDeclarator} declarator
* @param {MagicString} str
* @param {import('#compiler').SvelteNode[]} path
*/
function extract_type(declarator, str, path) {
if (declarator.id.typeAnnotation) {
let start = declarator.id.typeAnnotation.start + 1; // skip the colon
while (str.original[start] === ' ') {
start++;
}
return str.original.substring(start, declarator.id.typeAnnotation.end);
}
// try to find a comment with a type annotation, hinting at jsdoc
const parent = path.at(-1);
if (parent?.type === 'ExportNamedDeclaration' && parent.leadingComments) {
const last = parent.leadingComments[parent.leadingComments.length - 1];
if (last.type === 'Block') {
const match = /@type {([^}]+)}/.exec(last.value);
if (match) {
str.update(/** @type {any} */ (last).start, /** @type {any} */ (last).end, '');
return match[1];
}
}
}
// try to infer it from the init
if (declarator.init?.type === 'Literal') {
const type = typeof declarator.init.value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return type;
}
}
return 'any';
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SvelteWindow | import('#compiler').SvelteDocument | import('#compiler').SvelteBody} node
* @param {State} state
*/
function handle_events(node, state) {
/** @type {Map<string, import('#compiler').OnDirective[]>} */
const handlers = new Map();
for (const attribute of node.attributes) {
if (attribute.type !== 'OnDirective') continue;
let name = `on${attribute.name}`;
if (attribute.modifiers.includes('capture')) {
name += 'capture';
}
const nodes = handlers.get(name) || [];
nodes.push(attribute);
handlers.set(name, nodes);
}
for (const [name, nodes] of handlers) {
// turn on:click into a prop
let exported = name;
if (!regex_is_valid_identifier.test(name)) {
exported = `'${exported}'`;
}
// Check if prop already set, could happen when on:click on different elements
let local = state.props.find((prop) => prop.exported === exported)?.local;
const last = nodes[nodes.length - 1];
const payload_name =
last.expression?.type === 'ArrowFunctionExpression' &&
last.expression.params[0]?.type === 'Identifier'
? last.expression.params[0].name
: generate_event_name(last, state);
let prepend = '';
for (let i = 0; i < nodes.length - 1; i += 1) {
const node = nodes[i];
if (node.expression) {
let body = '';
if (node.expression.type === 'ArrowFunctionExpression') {
body = state.str.original.substring(
/** @type {number} */ (node.expression.body.start),
/** @type {number} */ (node.expression.body.end)
);
} else {
body = `${state.str.original.substring(
/** @type {number} */ (node.expression.start),
/** @type {number} */ (node.expression.end)
)}();`;
}
// TODO check how many indents needed
for (const modifier of node.modifiers) {
if (modifier === 'stopPropagation') {
body = `\n${state.indent}${payload_name}.stopPropagation();\n${body}`;
} else if (modifier === 'preventDefault') {
body = `\n${state.indent}${payload_name}.preventDefault();\n${body}`;
} else if (modifier === 'stopImmediatePropagation') {
body = `\n${state.indent}${payload_name}.stopImmediatePropagation();\n${body}`;
} else {
body = `\n${state.indent}// @migration-task: incorporate ${modifier} modifier\n${body}`;
}
}
prepend += `\n${state.indent}${body}\n`;
} else {
if (!local) {
local = state.scope.generate(`on${node.name}`);
state.props.push({
local,
exported,
init: '',
bindable: false,
optional: true,
type: '(event: any) => void'
});
}
prepend += `\n${state.indent}${local}?.(${payload_name});\n`;
}
state.str.remove(node.start, node.end);
}
if (last.expression) {
// remove : from on:click
state.str.remove(last.start + 2, last.start + 3);
// remove modifiers
if (last.modifiers.length > 0) {
state.str.remove(
last.start + last.name.length + 3,
state.str.original.indexOf('=', last.start)
);
}
if (last.modifiers.includes('capture')) {
state.str.appendRight(last.start + last.name.length + 3, 'capture');
}
for (const modifier of last.modifiers) {
if (modifier === 'stopPropagation') {
prepend += `\n${state.indent}${payload_name}.stopPropagation();\n`;
} else if (modifier === 'preventDefault') {
prepend += `\n${state.indent}${payload_name}.preventDefault();\n`;
} else if (modifier === 'stopImmediatePropagation') {
prepend += `\n${state.indent}${payload_name}.stopImmediatePropagation();\n`;
} else if (modifier !== 'capture') {
prepend += `\n${state.indent}// @migration-task: incorporate ${modifier} modifier\n`;
}
}
if (prepend) {
let pos = last.expression.start;
if (last.expression.type === 'ArrowFunctionExpression') {
pos = last.expression.body.start;
if (
last.expression.params.length > 0 &&
last.expression.params[0].type !== 'Identifier'
) {
const start = /** @type {number} */ (last.expression.params[0].start);
const end = /** @type {number} */ (last.expression.params[0].end);
// replace event payload with generated one that others use,
// then destructure generated payload param into what the user wrote
state.str.overwrite(start, end, payload_name);
prepend = `let ${state.str.original.substring(
start,
end
)} = ${payload_name};\n${prepend}`;
} else if (last.expression.params.length === 0) {
// add generated payload param to arrow function
const pos = state.str.original.lastIndexOf(')', last.expression.body.start);
state.str.prependLeft(pos, payload_name);
}
const needs_curlies = last.expression.body.type !== 'BlockStatement';
state.str.prependRight(
/** @type {number} */ (pos) + (needs_curlies ? 0 : 1),
`${needs_curlies ? '{' : ''}${prepend}${state.indent}`
);
state.str.appendRight(
/** @type {number} */ (last.expression.body.end) - (needs_curlies ? 0 : 1),
`\n${needs_curlies ? '}' : ''}`
);
} else {
state.str.update(
/** @type {number} */ (last.expression.start),
/** @type {number} */ (last.expression.end),
`(${payload_name}) => {${prepend}\n${state.indent}${state.str.original.substring(
/** @type {number} */ (last.expression.start),
/** @type {number} */ (last.expression.end)
)}?.(${payload_name});\n}`
);
}
}
} else {
// turn on:click into a prop
// Check if prop already set, could happen when on:click on different elements
if (!local) {
local = state.scope.generate(`on${last.name}`);
state.props.push({
local,
exported,
init: '',
bindable: false,
optional: true,
type: '(event: any) => void'
});
}
let replacement = '';
if (!prepend) {
if (exported === local) {
replacement = `{${name}}`;
} else {
replacement = `${name}={${local}}`;
}
} else {
replacement = `${name}={(${payload_name}) => {${prepend}\n${state.indent}${local}?.(${payload_name});\n}}`;
}
state.str.update(last.start, last.end, replacement);
}
}
}
/**
* @param {import('#compiler').OnDirective} last
* @param {State} state
*/
function generate_event_name(last, state) {
const scope =
(last.expression && state.analysis.template.scopes.get(last.expression)) || state.scope;
let name = 'event';
if (!scope.get(name)) return name;
let i = 1;
while (scope.get(`${name}${i}`)) i += 1;
return `${name}${i}`;
}
/**
* @param {import('estree').Identifier} node
* @param {State} state
*/
function handle_identifier(node, state) {
if (state.analysis.uses_props) {
if (node.name === '$$props' || node.name === '$$restProps') {
// not 100% correct for $$restProps but it'll do
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
state.props_name
);
} else {
const binding = state.scope.get(node.name);
if (binding?.kind === 'bindable_prop') {
state.str.prependLeft(/** @type {number} */ (node.start), `${state.props_name}.`);
}
}
} else if (node.name === '$$restProps' && state.analysis.uses_rest_props) {
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
state.rest_props_name
);
}
}
/** @param {string} content */
function guess_indent(content) {
const lines = content.split('\n');
const tabbed = lines.filter((line) => /^\t+/.test(line));
const spaced = lines.filter((line) => /^ {2,}/.test(line));
if (tabbed.length === 0 && spaced.length === 0) {
return '\t';
}
// More lines tabbed than spaced? Assume tabs, and
// default to tabs in the case of a tie (or nothing
// to go on)
if (tabbed.length >= spaced.length) {
return '\t';
}
// Otherwise, we need to guess the multiple
const min = spaced.reduce((previous, current) => {
const count = /^ +/.exec(current)?.[0].length ?? 0;
return Math.min(count, previous);
}, Infinity);
return ' '.repeat(min);
}

@ -4,7 +4,7 @@ import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js';
import { reserved } from './utils/names.js';
import full_char_code_at from './utils/full_char_code_at.js';
import { error } from '../../errors.js';
import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { getLocator } from 'locate-character';
@ -92,15 +92,15 @@ export class Parser {
if (current.type === 'RegularElement') {
current.end = current.start + 1;
error(current, 'unclosed-element', current.name);
e.element_unclosed(current, current.name);
} else {
current.end = current.start + 1;
error(current, 'unclosed-block');
e.block_unclosed(current);
}
}
if (state !== fragment) {
error(this.index, 'unexpected-eof');
e.unexpected_eof(this.index);
}
if (this.root.fragment.nodes.length) {
@ -158,7 +158,7 @@ export class Parser {
* @returns {never}
*/
acorn_error(err) {
error(err.pos, 'js-parse-error', err.message.replace(regex_position_indicator, ''));
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
@ -172,11 +172,7 @@ export class Parser {
}
if (required) {
if (this.index === this.template.length) {
error(this.index, 'unexpected-eof', str);
} else {
error(this.index, 'expected-token', str);
}
e.expected_token(this.index, str);
}
return false;
@ -241,7 +237,7 @@ export class Parser {
const identifier = this.template.slice(this.index, (this.index = i));
if (!allow_reserved && reserved.includes(identifier)) {
error(start, 'unexpected-reserved-word', identifier);
e.unexpected_reserved_word(start, identifier);
}
return identifier;
@ -250,7 +246,7 @@ export class Parser {
/** @param {RegExp} pattern */
read_until(pattern) {
if (this.index >= this.template.length) {
error(this.template.length, 'unexpected-eof');
e.unexpected_eof(this.template.length);
}
const start = this.index;
@ -267,7 +263,7 @@ export class Parser {
require_whitespace() {
if (!regex_whitespace.test(this.template[this.index])) {
error(this.index, 'missing-whitespace');
e.expected_whitespace(this.index);
}
this.allow_whitespace();

@ -9,7 +9,7 @@ import {
} from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
/**
* @param {import('../index.js').Parser} parser
@ -36,7 +36,7 @@ export default function read_pattern(parser, optional_allowed = false) {
}
if (!is_bracket_open(code)) {
error(i, 'expected-pattern');
e.expected_pattern(i);
}
const bracket_stack = [code];
@ -49,11 +49,7 @@ export default function read_pattern(parser, optional_allowed = false) {
} else if (is_bracket_close(code)) {
const popped = /** @type {number} */ (bracket_stack.pop());
if (!is_bracket_pair(popped, code)) {
error(
i,
'expected-token',
String.fromCharCode(/** @type {number} */ (get_bracket_close(popped)))
);
e.expected_token(i, String.fromCharCode(/** @type {number} */ (get_bracket_close(popped))));
}
if (bracket_stack.length === 0) {
i += code <= 0xffff ? 1 : 2;

@ -1,6 +1,6 @@
import { parse_expression_at } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
/**
* @param {import('../index.js').Parser} parser
@ -23,7 +23,7 @@ export default function read_expression(parser) {
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
error(index, 'expected-token', ')');
e.expected_token(index, ')');
}
index += 1;

@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js';
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/;
@ -22,24 +22,18 @@ export default function read_options(node) {
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') {
error(attribute, 'invalid-svelte-option-attribute');
e.svelte_options_invalid_attribute(attribute);
}
const { name } = attribute;
switch (name) {
case 'runes': {
const value = get_static_value(attribute, () =>
error(attribute, 'invalid-svelte-option-runes')
);
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-runes');
}
component_options.runes = value;
component_options.runes = get_boolean_value(attribute);
break;
}
case 'tag': {
error(attribute, 'tag-option-deprecated');
e.svelte_options_deprecated_tag(attribute);
break; // eslint doesn't know this is unnecessary
}
case 'customElement': {
@ -48,9 +42,9 @@ export default function read_options(node) {
const { value } = attribute;
if (value === true) {
error(attribute, 'invalid-svelte-option-customElement');
e.svelte_options_invalid_customelement(attribute);
} else if (value[0].type === 'Text') {
const tag = get_static_value(attribute, () => error(attribute, 'invalid-tag-property'));
const tag = get_static_value(attribute);
validate_tag(attribute, tag);
ce.tag = tag;
component_options.customElement = ce;
@ -61,7 +55,7 @@ export default function read_options(node) {
if (value[0].expression.type === 'Literal' && value[0].expression.value === null) {
break;
}
error(attribute, 'invalid-svelte-option-customElement');
e.svelte_options_invalid_customelement(attribute);
}
/** @type {Array<[string, any]>} */
@ -72,7 +66,7 @@ export default function read_options(node) {
property.computed ||
property.key.type !== 'Identifier'
) {
error(attribute, 'invalid-svelte-option-customElement');
e.svelte_options_invalid_customelement(attribute);
}
properties.push([property.key.name, property.value]);
}
@ -83,13 +77,13 @@ export default function read_options(node) {
validate_tag(tag, tag_value);
ce.tag = tag_value;
} else {
error(attribute, 'invalid-svelte-option-customElement');
e.svelte_options_invalid_customelement(attribute);
}
const props = properties.find(([name]) => name === 'props')?.[1];
if (props) {
if (props.type !== 'ObjectExpression') {
error(attribute, 'invalid-customElement-props-attribute');
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props = {};
for (const property of /** @type {import('estree').ObjectExpression} */ (props)
@ -100,7 +94,7 @@ export default function read_options(node) {
property.key.type !== 'Identifier' ||
property.value.type !== 'ObjectExpression'
) {
error(attribute, 'invalid-customElement-props-attribute');
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name] = {};
for (const prop of property.value.properties) {
@ -110,7 +104,7 @@ export default function read_options(node) {
prop.key.type !== 'Identifier' ||
prop.value.type !== 'Literal'
) {
error(attribute, 'invalid-customElement-props-attribute');
e.svelte_options_invalid_customelement_props(attribute);
}
if (prop.key.name === 'type') {
@ -119,21 +113,21 @@ export default function read_options(node) {
/** @type {string} */ (prop.value.value)
) === -1
) {
error(attribute, 'invalid-customElement-props-attribute');
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].type = /** @type {any} */ (prop.value.value);
} else if (prop.key.name === 'reflect') {
if (typeof prop.value.value !== 'boolean') {
error(attribute, 'invalid-customElement-props-attribute');
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].reflect = prop.value.value;
} else if (prop.key.name === 'attribute') {
if (typeof prop.value.value !== 'string') {
error(attribute, 'invalid-customElement-props-attribute');
e.svelte_options_invalid_customelement_props(attribute);
}
ce.props[property.key.name].attribute = prop.value.value;
} else {
error(attribute, 'invalid-customElement-props-attribute');
e.svelte_options_invalid_customelement_props(attribute);
}
}
}
@ -143,7 +137,7 @@ export default function read_options(node) {
if (shadow) {
const shadowdom = shadow?.value;
if (shadowdom !== 'open' && shadowdom !== 'none') {
error(shadow, 'invalid-customElement-shadow-attribute');
e.svelte_options_invalid_customelement_shadow(shadow);
}
ce.shadow = shadowdom;
}
@ -157,55 +151,32 @@ export default function read_options(node) {
break;
}
case 'namespace': {
const value = get_static_value(attribute, () =>
error(attribute, 'invalid-svelte-option-namespace')
);
if (typeof value !== 'string') {
error(attribute, 'invalid-svelte-option-namespace');
}
const value = get_static_value(attribute);
if (value === namespace_svg) {
component_options.namespace = 'svg';
} else if (value === 'html' || value === 'svg' || value === 'foreign') {
component_options.namespace = value;
} else {
error(attribute, 'invalid-svelte-option-namespace');
e.svelte_options_invalid_attribute_value(attribute, `"html", "svg" or "foreign"`);
}
break;
}
case 'immutable': {
const value = get_static_value(attribute, () =>
error(attribute, 'invalid-svelte-option-immutable')
);
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-immutable');
}
component_options.immutable = value;
component_options.immutable = get_boolean_value(attribute);
break;
}
case 'preserveWhitespace': {
const value = get_static_value(attribute, () =>
error(attribute, 'invalid-svelte-option-preserveWhitespace')
);
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-preserveWhitespace');
}
component_options.preserveWhitespace = value;
component_options.preserveWhitespace = get_boolean_value(attribute);
break;
}
case 'accessors': {
const value = get_static_value(attribute, () =>
error(attribute, 'invalid-svelte-option-accessors')
);
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-accessors');
}
component_options.accessors = value;
component_options.accessors = get_boolean_value(attribute);
break;
}
default:
error(attribute, 'unknown-svelte-option-attribute', name);
e.svelte_options_unknown_attribute(attribute, name);
}
}
@ -214,33 +185,43 @@ export default function read_options(node) {
/**
* @param {any} attribute
* @param {(attribute: any) => never} error
*/
function get_static_value(attribute, error) {
function get_static_value(attribute) {
const { value } = attribute;
const chunk = value[0];
if (!chunk) return true;
if (value.length > 1) {
error(attribute);
return null;
}
if (chunk.type === 'Text') return chunk.data;
if (chunk.expression.type !== 'Literal') {
error(attribute);
return null;
}
return chunk.expression.value;
}
/**
* @param {any} attribute
* @param {string} tag
*/
function get_boolean_value(attribute) {
const value = get_static_value(attribute);
if (typeof value !== 'boolean') {
e.svelte_options_invalid_attribute_value(attribute, 'true or false');
}
return value;
}
/**
* @param {any} attribute
* @param {string | null} tag
* @returns {asserts tag is string}
*/
function validate_tag(attribute, tag) {
if (typeof tag !== 'string' && tag !== null) {
error(attribute, 'invalid-tag-property');
if (typeof tag !== 'string') {
e.svelte_options_invalid_tagname(attribute);
}
if (tag && !regex_valid_tag_name.test(tag)) {
error(attribute, 'invalid-tag-property');
e.svelte_options_invalid_tagname(attribute);
}
// TODO do we still need this?
// if (tag && !component.compile_options.customElement) {

@ -1,6 +1,6 @@
import * as acorn from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
@ -16,13 +16,13 @@ function get_context(attributes) {
if (!context) return 'default';
if (context.value.length !== 1 || context.value[0].type !== 'Text') {
error(context.start, 'invalid-script-context');
e.script_invalid_context(context.start);
}
const value = context.value[0].data;
if (value !== 'module') {
error(context.start, 'invalid-script-context');
e.script_invalid_context(context.start);
}
return value;
@ -38,7 +38,7 @@ export function read_script(parser, start, attributes) {
const script_start = parser.index;
const data = parser.read_until(regex_closing_script_tag);
if (parser.index >= parser.template.length) {
error(parser.template.length, 'unclosed-element', 'script');
e.element_unclosed(parser.template.length, 'script');
}
const source =

@ -1,4 +1,4 @@
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/;
const REGEX_CLOSING_BRACKET = /[\s\]]/;
@ -64,7 +64,7 @@ function read_body(parser, close) {
}
}
error(parser.template.length, 'expected-token', close);
e.expected_token(parser.template.length, close);
}
/**
@ -154,7 +154,7 @@ function read_selector_list(parser, inside_pseudo_class = false) {
}
}
error(parser.template.length, 'unexpected-eof');
e.unexpected_eof(parser.template.length);
}
/**
@ -352,7 +352,7 @@ function read_selector(parser, inside_pseudo_class = false) {
if (combinator) {
if (relative_selector.selectors.length === 0) {
if (!inside_pseudo_class) {
error(start, 'invalid-css-selector');
e.css_selector_invalid(start);
}
} else {
relative_selector.end = index;
@ -365,12 +365,12 @@ function read_selector(parser, inside_pseudo_class = false) {
parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
error(parser.index, 'invalid-css-selector');
e.css_selector_invalid(parser.index);
}
}
}
error(parser.template.length, 'unexpected-eof');
e.unexpected_eof(parser.template.length);
}
/**
@ -471,12 +471,13 @@ function read_declaration(parser) {
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
parser.allow_whitespace();
parser.eat(':');
let index = parser.index;
parser.allow_whitespace();
const value = read_value(parser);
if (!value && !property.startsWith('--')) {
error(parser.index, 'invalid-css-declaration');
e.css_empty_declaration({ start, end: index });
}
const end = parser.index;
@ -531,7 +532,7 @@ function read_value(parser) {
parser.index++;
}
error(parser.template.length, 'unexpected-eof');
e.unexpected_eof(parser.template.length);
}
/**
@ -564,7 +565,7 @@ function read_attribute_value(parser) {
parser.index++;
}
error(parser.template.length, 'unexpected-eof');
e.unexpected_eof(parser.template.length);
}
/**
@ -577,7 +578,7 @@ function read_identifier(parser) {
let identifier = '';
if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
error(start, 'invalid-css-identifier');
e.css_expected_identifier(start);
}
let escaped = false;
@ -602,7 +603,7 @@ function read_identifier(parser) {
}
if (identifier === '') {
error(start, 'invalid-css-identifier');
e.css_expected_identifier(start);
}
return identifier;

@ -1,11 +1,9 @@
import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js';
import fuzzymatch from '../utils/fuzzymatch.js';
import { is_void } from '../utils/names.js';
import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js';
import read_style from '../read/style.js';
import { closing_tag_omitted, decode_character_references } from '../utils/html.js';
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute } from '../../nodes.js';
@ -87,8 +85,7 @@ export default function tag(parser) {
type: 'Comment',
start,
end: parser.index,
data,
ignores: extract_svelte_ignore(data)
data
});
return;
@ -104,19 +101,18 @@ export default function tag(parser) {
['svelte:options', 'svelte:window', 'svelte:body', 'svelte:document'].includes(name) &&
/** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes.length
) {
error(
e.svelte_meta_invalid_content(
/** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes[0].start,
'invalid-element-content',
name
);
}
} else {
if (name in parser.meta_tags) {
error(start, 'duplicate-svelte-element', name);
e.svelte_meta_duplicate(start, name);
}
if (parent.type !== 'Root') {
error(start, 'invalid-svelte-element-placement', name);
e.svelte_meta_invalid_placement(start, name);
}
parser.meta_tags[name] = true;
@ -168,7 +164,7 @@ export default function tag(parser) {
if (is_closing_tag) {
if (is_void(name)) {
error(start, 'invalid-void-content');
e.void_element_invalid_content(start);
}
parser.eat('>', true);
@ -177,14 +173,9 @@ export default function tag(parser) {
while (/** @type {import('#compiler').RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
error(
start,
'invalid-closing-tag-after-autoclose',
name,
parser.last_auto_closed_tag.reason
);
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
error(start, 'invalid-closing-tag', name);
e.element_invalid_closing_tag(start, name);
}
}
@ -225,7 +216,7 @@ export default function tag(parser) {
while ((attribute = read(parser))) {
if (attribute.type === 'Attribute' || attribute.type === 'BindDirective') {
if (unique_names.includes(attribute.name)) {
error(attribute.start, 'duplicate-attribute');
e.attribute_duplicate(attribute.start);
// <svelte:element bind:this this=..> is allowed
} else if (attribute.name !== 'this') {
unique_names.push(attribute.name);
@ -242,7 +233,7 @@ export default function tag(parser) {
(attr) => attr.type === 'Attribute' && attr.name === 'this'
);
if (index === -1) {
error(start, 'missing-svelte-component-definition');
e.svelte_component_missing_this(start);
}
const definition = /** @type {import('#compiler').Attribute} */ (
@ -253,7 +244,7 @@ export default function tag(parser) {
definition.value.length !== 1 ||
definition.value[0].type === 'Text'
) {
error(definition.start, 'invalid-svelte-component-definition');
e.svelte_component_invalid_this(definition.start);
}
element.expression = definition.value[0].expression;
@ -265,14 +256,14 @@ export default function tag(parser) {
(attr) => attr.type === 'Attribute' && attr.name === 'this'
);
if (index === -1) {
error(start, 'missing-svelte-element-definition');
e.svelte_element_missing_this(start);
}
const definition = /** @type {import('#compiler').Attribute} */ (
element.attributes.splice(index, 1)[0]
);
if (definition.value === true || definition.value.length !== 1) {
error(definition.start, 'invalid-svelte-element-definition');
e.svelte_element_invalid_this(definition.start);
}
const chunk = definition.value[0];
element.tag =
@ -311,17 +302,17 @@ export default function tag(parser) {
}
if (content.context === 'module') {
if (current.module) error(start, 'duplicate-script-element');
if (current.module) e.script_duplicate(start);
current.module = content;
} else {
if (current.instance) error(start, 'duplicate-script-element');
if (current.instance) e.script_duplicate(start);
current.instance = content;
}
} else {
const content = read_style(parser, start, element.attributes);
content.content.comment = prev_comment;
if (current.css) error(start, 'duplicate-style-element');
if (current.css) e.style_duplicate(start);
current.css = content;
}
return;
@ -396,7 +387,7 @@ function read_tag_name(parser) {
}
if (!legal) {
error(start, 'invalid-self-placement');
e.svelte_self_invalid_placement(start);
}
return 'svelte:self';
@ -412,12 +403,12 @@ function read_tag_name(parser) {
if (meta_tags.has(name)) return name;
if (name.startsWith('svelte:')) {
const match = fuzzymatch(name.slice(7), valid_meta_tags);
error(start, 'invalid-svelte-tag', valid_meta_tags, match);
const list = `${valid_meta_tags.slice(0, -1).join(', ')} or ${valid_meta_tags[valid_meta_tags.length - 1]}`;
e.svelte_meta_invalid_tag(start, list);
}
if (!valid_tag_name.test(name)) {
error(start, 'invalid-tag-name');
e.element_invalid_tag_name(start);
}
return name;
@ -445,7 +436,7 @@ function read_static_attribute(parser) {
parser.allow_whitespace();
let raw = parser.match_regex(regex_attribute_value);
if (!raw) {
error(parser.index, 'missing-attribute-value');
e.expected_attribute_value(parser.index);
}
parser.index += raw.length;
@ -468,7 +459,7 @@ function read_static_attribute(parser) {
}
if (parser.match_regex(regex_starts_with_quote_characters)) {
error(parser.index, 'expected-token', '=');
e.expected_token(parser.index, '=');
}
return create_attribute(name, start, parser.index, value);
@ -509,7 +500,7 @@ function read_attribute(parser) {
const name = parser.read_identifier();
if (name === null) {
error(start, 'empty-attribute-shorthand');
e.attribute_empty_shorthand(start);
}
parser.allow_whitespace();
@ -554,14 +545,14 @@ function read_attribute(parser) {
value = read_attribute_value(parser);
end = parser.index;
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
error(parser.index, 'expected-token', '=');
e.expected_token(parser.index, '=');
}
if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
if (directive_name === '') {
error(start + colon_index + 1, 'empty-directive-name', type);
e.directive_missing_name({ start, end: start + colon_index + 1 }, name);
}
if (type === 'StyleDirective') {
@ -586,7 +577,7 @@ function read_attribute(parser) {
const attribute_contains_text =
/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
if (attribute_contains_text) {
error(/** @type {number} */ (first_value.start), 'invalid-directive-value');
e.directive_invalid_value(/** @type {number} */ (first_value.start));
} else {
expression = first_value.expression;
}
@ -679,22 +670,22 @@ function read_attribute_value(parser) {
},
'in attribute value'
);
} catch (/** @type {any} e */ e) {
if (e.code === 'js-parse-error') {
} catch (/** @type {any} */ error) {
if (error.code === 'js_parse_error') {
// if the attribute value didn't close + self-closing tag
// eg: `<Component test={{a:1} />`
// acorn may throw a `Unterminated regular expression` because of `/>`
const pos = e.position?.[0];
const pos = error.position?.[0];
if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
parser.index = pos;
error(pos, 'unclosed-attribute-value', quote_mark || '}');
e.expected_token(pos, quote_mark || '}');
}
}
throw e;
throw error;
}
if (value.length === 0 && !quote_mark) {
error(parser.index, 'missing-attribute-value');
e.expected_attribute_value(parser.index);
}
if (quote_mark) parser.index += 1;
@ -741,12 +732,12 @@ function read_sequence(parser, done, location) {
const index = parser.index - 1;
parser.eat('#');
const name = parser.read_until(/[^a-z]/);
error(index, 'invalid-block-placement', location, name);
e.block_invalid_placement(index, name, location);
} else if (parser.match('@')) {
const index = parser.index - 1;
parser.eat('@');
const name = parser.read_until(/[^a-z]/);
error(index, 'invalid-tag-placement', location, name);
e.tag_invalid_placement(index, name, location);
}
flush(parser.index - 1);
@ -784,5 +775,5 @@ function read_sequence(parser, done, location) {
}
}
error(parser.template.length, 'unexpected-eof');
e.unexpected_eof(parser.template.length);
}

@ -1,6 +1,6 @@
import read_pattern from '../read/context.js';
import read_expression from '../read/expression.js';
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
import { create_fragment } from '../utils/create.js';
import { walk } from 'zimmerframe';
@ -147,7 +147,7 @@ function open(parser) {
parser.allow_whitespace();
index = parser.read_identifier();
if (!index) {
error(parser.index, 'expected-identifier');
e.expected_identifier(parser.index);
}
parser.allow_whitespace();
@ -265,7 +265,7 @@ function open(parser) {
const name_end = parser.index;
if (name === null) {
error(parser.index, 'expected-identifier');
e.expected_identifier(parser.index);
}
parser.eat('(', true);
@ -320,7 +320,7 @@ function open(parser) {
return;
}
error(parser.index, 'expected-block-type');
e.expected_block_type(parser.index);
}
/** @param {import('../index.js').Parser} parser */
@ -330,8 +330,8 @@ function next(parser) {
const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
if (block.type === 'IfBlock') {
if (!parser.eat('else')) error(start, 'expected-token', '{:else} or {:else if}');
if (parser.eat('if')) error(start, 'invalid-elseif');
if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
if (parser.eat('if')) e.block_invalid_elseif(start);
parser.allow_whitespace();
@ -373,7 +373,7 @@ function next(parser) {
}
if (block.type === 'EachBlock') {
if (!parser.eat('else')) error(start, 'expected-token', '{:else}');
if (!parser.eat('else')) e.expected_token(start, '{:else}');
parser.allow_whitespace();
parser.eat('}', true);
@ -389,7 +389,7 @@ function next(parser) {
if (block.type === 'AwaitBlock') {
if (parser.eat('then')) {
if (block.then) {
error(start, 'duplicate-block-part', '{:then}');
e.block_duplicate_clause(start, '{:then}');
}
if (!parser.eat('}')) {
@ -408,7 +408,7 @@ function next(parser) {
if (parser.eat('catch')) {
if (block.catch) {
error(start, 'duplicate-block-part', '{:catch}');
e.block_duplicate_clause(start, '{:catch}');
}
if (!parser.eat('}')) {
@ -425,10 +425,10 @@ function next(parser) {
return;
}
error(start, 'expected-token', '{:then ...} or {:catch ...}');
e.expected_token(start, '{:then ...} or {:catch ...}');
}
error(start, 'invalid-continuing-block-placement');
e.block_invalid_continuation_placement(start);
}
/** @param {import('../index.js').Parser} parser */
@ -466,11 +466,11 @@ function close(parser) {
case 'RegularElement':
// TODO handle implicitly closed elements
error(start, 'unexpected-block-close');
e.block_unexpected_close(start);
break;
default:
error(start, 'unexpected-block-close');
e.block_unexpected_close(start);
}
parser.allow_whitespace();
@ -522,7 +522,7 @@ function special(parser) {
identifiers.forEach(
/** @param {any} node */ (node) => {
if (node.type !== 'Identifier') {
error(/** @type {number} */ (node.start), 'invalid-debug');
e.debug_tag_invalid_arguments(/** @type {number} */ (node.start));
}
}
);
@ -583,7 +583,7 @@ function special(parser) {
expression.expression.type !== 'CallExpression' ||
!expression.expression.optional)
) {
error(expression, 'invalid-render-expression');
e.render_tag_invalid_expression(expression);
}
parser.allow_whitespace();

@ -7,11 +7,12 @@ import {
regex_starts_with_vowel,
regex_whitespaces
} from '../patterns.js';
import { warn } from '../../warnings.js';
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';
const aria_roles = roles_map.keys();
const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract);
@ -102,7 +103,7 @@ function has_disabled_attribute(attribute_map) {
const aria_disabled_attr = attribute_map.get('aria-disabled');
if (aria_disabled_attr) {
const aria_disabled_attr_value = get_static_value(aria_disabled_attr);
if (aria_disabled_attr_value === true) {
if (aria_disabled_attr_value === 'true') {
return true;
}
}
@ -593,40 +594,52 @@ function is_parent(parent, elements) {
}
/**
* @param {import('#compiler').Attribute} attribute
* @param {import('aria-query').ARIAProperty} name
* @param {import('aria-query').ARIAPropertyDefinition} schema
* @param {string | boolean} value
* @returns {boolean}
* @param {string | true | null} value
*/
function is_valid_aria_attribute_value(schema, value) {
switch (schema.type) {
case 'boolean':
return typeof value === 'boolean';
case 'string':
case 'id':
return typeof value === 'string';
case 'tristate':
return typeof value === 'boolean' || value === 'mixed';
case 'integer':
case 'number':
return typeof value !== 'boolean' && isNaN(Number(value)) === false;
case 'token': // single token
return (
(schema.values || []).indexOf(typeof value === 'string' ? value.toLowerCase() : value) > -1
);
case 'idlist': // if list of ids, split each
return (
typeof value === 'string' &&
value.split(regex_whitespaces).every((id) => typeof id === 'string')
function validate_aria_attribute_value(attribute, name, schema, value) {
const type = schema.type;
if (value === null) return;
if (value === true) value = '';
if (type === 'boolean' && value !== 'true' && value !== 'false') {
w.a11y_incorrect_aria_attribute_type_boolean(attribute, name);
} else if (type === 'integer' && (value === '' || !Number.isInteger(+value))) {
w.a11y_incorrect_aria_attribute_type_integer(attribute, name);
} else if (type === 'number' && (value === '' || isNaN(+value))) {
w.a11y_incorrect_aria_attribute_type(attribute, name, 'number');
} else if ((type === 'string' || type === 'id') && value === '') {
w.a11y_incorrect_aria_attribute_type(attribute, name, 'non-empty string');
} else if (type === 'idlist' && value === '') {
w.a11y_incorrect_aria_attribute_type_idlist(attribute, name);
} else if (type === 'token') {
const values = (schema.values ?? []).map((value) => value.toString());
if (!values.includes(value.toLowerCase())) {
w.a11y_incorrect_aria_attribute_type_token(
attribute,
name,
list(values.map((v) => `"${v}"`))
);
case 'tokenlist': // if list of tokens, split each
return (
typeof value === 'string' &&
value
.split(regex_whitespaces)
.every((token) => (schema.values || []).indexOf(token.toLowerCase()) > -1)
}
} else if (type === 'tokenlist') {
const values = (schema.values ?? []).map((value) => value.toString());
if (
value
.toLowerCase()
.split(regex_whitespaces)
.some((value) => !values.includes(value))
) {
w.a11y_incorrect_aria_attribute_type_tokenlist(
attribute,
name,
list(values.map((v) => `"${v}"`))
);
default:
return false;
}
} else if (type === 'tristate' && value !== 'true' && value !== 'false' && value !== 'mixed') {
w.a11y_incorrect_aria_attribute_type_tristate(attribute, name);
}
}
@ -642,7 +655,8 @@ function warn_missing_attribute(node, attributes, name = node.name) {
attributes.length > 1
? attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}`
: attributes[0];
return /** @type {const} */ ([node, 'a11y-missing-attribute', name, article, sequence]);
w.a11y_missing_attribute(node, name, article, sequence);
}
/**
@ -667,22 +681,11 @@ function get_static_text_value(attribute) {
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('./types.js').AnalysisState} state
* @param {import('#compiler').SvelteNode[]} path
*/
function check_element(node, state, path) {
function check_element(node, state) {
// foreign namespace means elements can have completely different meanings, therefore we don't check them
if (state.options.namespace === 'foreign') return;
/**
* @template {keyof import('../../warnings.js').AllWarnings} T
* @param {{ start?: number, end?: number }} node
* @param {T} code
* @param {Parameters<import('../../warnings.js').AllWarnings[T]>} args
* @returns {void}
*/
const push_warning = (node, code, ...args) =>
warn(state.analysis.warnings, node, path, code, ...args);
/** @type {Map<string, import('#compiler').Attribute>} */
const attribute_map = new Map();
@ -729,28 +732,30 @@ function check_element(node, state, path) {
if (name.startsWith('aria-')) {
if (invisible_elements.includes(node.name)) {
// aria-unsupported-elements
push_warning(attribute, 'a11y-aria-attributes', node.name);
w.a11y_aria_attributes(attribute, node.name);
}
const type = name.slice(5);
if (!aria_attributes.includes(type)) {
const match = fuzzymatch(type, aria_attributes);
push_warning(attribute, 'a11y-unknown-aria-attribute', type, match);
w.a11y_unknown_aria_attribute(attribute, type, match);
}
if (name === 'aria-hidden' && regex_heading_tags.test(node.name)) {
push_warning(attribute, 'a11y-hidden', node.name);
w.a11y_hidden(attribute, node.name);
}
// aria-proptypes
let value = get_static_value(attribute);
if (value === 'true') value = true;
if (value === 'false') value = false;
if (value !== null && value !== undefined) {
const schema = aria.get(/** @type {import('aria-query').ARIAProperty} */ (name));
if (schema !== undefined && !is_valid_aria_attribute_value(schema, value)) {
push_warning(attribute, 'a11y-incorrect-aria-attribute-type', schema, name);
}
const schema = aria.get(/** @type {import('aria-query').ARIAProperty} */ (name));
if (schema !== undefined) {
validate_aria_attribute_value(
attribute,
/** @type {import('aria-query').ARIAProperty} */ (name),
schema,
value
);
}
// aria-activedescendant-has-tabindex
@ -760,7 +765,7 @@ function check_element(node, state, path) {
!is_interactive_element(node.name, attribute_map) &&
!attribute_map.has('tabindex')
) {
push_warning(attribute, 'a11y-aria-activedescendant-has-tabindex');
w.a11y_aria_activedescendant_has_tabindex(attribute);
}
}
@ -768,7 +773,7 @@ function check_element(node, state, path) {
if (name === 'role') {
if (invisible_elements.includes(node.name)) {
// aria-unsupported-elements
push_warning(attribute, 'a11y-misplaced-role', node.name);
w.a11y_misplaced_role(attribute, node.name);
}
const value = get_static_value(attribute);
@ -778,10 +783,10 @@ function check_element(node, state, path) {
/** @type {import('aria-query').ARIARoleDefinitionKey} current_role */ (c_r);
if (current_role && is_abstract_role(current_role)) {
push_warning(attribute, 'a11y-no-abstract-role', current_role);
w.a11y_no_abstract_role(attribute, current_role);
} else if (current_role && !aria_roles.includes(current_role)) {
const match = fuzzymatch(current_role, aria_roles);
push_warning(attribute, 'a11y-unknown-role', current_role, match);
w.a11y_unknown_role(attribute, current_role, match);
}
// no-redundant-roles
@ -790,7 +795,7 @@ function check_element(node, state, path) {
// <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back
!['ul', 'ol', 'li'].includes(node.name)
) {
push_warning(attribute, 'a11y-no-redundant-roles', current_role);
w.a11y_no_redundant_roles(attribute, current_role);
}
// Footers and headers are special cases, and should not have redundant roles unless they are the children of sections or articles.
@ -799,7 +804,7 @@ function check_element(node, state, path) {
const has_nested_redundant_role =
current_role === a11y_nested_implicit_semantics.get(node.name);
if (has_nested_redundant_role) {
push_warning(attribute, 'a11y-no-redundant-roles', current_role);
w.a11y_no_redundant_roles(attribute, current_role);
}
}
@ -815,11 +820,13 @@ function check_element(node, state, path) {
(prop) => !attributes.find((a) => a.name === prop)
);
if (has_missing_props) {
push_warning(
w.a11y_role_has_required_aria_props(
attribute,
'a11y-role-has-required-aria-props',
current_role,
required_role_props
list(
required_role_props.map((v) => `"${v}"`),
'and'
)
);
}
}
@ -838,7 +845,7 @@ function check_element(node, state, path) {
a11y_interactive_handlers.includes(handler)
);
if (has_interactive_handlers) {
push_warning(node, 'a11y-interactive-supports-focus', current_role);
w.a11y_interactive_supports_focus(node, current_role);
}
}
@ -847,12 +854,7 @@ function check_element(node, state, path) {
is_interactive_element(node.name, attribute_map) &&
(is_non_interactive_roles(current_role) || is_presentation_role(current_role))
) {
push_warning(
node,
'a11y-no-interactive-element-to-noninteractive-role',
current_role,
node.name
);
w.a11y_no_interactive_element_to_noninteractive_role(node, node.name, current_role);
}
// no-noninteractive-element-to-interactive-role
@ -863,12 +865,7 @@ function check_element(node, state, path) {
current_role
)
) {
push_warning(
node,
'a11y-no-noninteractive-element-to-interactive-role',
current_role,
node.name
);
w.a11y_no_noninteractive_element_to_interactive_role(node, node.name, current_role);
}
}
}
@ -876,17 +873,17 @@ function check_element(node, state, path) {
// no-access-key
if (name === 'accesskey') {
push_warning(attribute, 'a11y-accesskey');
w.a11y_accesskey(attribute);
}
// no-autofocus
if (name === 'autofocus') {
push_warning(attribute, 'a11y-autofocus');
w.a11y_autofocus(attribute);
}
// scope
if (name === 'scope' && !is_dynamic_element && node.name !== 'th') {
push_warning(attribute, 'a11y-misplaced-scope');
w.a11y_misplaced_scope(attribute);
}
// tabindex-no-positive
@ -894,7 +891,7 @@ function check_element(node, state, path) {
const value = get_static_value(attribute);
// @ts-ignore todo is tabindex=true correct case?
if (!isNaN(value) && +value > 0) {
push_warning(attribute, 'a11y-positive-tabindex');
w.a11y_positive_tabindex(attribute);
}
}
}
@ -918,7 +915,7 @@ function check_element(node, state, path) {
const has_key_event =
handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress');
if (!has_key_event) {
push_warning(node, 'a11y-click-events-have-key-events');
w.a11y_click_events_have_key_events(node);
}
}
}
@ -936,7 +933,7 @@ function check_element(node, state, path) {
const tab_index = attribute_map.get('tabindex');
const tab_index_value = get_static_text_value(tab_index);
if (tab_index && (tab_index_value === null || Number(tab_index_value) >= 0)) {
push_warning(node, 'a11y-no-noninteractive-tabindex');
w.a11y_no_noninteractive_tabindex(node);
}
}
@ -951,14 +948,11 @@ function check_element(node, state, path) {
if (
invalid_aria_props.includes(/** @type {import('aria-query').ARIAProperty} */ (attr.name))
) {
push_warning(
attr,
'a11y-role-supports-aria-props',
attr.name,
role_value,
is_implicit,
node.name
);
if (is_implicit) {
w.a11y_role_supports_aria_props_implicit(attr, attr.name, role_value, node.name);
} else {
w.a11y_role_supports_aria_props(attr, attr.name, role_value);
}
}
}
}
@ -976,7 +970,7 @@ function check_element(node, state, path) {
a11y_recommended_interactive_handlers.includes(handler)
);
if (has_interactive_handlers) {
push_warning(node, 'a11y-no-noninteractive-element-interactions', node.name);
w.a11y_no_noninteractive_element_interactions(node, node.name);
}
}
@ -995,16 +989,16 @@ function check_element(node, state, path) {
a11y_interactive_handlers.includes(handler)
);
if (interactive_handlers.length > 0) {
push_warning(node, 'a11y-no-static-element-interactions', node.name, interactive_handlers);
w.a11y_no_static_element_interactions(node, node.name, list(interactive_handlers));
}
}
if (handlers.has('mouseover') && !handlers.has('focus')) {
push_warning(node, 'a11y-mouse-events-have-key-events', 'mouseover', 'focus');
w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus');
}
if (handlers.has('mouseout') && !handlers.has('blur')) {
push_warning(node, 'a11y-mouse-events-have-key-events', 'mouseout', 'blur');
w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur');
}
// element-specific checks
@ -1023,14 +1017,14 @@ function check_element(node, state, path) {
const href_value = get_static_text_value(href);
if (href_value !== null) {
if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) {
push_warning(href, 'a11y-invalid-attribute', href.name, href_value);
w.a11y_invalid_attribute(href, href_value, href.name);
}
}
} else if (!has_spread) {
const id_attribute = get_static_value(attribute_map.get('id'));
const name_attribute = get_static_value(attribute_map.get('name'));
if (!id_attribute && !name_attribute) {
push_warning(...warn_missing_attribute(node, ['href']));
warn_missing_attribute(node, ['href']);
}
}
} else if (!has_spread) {
@ -1038,7 +1032,7 @@ function check_element(node, state, path) {
if (required_attributes) {
const has_attribute = required_attributes.some((name) => attribute_map.has(name));
if (!has_attribute) {
push_warning(...warn_missing_attribute(node, required_attributes));
warn_missing_attribute(node, required_attributes);
}
}
}
@ -1050,7 +1044,7 @@ function check_element(node, state, path) {
const required_attributes = ['alt', 'aria-label', 'aria-labelledby'];
const has_attribute = required_attributes.some((name) => attribute_map.has(name));
if (!has_attribute) {
push_warning(...warn_missing_attribute(node, required_attributes, 'input type="image"'));
warn_missing_attribute(node, required_attributes, 'input type="image"');
}
}
// autocomplete-valid
@ -1058,7 +1052,11 @@ function check_element(node, state, path) {
if (type && autocomplete) {
const autocomplete_value = get_static_value(autocomplete);
if (!is_valid_autocomplete(autocomplete_value)) {
push_warning(autocomplete, 'a11y-autocomplete-valid', type_value, autocomplete_value);
w.a11y_autocomplete_valid(
autocomplete,
/** @type {string} */ (autocomplete_value),
type_value ?? '...'
);
}
}
}
@ -1068,7 +1066,7 @@ function check_element(node, state, path) {
const aria_hidden = get_static_value(attribute_map.get('aria-hidden'));
if (alt_attribute && !aria_hidden) {
if (/\b(image|picture|photo)\b/i.test(alt_attribute)) {
push_warning(node, 'a11y-img-redundant-alt');
w.a11y_img_redundant_alt(node);
}
}
}
@ -1098,7 +1096,7 @@ function check_element(node, state, path) {
return has;
};
if (!attribute_map.has('for') && !has_input_child(node)) {
push_warning(node, 'a11y-label-has-associated-control');
w.a11y_label_has_associated_control(node);
}
}
@ -1120,13 +1118,13 @@ function check_element(node, state, path) {
);
}
if (!has_caption) {
push_warning(node, 'a11y-media-has-caption');
w.a11y_media_has_caption(node);
}
}
if (node.name === 'figcaption') {
if (!is_parent(node.parent, ['figure'])) {
push_warning(node, 'a11y-structure', true);
w.a11y_figcaption_parent(node);
}
}
@ -1140,13 +1138,13 @@ function check_element(node, state, path) {
(child) => child.type === 'RegularElement' && child.name === 'figcaption'
);
if (index !== -1 && index !== 0 && index !== children.length - 1) {
push_warning(children[index], 'a11y-structure', false);
w.a11y_figcaption_index(children[index]);
}
}
if (a11y_distracting_elements.includes(node.name)) {
// no-distracting-elements
push_warning(node, 'a11y-distracting-elements', node.name);
w.a11y_distracting_elements(node, node.name);
}
// Check content
@ -1156,7 +1154,7 @@ function check_element(node, state, path) {
a11y_required_content.includes(node.name) &&
node.fragment.nodes.length === 0
) {
push_warning(node, 'a11y-missing-content', node.name);
w.a11y_missing_content(node, node.name);
}
}
@ -1165,9 +1163,9 @@ function check_element(node, state, path) {
*/
export const a11y_validators = {
RegularElement(node, context) {
check_element(node, context.state, context.path);
check_element(node, context.state);
},
SvelteElement(node, context) {
check_element(node, context.state, context.path);
check_element(node, context.state);
}
};

@ -1,5 +1,5 @@
import { walk } from 'zimmerframe';
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { merge } from '../../visitors.js';
@ -98,31 +98,26 @@ const validation_visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
if (node.prelude.children.length > 1) {
error(node.prelude, 'invalid-css-global-block-list');
e.css_global_block_invalid_list(node.prelude);
}
const complex_selector = node.prelude.children[0];
const relative_selector = complex_selector.children[complex_selector.children.length - 1];
if (relative_selector.selectors.length > 1) {
error(
relative_selector.selectors[relative_selector.selectors.length - 1],
'invalid-css-global-block-modifier'
e.css_global_block_invalid_modifier(
relative_selector.selectors[relative_selector.selectors.length - 1]
);
}
if (relative_selector.combinator && relative_selector.combinator.name !== ' ') {
error(
relative_selector,
'invalid-css-global-block-combinator',
relative_selector.combinator.name
);
e.css_global_block_invalid_combinator(relative_selector, relative_selector.combinator.name);
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (declaration) {
error(declaration, 'invalid-css-global-block-declaration');
e.css_global_block_invalid_declaration(declaration);
}
}
@ -137,7 +132,7 @@ const validation_visitors = {
if (a !== b) {
for (let i = a; i <= b; i += 1) {
if (is_global(node.children[i])) {
error(node.children[i].selectors[0], 'invalid-css-global-placement');
e.css_global_invalid_placement(node.children[i].selectors[0]);
}
}
}
@ -152,12 +147,12 @@ const validation_visitors = {
const child = selector.args?.children[0].children[0];
// ensure `:global(element)` to be at the first position in a compound selector
if (child?.selectors[0].type === 'TypeSelector' && i !== 0) {
error(selector, 'invalid-css-global-selector-list');
e.css_global_invalid_selector_list(selector);
}
// ensure `:global(.class)` is not followed by a type selector, eg: `:global(.class)element`
if (relative_selector.selectors[i + 1]?.type === 'TypeSelector') {
error(relative_selector.selectors[i + 1], 'invalid-css-type-selector-placement');
e.css_type_selector_invalid_placement(relative_selector.selectors[i + 1]);
}
// ensure `:global(...)`contains a single selector
@ -167,7 +162,7 @@ const validation_visitors = {
selector.args.children.length > 1 &&
(node.children.length > 1 || relative_selector.selectors.length > 1)
) {
error(selector, 'invalid-css-global-selector');
e.css_global_invalid_selector(selector);
}
}
}
@ -176,7 +171,7 @@ const validation_visitors = {
NestingSelector(node, context) {
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) {
error(node, 'invalid-nesting-selector');
e.css_nesting_selector_invalid_placement(node);
}
}
};

@ -1,16 +1,15 @@
import { walk } from 'zimmerframe';
import { warn } from '../../../warnings.js';
import * as w from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js';
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').RawWarning[]} warnings
*/
export function warn_unused(stylesheet, warnings) {
walk(stylesheet, { warnings, stylesheet }, visitors);
export function warn_unused(stylesheet) {
walk(stylesheet, { stylesheet }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { warnings: import('../../types.js').RawWarning[], stylesheet: import('#compiler').Css.StyleSheet }>} */
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { stylesheet: import('#compiler').Css.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {
@ -26,7 +25,7 @@ const visitors = {
if (!node.metadata.used) {
const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start);
warn(context.state.warnings, node, context.path, 'css-unused-selector', text);
w.css_unused_selector(node, text);
}
context.next();

@ -1,6 +1,7 @@
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { error } from '../../errors.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import {
extract_identifiers,
extract_all_identifiers_from_expression,
@ -14,7 +15,6 @@ import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import { merge } from '../visitors.js';
import { validation_legacy, validation_runes, validation_runes_js } from './validation.js';
import { warn } from '../../warnings.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { regex_starts_with_newline } from '../patterns.js';
import { create_attribute, is_element_node } from '../nodes.js';
@ -24,6 +24,7 @@ import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
/**
* @param {import('#compiler').Script | null} script
@ -229,20 +230,13 @@ export function analyze_module(ast, options) {
for (const [name, references] of scope.references) {
if (name[0] !== '$' || ReservedKeywords.includes(name)) continue;
if (name === '$' || name[1] === '$') {
error(references[0].node, 'illegal-global', name);
e.global_reference_invalid(references[0].node, name);
}
}
/** @type {import('../types').RawWarning[]} */
const warnings = [];
const analysis = {
warnings
};
walk(
/** @type {import('estree').Node} */ (ast),
{ scope, analysis },
{ scope, analysis: { runes: true } },
// @ts-expect-error TODO clean this mess up
merge(set_scope(scopes), validation_runes_js, runes_scope_js_tweaker)
);
@ -250,7 +244,6 @@ export function analyze_module(ast, options) {
return {
module: { ast, scope, scopes },
name: options.filename || 'module',
warnings,
accessors: false,
runes: true,
immutable: true
@ -274,14 +267,11 @@ export function analyze_component(root, source, options) {
/** @type {import('../types.js').Template} */
const template = { ast: root.fragment, scope, scopes };
/** @type {import('../types').RawWarning[]} */
const warnings = [];
// create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || ReservedKeywords.includes(name)) continue;
if (name === '$' || name[1] === '$') {
error(references[0].node, 'illegal-global', name);
e.global_reference_invalid(references[0].node, name);
}
const store_name = name.slice(1);
@ -321,16 +311,16 @@ export function analyze_component(root, source, options) {
}
if (is_nested_store_subscription_node) {
error(is_nested_store_subscription_node, 'illegal-store-subscription');
e.store_invalid_scoped_subscription(is_nested_store_subscription_node);
}
if (options.runes !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) {
error(references[0].node, 'illegal-global', name);
e.global_reference_invalid(references[0].node, name);
} else if (declaration !== null && Runes.includes(/** @type {any} */ (name))) {
for (const { node, path } of references) {
if (path.at(-1)?.type === 'CallExpression') {
warn(warnings, node, [], 'store-with-rune-name', store_name);
w.store_rune_conflict(node, store_name);
}
}
}
@ -345,7 +335,7 @@ export function analyze_component(root, source, options) {
// const state = $state(0) is valid
get_rune(/** @type {import('estree').Node} */ (path.at(-1)), module.scope) === null
) {
error(node, 'illegal-subscription');
e.store_invalid_subscription(node);
}
}
}
@ -380,17 +370,20 @@ export function analyze_component(root, source, options) {
uses_slots: false,
uses_component_bindings: false,
uses_render_tags: false,
needs_context: false,
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement
? true
: !!options.accessors ||
: (runes ? false : !!options.accessors) ||
// because $set method needs accessors
!!options.legacy?.componentApi,
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Map(),
warnings,
css: {
ast: root.css,
hash: root.css
@ -406,19 +399,31 @@ export function analyze_component(root, source, options) {
source
};
if (!options.customElement && root.options?.customElement) {
warn(analysis.warnings, root.options, [], 'missing-custom-element-compile-option');
if (root.options) {
for (const attribute of root.options.attributes) {
if (attribute.name === 'accessors') {
w.options_deprecated_accessors(attribute);
}
if (attribute.name === 'customElement' && !options.customElement) {
w.options_missing_custom_element(attribute);
}
if (attribute.name === 'immutable') {
w.options_deprecated_immutable(attribute);
}
}
}
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
if (props_refs) {
error(props_refs[0].node, 'invalid-legacy-props');
e.legacy_props_invalid(props_refs[0].node);
}
const rest_props_refs = module.scope.references.get('$$restProps');
if (rest_props_refs) {
error(rest_props_refs[0].node, 'invalid-legacy-rest-props');
e.legacy_rest_props_invalid(rest_props_refs[0].node);
}
for (const { ast, scope, scopes } of [module, instance, template]) {
@ -433,7 +438,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
private_derived_state: [],
function_depth: scope.function_depth
function_depth: scope.function_depth,
ignores: new Set()
};
walk(
@ -442,20 +448,6 @@ export function analyze_component(root, source, options) {
merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors)
);
}
if (analysis.exports.length > 0) {
for (const [_, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (
analysis.exports.some(
({ alias, name }) => (binding.prop_alias ?? binding.node.name) === (alias ?? name)
)
) {
error(binding.node, 'conflicting-property-name');
}
}
}
}
} else {
instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic');
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
@ -475,7 +467,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
private_derived_state: [],
function_depth: scope.function_depth
function_depth: scope.function_depth,
ignores: new Set()
};
walk(
@ -495,7 +488,7 @@ export function analyze_component(root, source, options) {
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
);
if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
warn(warnings, binding.node, [], 'unused-export-let', name);
w.export_let_unused(binding.node, name);
}
}
}
@ -503,8 +496,15 @@ export function analyze_component(root, source, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
}
if (analysis.event_directive_node && analysis.uses_event_attributes) {
e.mixed_event_handler_syntaxes(
analysis.event_directive_node,
analysis.event_directive_node.name
);
}
if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) {
error(analysis.slot_names.values().next().value, 'conflicting-slot-usage');
e.slot_snippet_conflict(analysis.slot_names.values().next().value);
}
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template
@ -535,7 +535,7 @@ export function analyze_component(root, source, options) {
type === 'AwaitBlock' ||
type === 'KeyBlock'
) {
warn(warnings, binding.node, [], 'non-state-reference', name);
w.non_reactive_update(binding.node, name);
continue outer;
}
}
@ -543,7 +543,7 @@ export function analyze_component(root, source, options) {
}
}
warn(warnings, binding.node, [], 'non-state-reference', name);
w.non_reactive_update(binding.node, name);
continue outer;
}
}
@ -557,7 +557,13 @@ export function analyze_component(root, source, options) {
for (const element of analysis.elements) {
prune(analysis.css.ast, element);
}
warn_unused(analysis.css.ast, analysis.warnings);
if (
!analysis.css.ast.content.comment ||
!extract_svelte_ignore(analysis.css.ast.content.comment.data).includes('css_unused_selector')
) {
warn_unused(analysis.css.ast);
}
outer: for (const element of analysis.elements) {
if (element.metadata.scoped) {
@ -679,7 +685,7 @@ const legacy_scope_tweaker = {
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
)
) {
warn(state.analysis.warnings, node, path, 'module-script-reactive-declaration');
w.reactive_declaration_module_script(node);
}
if (
@ -808,6 +814,8 @@ const legacy_scope_tweaker = {
return next();
}
state.analysis.needs_props = true;
if (!node.declaration) {
for (const specifier of node.specifiers) {
const binding = /** @type {import('#compiler').Binding} */ (
@ -865,7 +873,7 @@ const legacy_scope_tweaker = {
}
};
/** @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, { scope: Scope, analysis: { warnings: import('../types').RawWarning[] } }>} */
/** @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, { scope: Scope, analysis: { runes: true } }>} */
const runes_scope_js_tweaker = {
VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return;
@ -936,6 +944,8 @@ const runes_scope_tweaker = {
}
if (rune === '$props') {
state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;
@ -1043,8 +1053,94 @@ const function_visitor = (node, context) => {
});
};
/**
* A 'safe' identifier means that the `foo` in `foo.bar` or `foo()` will not
* call functions that require component context to exist
* @param {import('estree').Expression | import('estree').Super} expression
* @param {Scope} scope
*/
function is_safe_identifier(expression, scope) {
let node = expression;
while (node.type === 'MemberExpression') node = node.object;
if (node.type !== 'Identifier') return false;
const binding = scope.get(node.name);
if (!binding) return true;
if (binding.kind === 'store_sub') {
return is_safe_identifier({ name: node.name.slice(1), type: 'Identifier' }, scope);
}
return (
binding.declaration_kind !== 'import' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'rest_prop'
);
}
/** @type {import('./types').Visitors} */
const common_visitors = {
_(node, context) {
// @ts-expect-error
const comments = /** @type {import('estree').Comment[]} */ (node.leadingComments);
if (comments) {
/** @type {string[]} */
const ignores = [];
for (const comment of comments) {
ignores.push(...extract_svelte_ignore(comment.value));
}
if (ignores.length > 0) {
// @ts-expect-error see below
node.ignores = new Set([...context.state.ignores, ...ignores]);
}
}
// @ts-expect-error
if (node.ignores) {
context.next({
...context.state,
// @ts-expect-error see below
ignores: node.ignores
});
} else if (context.state.ignores.size > 0) {
// @ts-expect-error
node.ignores = context.state.ignores;
}
},
Fragment(node, context) {
/** @type {string[]} */
let ignores = [];
for (const child of node.nodes) {
if (child.type === 'Text' && child.data.trim() === '') {
continue;
}
if (child.type === 'Comment') {
ignores.push(...extract_svelte_ignore(child.data));
} else {
const combined_ignores = new Set(context.state.ignores);
for (const ignore of ignores) combined_ignores.add(ignore);
if (combined_ignores.size > 0) {
// TODO this is a grotesque hack that's made necessary by the fact that
// we can't call `context.visit(...)` here, because we do the convoluted
// visitor merging thing. I'm increasingly of the view that we should
// rearchitect this stuff and have a single visitor per node. It'd be
// more efficient and much simpler.
// @ts-expect-error
child.ignores = combined_ignores;
}
ignores = [];
}
}
},
Attribute(node, context) {
if (node.value === true) return;
@ -1066,6 +1162,11 @@ const common_visitors = {
});
if (is_event_attribute(node)) {
const parent = context.path.at(-1);
if (parent?.type === 'RegularElement' || parent?.type === 'SvelteElement') {
context.state.analysis.uses_event_attributes = true;
}
const expression = node.value[0].expression;
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
@ -1142,7 +1243,7 @@ const common_visitors = {
binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth
) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');
w.state_referenced_locally(node);
}
}
},
@ -1155,6 +1256,8 @@ const common_visitors = {
}
const callee = node.callee;
const rune = get_rune(node, context.state.scope);
if (callee.type === 'Identifier') {
const binding = context.state.scope.get(callee.name);
@ -1162,7 +1265,7 @@ const common_visitors = {
binding.is_called = true;
}
if (get_rune(node, context.state.scope) === '$derived') {
if (rune === '$derived') {
// special case — `$derived(foo)` is treated as `$derived(() => foo)`
// for the purposes of identifying static state references
context.next({
@ -1174,6 +1277,16 @@ const common_visitors = {
}
}
if (rune === '$effect' || rune === '$effect.pre') {
// `$effect` needs context because Svelte needs to know whether it should re-run
// effects that invalidate themselves, and that's determined by whether we're in runes mode
context.state.analysis.needs_context = true;
} else if (rune === null) {
if (!is_safe_identifier(callee, context.state.scope)) {
context.state.analysis.needs_context = true;
}
}
context.next();
},
MemberExpression(node, context) {
@ -1181,8 +1294,19 @@ const common_visitors = {
context.state.expression.metadata.dynamic = true;
}
if (!is_safe_identifier(node, context.state.scope)) {
context.state.analysis.needs_context = true;
}
context.next();
},
OnDirective(node, { state, path, next }) {
const parent = path.at(-1);
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
state.analysis.event_directive_node ??= node;
}
next();
},
BindDirective(node, context) {
let i = context.path.length;
while (i--) {
@ -1425,7 +1549,7 @@ function order_reactive_statements(unsorted_reactive_declarations) {
const cycle = check_graph_for_cycles(edges);
if (cycle?.length) {
const declaration = /** @type {Tuple[]} */ (lookup.get(cycle[0]))[0];
error(declaration[0], 'cyclical-reactive-declaration', cycle);
e.reactive_declaration_cycle(declaration[0], cycle.join(' → '));
}
// We use a map and take advantage of the fact that the spec says insertion order is preserved when iterating

@ -22,6 +22,7 @@ export interface AnalysisState {
expression: ExpressionTag | ClassDirective | SpreadAttribute | null;
private_derived_state: string[];
function_depth: number;
ignores: Set<string>;
}
export interface LegacyAnalysisState extends AnalysisState {

@ -1,23 +1,26 @@
import is_reference from 'is-reference';
import {
disallowed_paragraph_contents,
interactive_elements,
is_tag_valid_with_parent
} from '../../../constants.js';
import { error } from '../../errors.js';
import * as e from '../../errors.js';
import {
extract_identifiers,
get_parent,
is_event_attribute,
is_expression_attribute,
is_text_attribute,
object,
unwrap_optional
} from '../../utils/ast.js';
import { warn } from '../../warnings.js';
import * as w from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { binding_properties } from '../bindings.js';
import {
ContentEditableBindings,
EventModifiers,
Runes,
SVGElements,
VoidElements
} from '../constants.js';
@ -44,14 +47,14 @@ function validate_component(node, context) {
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective'
) {
error(attribute, 'invalid-component-directive');
e.component_invalid_directive(attribute);
}
if (
attribute.type === 'OnDirective' &&
(attribute.modifiers.length > 1 || attribute.modifiers.some((m) => m !== 'once'))
) {
error(attribute, 'invalid-event-modifier');
e.event_handler_invalid_component_modifier(attribute);
}
if (attribute.type === 'Attribute') {
@ -62,12 +65,12 @@ function validate_component(node, context) {
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression');
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}
validate_attribute_name(attribute, context);
validate_attribute_name(attribute);
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
@ -93,8 +96,12 @@ const react_attributes = new Map([
*/
function validate_element(node, context) {
let has_animate_directive = false;
let has_in_transition = false;
let has_out_transition = false;
/** @type {import('#compiler').TransitionDirective | null} */
let in_transition = null;
/** @type {import('#compiler').TransitionDirective | null} */
let out_transition = null;
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
@ -107,18 +114,18 @@ function validate_element(node, context) {
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression');
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}
if (regex_illegal_attribute_character.test(attribute.name)) {
error(attribute, 'invalid-attribute-name', attribute.name);
e.attribute_invalid_name(attribute, attribute.name);
}
if (attribute.name.startsWith('on') && attribute.name.length > 2) {
if (!is_expression) {
error(attribute, 'invalid-event-attribute-value');
e.attribute_invalid_event_handler(attribute);
}
const value = attribute.value[0].expression;
@ -127,13 +134,7 @@ function validate_element(node, context) {
value.name === attribute.name &&
!context.state.scope.get(value.name)
) {
warn(
context.state.analysis.warnings,
attribute,
context.path,
'global-event-reference',
attribute.name
);
w.attribute_global_event_reference(attribute, attribute.name);
}
}
@ -143,28 +144,21 @@ function validate_element(node, context) {
}
if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') {
warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is');
w.attribute_avoid_is(attribute);
}
const correct_name = react_attributes.get(attribute.name);
if (correct_name) {
warn(
context.state.analysis.warnings,
attribute,
context.path,
'invalid-html-attribute',
attribute.name,
correct_name
);
w.attribute_invalid_property_name(attribute, attribute.name, correct_name);
}
validate_attribute_name(attribute, context);
validate_attribute_name(attribute);
} else if (attribute.type === 'AnimateDirective') {
const parent = context.path.at(-2);
if (parent?.type !== 'EachBlock') {
error(attribute, 'invalid-animation', 'no-each');
e.animation_invalid_placement(attribute);
} else if (!parent.key) {
error(attribute, 'invalid-animation', 'each-key');
e.animation_missing_key(attribute);
} else if (
parent.body.nodes.filter(
(n) =>
@ -173,34 +167,39 @@ function validate_element(node, context) {
(n.type !== 'Text' || n.data.trim() !== '')
).length > 1
) {
error(attribute, 'invalid-animation', 'child');
e.animation_invalid_placement(attribute);
}
if (has_animate_directive) {
error(attribute, 'duplicate-animation');
e.animation_duplicate(attribute);
} else {
has_animate_directive = true;
}
} else if (attribute.type === 'TransitionDirective') {
if ((attribute.outro && has_out_transition) || (attribute.intro && has_in_transition)) {
/** @param {boolean} _in @param {boolean} _out */
const type = (_in, _out) => (_in && _out ? 'transition' : _in ? 'in' : 'out');
error(
attribute,
'duplicate-transition',
type(has_in_transition, has_out_transition),
type(attribute.intro, attribute.outro)
);
const existing = /** @type {import('#compiler').TransitionDirective | null} */ (
(attribute.intro && in_transition) || (attribute.outro && out_transition)
);
if (existing) {
const a = existing.intro ? (existing.outro ? 'transition' : 'in') : 'out';
const b = attribute.intro ? (attribute.outro ? 'transition' : 'in') : 'out';
if (a === b) {
e.transition_duplicate(attribute, a);
} else {
e.transition_conflict(attribute, a, b);
}
}
has_in_transition = has_in_transition || attribute.intro;
has_out_transition = has_out_transition || attribute.outro;
if (attribute.intro) in_transition = attribute;
if (attribute.outro) out_transition = attribute;
} else if (attribute.type === 'OnDirective') {
let has_passive_modifier = false;
let conflicting_passive_modifier = '';
for (const modifier of attribute.modifiers) {
if (!EventModifiers.includes(modifier)) {
error(attribute, 'invalid-event-modifier', EventModifiers);
const list = `${EventModifiers.slice(0, -1).join(', ')} or ${EventModifiers.at(-1)}`;
e.event_handler_invalid_modifier(attribute, list);
}
if (modifier === 'passive') {
has_passive_modifier = true;
@ -208,9 +207,8 @@ function validate_element(node, context) {
conflicting_passive_modifier = modifier;
}
if (has_passive_modifier && conflicting_passive_modifier) {
error(
e.event_handler_invalid_modifier_combination(
attribute,
'invalid-event-modifier-combination',
'passive',
conflicting_passive_modifier
);
@ -222,16 +220,15 @@ function validate_element(node, context) {
/**
* @param {import('#compiler').Attribute} attribute
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
*/
function validate_attribute_name(attribute, context) {
function validate_attribute_name(attribute) {
if (
attribute.name.includes(':') &&
!attribute.name.startsWith('xmlns:') &&
!attribute.name.startsWith('xlink:') &&
!attribute.name.startsWith('xml:')
) {
warn(context.state.analysis.warnings, attribute, context.path, 'illegal-attribute-character');
w.attribute_illegal_colon(attribute);
}
}
@ -259,7 +256,7 @@ function validate_slot_attribute(context, attribute) {
if (owner) {
if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-attribute');
e.slot_attribute_invalid(attribute);
}
if (
@ -268,13 +265,13 @@ function validate_slot_attribute(context, attribute) {
owner.type === 'SvelteSelf'
) {
if (owner !== context.path.at(-2)) {
error(attribute, 'invalid-slot-placement');
e.slot_attribute_invalid_placement(attribute);
}
const name = attribute.value[0].data;
if (context.state.component_slots.has(name)) {
error(attribute, 'duplicate-slot-name', name, owner.name);
e.slot_attribute_duplicate(attribute, name, owner.name);
}
context.state.component_slots.add(name);
@ -291,12 +288,12 @@ function validate_slot_attribute(context, attribute) {
}
}
error(node, 'invalid-default-slot-content');
e.slot_default_duplicate(node);
}
}
}
} else {
error(attribute, 'invalid-slot-placement');
e.slot_attribute_invalid_placement(attribute);
}
}
@ -309,7 +306,7 @@ function validate_block_not_empty(node, context) {
// Assumption: If the block has zero elements, someone's in the middle of typing it out,
// so don't warn in that case because it would be distracting.
if (node.nodes.length === 1 && node.nodes[0].type === 'Text' && !node.nodes[0].raw.trim()) {
warn(context.state.analysis.warnings, node.nodes[0], context.path, 'empty-block');
w.block_empty(node.nodes[0]);
}
}
@ -327,7 +324,7 @@ const validation = {
const left = object(assignee);
if (left === null) {
error(node, 'invalid-binding-expression');
e.bind_invalid_expression(node);
}
const binding = context.state.scope.get(left.name);
@ -347,36 +344,30 @@ const validation = {
binding.kind !== 'store_sub' &&
!binding.mutated)
) {
error(node.expression, 'invalid-binding-value');
e.bind_invalid_value(node.expression);
}
if (binding.kind === 'derived') {
error(node.expression, 'invalid-derived-binding');
e.constant_binding(node.expression, 'derived state');
}
if (context.state.analysis.runes && binding.kind === 'each') {
error(node, 'invalid-each-assignment');
e.each_item_invalid_assignment(node);
}
if (binding.kind === 'snippet') {
error(node, 'invalid-snippet-assignment');
e.snippet_parameter_assignment(node);
}
}
if (node.name === 'group') {
if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
throw new Error('Cannot find declaration for bind:group');
}
}
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
warn(
context.state.analysis.warnings,
binding.node,
context.path,
'invalid-rest-eachblock-binding',
binding.node.name
);
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
const parent = context.path.at(-1);
@ -389,21 +380,14 @@ const validation = {
parent?.type === 'SvelteBody'
) {
if (context.state.options.namespace === 'foreign' && node.name !== 'this') {
error(
node,
'invalid-binding',
node.name,
undefined,
'. Foreign elements only support bind:this'
);
e.bind_invalid_name(node, node.name, 'Foreign elements only support `bind:this`');
}
if (node.name in binding_properties) {
const property = binding_properties[node.name];
if (property.valid_elements && !property.valid_elements.includes(parent.name)) {
error(
e.bind_invalid_target(
node,
'invalid-binding',
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
);
@ -415,17 +399,17 @@ const validation = {
);
if (type && !is_text_attribute(type)) {
if (node.name !== 'value' || type.value === true) {
error(type, 'invalid-type-attribute');
e.attribute_invalid_type(type);
}
return; // bind:value can handle dynamic `type` attributes
}
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
error(node, 'invalid-binding', node.name, '<input type="checkbox">');
e.bind_invalid_target(node, node.name, '<input type="checkbox">');
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
error(node, 'invalid-binding', node.name, '<input type="file">');
e.bind_invalid_target(node, node.name, '<input type="file">');
}
}
@ -438,14 +422,13 @@ const validation = {
a.value !== true
);
if (multiple) {
error(multiple, 'invalid-multiple-attribute');
e.attribute_invalid_multiple(multiple);
}
}
if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) {
error(
e.bind_invalid_target(
node,
'invalid-binding',
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
);
@ -456,9 +439,9 @@ const validation = {
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable')
);
if (!contenteditable) {
error(node, 'missing-contenteditable-attribute');
e.attribute_contenteditable_missing(node);
} else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) {
error(contenteditable, 'dynamic-contenteditable-attribute');
e.attribute_contenteditable_dynamic(contenteditable);
}
}
} else {
@ -466,15 +449,15 @@ const validation = {
if (match) {
const property = binding_properties[match];
if (!property.valid_elements || property.valid_elements.includes(parent.name)) {
error(node, 'invalid-binding', node.name, undefined, ` (did you mean '${match}'?)`);
e.bind_invalid_name(node, node.name, `Did you mean '${match}'?`);
}
}
error(node, 'invalid-binding', node.name);
e.bind_invalid_name(node, node.name);
}
}
},
ExportDefaultDeclaration(node) {
error(node, 'default-export');
e.module_illegal_default_export(node);
},
ConstTag(node, context) {
const parent = context.path.at(-1);
@ -491,7 +474,7 @@ const validation = {
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {
error(node, 'invalid-const-placement');
e.const_tag_invalid_placement(node);
}
},
ImportDeclaration(node, context) {
@ -502,7 +485,7 @@ const validation = {
specifier.imported.name === 'beforeUpdate' ||
specifier.imported.name === 'afterUpdate'
) {
error(specifier, 'invalid-runes-mode-import', specifier.imported.name);
e.runes_mode_invalid_import(specifier, specifier.imported.name);
}
}
}
@ -520,14 +503,14 @@ const validation = {
parent.type !== 'SvelteSelf' &&
parent.type !== 'SvelteFragment')
) {
error(node, 'invalid-let-directive-placement');
e.let_directive_invalid_placement(node);
}
},
RegularElement(node, context) {
if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') {
error(node, 'invalid-textarea-content');
e.textarea_invalid_content(node);
}
}
}
@ -538,20 +521,14 @@ const validation = {
binding.declaration_kind === 'import' &&
binding.references.length === 0
) {
warn(
context.state.analysis.warnings,
node,
context.path,
'component-name-lowercase',
node.name
);
w.component_name_lowercase(node, node.name);
}
validate_element(node, context);
if (context.state.parent_element) {
if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) {
error(node, 'invalid-node-placement', `<${node.name}>`, context.state.parent_element);
e.node_invalid_placement(node, `<${node.name}>`, context.state.parent_element);
}
}
@ -563,7 +540,7 @@ const validation = {
parent.name === node.name &&
interactive_elements.has(parent.name)
) {
error(node, 'invalid-node-placement', `<${node.name}>`, parent.name);
e.node_invalid_placement(node, `<${node.name}>`, parent.name);
}
}
}
@ -572,7 +549,7 @@ const validation = {
const path = context.path;
for (let parent of path) {
if (parent.type === 'RegularElement' && parent.name === 'p') {
error(node, 'invalid-node-placement', `<${node.name}>`, parent.name);
e.node_invalid_placement(node, `<${node.name}>`, parent.name);
}
}
}
@ -583,13 +560,7 @@ const validation = {
!VoidElements.includes(node.name) &&
!SVGElements.includes(node.name)
) {
warn(
context.state.analysis.warnings,
node,
context.path,
'invalid-self-closing-tag',
node.name
);
w.element_invalid_self_closing_tag(node, node.name);
}
context.next({
@ -603,7 +574,7 @@ const validation = {
const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) {
if (arg.type === 'SpreadElement') {
error(arg, 'invalid-render-spread-argument');
e.render_tag_invalid_spread_argument(arg);
}
}
@ -613,7 +584,7 @@ const validation = {
callee.property.type === 'Identifier' &&
['bind', 'apply', 'call'].includes(callee.property.name)
) {
error(node, 'invalid-render-call');
e.render_tag_invalid_call_expression(node);
}
const is_inside_textarea = context.path.find((n) => {
@ -625,9 +596,8 @@ const validation = {
);
});
if (is_inside_textarea) {
error(
e.tag_invalid_placement(
node,
'invalid-tag-placement',
'inside <textarea> or <svelte:element this="textarea">',
'render'
);
@ -667,19 +637,19 @@ const validation = {
(node) => node.type !== 'SnippetBlock' && (node.type !== 'Text' || node.data.trim())
)
) {
error(node, 'conflicting-children-snippet');
e.snippet_conflict(node);
}
}
},
StyleDirective(node) {
if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) {
error(node, 'invalid-style-directive-modifier');
e.style_directive_invalid_modifier(node);
}
},
SvelteHead(node) {
const attribute = node.attributes[0];
if (attribute) {
error(attribute, 'illegal-svelte-head-attribute');
e.svelte_head_illegal_attribute(attribute);
}
},
SvelteElement(node, context) {
@ -692,7 +662,7 @@ const validation = {
SvelteFragment(node, context) {
const parent = context.path.at(-2);
if (parent?.type !== 'Component' && parent?.type !== 'SvelteComponent') {
error(node, 'invalid-svelte-fragment-placement');
e.svelte_fragment_invalid_placement(node);
}
for (const attribute of node.attributes) {
@ -701,7 +671,7 @@ const validation = {
validate_slot_attribute(context, attribute);
}
} else if (attribute.type !== 'LetDirective') {
error(attribute, 'invalid-svelte-fragment-attribute');
e.svelte_fragment_invalid_attribute(attribute);
}
}
},
@ -710,15 +680,15 @@ const validation = {
if (attribute.type === 'Attribute') {
if (attribute.name === 'name') {
if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-name', false);
e.slot_element_invalid_name(attribute);
}
const slot_name = attribute.value[0].data;
if (slot_name === 'default') {
error(attribute, 'invalid-slot-name', true);
e.slot_element_invalid_name_default(attribute);
}
}
} else if (attribute.type !== 'SpreadAttribute' && attribute.type !== 'LetDirective') {
error(attribute, 'invalid-slot-element-attribute');
e.slot_element_invalid_attribute(attribute);
}
}
},
@ -729,19 +699,19 @@ const validation = {
if (!node.parent) return;
if (context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', 'Text node', context.state.parent_element);
e.node_invalid_placement(node, 'Text node', context.state.parent_element);
}
}
},
TitleElement(node) {
const attribute = node.attributes[0];
if (attribute) {
error(attribute, 'illegal-title-attribute');
e.title_illegal_attribute(attribute);
}
const child = node.fragment.nodes.find((n) => n.type !== 'Text' && n.type !== 'ExpressionTag');
if (child) {
error(child, 'invalid-title-content');
e.title_invalid_content(child);
}
},
UpdateExpression(node, context) {
@ -751,7 +721,7 @@ const validation = {
if (!node.parent) return;
if (context.state.parent_element) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', '{expression}', context.state.parent_element);
e.node_invalid_placement(node, '{expression}', context.state.parent_element);
}
}
}
@ -772,7 +742,7 @@ export const validation_legacy = merge(validation, a11y_validators, {
}
if (state.scope.get(callee.name)?.kind !== 'store_sub') {
error(node.init, 'invalid-rune-usage', callee.name);
e.rune_invalid_usage(node.init, callee.name);
}
},
AssignmentExpression(node, { state, path }) {
@ -786,7 +756,7 @@ export const validation_legacy = merge(validation, a11y_validators, {
(state.ast_type !== 'instance' ||
/** @type {import('#compiler').SvelteNode} */ (path.at(-1)).type !== 'Program')
) {
warn(state.analysis.warnings, node, path, 'no-reactive-declaration');
w.reactive_declaration_invalid_placement(node);
}
},
UpdateExpression(node, { state }) {
@ -805,11 +775,11 @@ function validate_export(node, scope, name) {
if (!binding) return;
if (binding.kind === 'derived') {
error(node, 'invalid-derived-export');
e.derived_invalid_export(node);
}
if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
error(node, 'invalid-state-export');
e.state_invalid_export(node);
}
}
@ -827,7 +797,7 @@ function validate_call_expression(node, scope, path) {
if (rune === '$props') {
if (parent.type === 'VariableDeclarator') return;
error(node, 'invalid-props-location');
e.props_invalid_placement(node);
}
if (rune === '$bindable') {
@ -840,7 +810,7 @@ function validate_call_expression(node, scope, path) {
return;
}
}
error(node, 'invalid-bindable-location');
e.bindable_invalid_location(node);
}
if (
@ -851,46 +821,46 @@ function validate_call_expression(node, scope, path) {
) {
if (parent.type === 'VariableDeclarator') return;
if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return;
error(node, 'invalid-state-location', rune);
e.state_invalid_placement(node, rune);
}
if (rune === '$effect' || rune === '$effect.pre') {
if (parent.type !== 'ExpressionStatement') {
error(node, 'invalid-effect-location');
e.effect_invalid_placement(node);
}
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
}
if (rune === '$effect.active') {
if (node.arguments.length !== 0) {
error(node, 'invalid-rune-args-length', rune, [0]);
e.rune_invalid_arguments(node, rune);
}
}
if (rune === '$effect.root') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
}
if (rune === '$inspect') {
if (node.arguments.length < 1) {
error(node, 'invalid-rune-args-length', rune, [1, 'more']);
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
}
}
if (rune === '$inspect().with') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
}
if (rune === '$state.snapshot') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
}
}
@ -906,7 +876,7 @@ function ensure_no_module_import_conflict(node, state) {
state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) {
error(node.id, 'illegal-variable-declaration');
e.declaration_duplicate_module_import(node.id);
}
}
}
@ -932,7 +902,7 @@ export const validation_runes_js = {
},
CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$host') {
error(node, 'invalid-host-location');
e.host_invalid_placement(node);
}
validate_call_expression(node, state.scope, path);
},
@ -945,13 +915,13 @@ export const validation_runes_js = {
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]);
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
} else if (rune === '$props') {
error(node, 'invalid-props-location');
e.props_invalid_placement(node);
} else if (rune === '$bindable') {
error(node, 'invalid-bindable-location');
e.bindable_invalid_location(node);
}
},
AssignmentExpression(node, { state }) {
@ -989,12 +959,12 @@ export const validation_runes_js = {
const allowed_depth = context.state.ast_type === 'module' ? 0 : 1;
if (context.state.scope.function_depth > allowed_depth) {
warn(context.state.analysis.warnings, node, context.path, 'avoid-nested-class');
w.perf_avoid_nested_class(node);
}
},
NewExpression(node, context) {
if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) {
warn(context.state.analysis.warnings, node, context.path, 'avoid-inline-class');
w.perf_avoid_inline_class(node);
}
}
};
@ -1021,16 +991,24 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (binding?.declaration_kind === 'const' && binding.kind !== 'each') {
error(
node,
'invalid-const-assignment',
is_binding,
// This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
(binding.kind !== 'normal' || !binding.initial)
);
// e.invalid_const_assignment(
// node,
// is_binding,
// // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
// binding.kind !== 'state' &&
// binding.kind !== 'frozen_state' &&
// (binding.kind !== 'normal' || !binding.initial)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
const thing = 'constant';
if (is_binding) {
e.constant_binding(node, thing);
} else {
e.constant_assignment(node, thing);
}
}
}
}
@ -1048,16 +1026,16 @@ function validate_assignment(node, argument, state) {
if (state.analysis.runes) {
if (binding?.kind === 'derived') {
error(node, 'invalid-derived-assignment');
e.constant_assignment(node, 'derived state');
}
if (binding?.kind === 'each') {
error(node, 'invalid-each-assignment');
e.each_item_invalid_assignment(node);
}
}
if (binding?.kind === 'snippet') {
error(node, 'invalid-snippet-assignment');
e.snippet_parameter_assignment(node);
}
}
@ -1073,15 +1051,46 @@ function validate_assignment(node, argument, state) {
if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') {
if (state.private_derived_state.includes(property.name)) {
error(node, 'invalid-derived-assignment');
e.constant_assignment(node, 'derived state');
}
}
}
export const validation_runes = merge(validation, a11y_validators, {
Identifier(node, { path, state }) {
let i = path.length;
let parent = /** @type {import('estree').Expression} */ (path[--i]);
if (
Runes.includes(/** @type {Runes[number]} */ (node.name)) &&
is_reference(node, parent) &&
state.scope.get(node.name) === null &&
state.scope.get(node.name.slice(1)) === null
) {
/** @type {import('estree').Expression} */
let current = node;
let name = node.name;
while (parent.type === 'MemberExpression') {
if (parent.computed) e.rune_invalid_computed_property(parent);
name += `.${/** @type {import('estree').Identifier} */ (parent.property).name}`;
current = parent;
parent = /** @type {import('estree').Expression} */ (path[--i]);
if (!Runes.includes(/** @type {Runes[number]} */ (name))) {
e.rune_invalid_name(parent, name);
}
}
if (parent.type !== 'CallExpression') {
e.rune_missing_parentheses(current);
}
}
},
LabeledStatement(node, { path }) {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
error(node, 'invalid-legacy-reactive-statement');
e.legacy_reactive_statement_invalid(node);
},
ExportNamedDeclaration(node, { state, next }) {
if (state.ast_type === 'module') {
@ -1099,7 +1108,7 @@ export const validation_runes = merge(validation, a11y_validators, {
if (node.declaration?.type !== 'VariableDeclaration') return;
if (node.declaration.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export');
e.legacy_export_invalid(node);
}
},
ExportSpecifier(node, { state }) {
@ -1110,12 +1119,12 @@ export const validation_runes = merge(validation, a11y_validators, {
CallExpression(node, { state, path }) {
const rune = get_rune(node, state.scope);
if (rune === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
e.rune_invalid_arguments_length(node, '$bindable', 'zero or one arguments');
} else if (rune === '$host') {
if (node.arguments.length > 0) {
error(node, 'invalid-rune-args-length', '$host', [0]);
e.rune_invalid_arguments(node, '$host');
} else if (state.ast_type === 'module' || !state.analysis.custom_element) {
error(node, 'invalid-host-location');
e.host_invalid_placement(node);
}
}
@ -1127,60 +1136,55 @@ export const validation_runes = merge(validation, a11y_validators, {
context.type === 'Identifier' &&
(context.name === '$state' || context.name === '$derived')
) {
error(node, 'invalid-state-location', context.name);
e.state_invalid_placement(node, context.name);
}
next({ ...state });
},
VariableDeclarator(node, { state, path }) {
VariableDeclarator(node, { state }) {
ensure_no_module_import_conflict(node, state);
const init = node.init;
const rune = get_rune(init, state.scope);
if (rune === null) {
if (init?.type === 'Identifier' && init.name === '$props' && !state.scope.get('props')) {
warn(state.analysis.warnings, node, path, 'invalid-props-declaration');
}
return;
}
if (rune === null) return;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
// TODO some of this is duplicated with above, seems off
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]);
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
} else if (rune === '$props') {
if (state.has_props_rune) {
error(node, 'duplicate-props-rune');
e.props_duplicate(node);
}
state.has_props_rune = true;
if (args.length > 0) {
error(node, 'invalid-rune-args-length', rune, [0]);
e.rune_invalid_arguments(node, rune);
}
if (node.id.type !== 'ObjectPattern') {
error(node, 'invalid-props-id');
e.props_invalid_identifier(node);
}
if (state.scope !== state.analysis.instance.scope) {
error(node, 'invalid-props-location');
e.props_invalid_placement(node);
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
error(property, 'invalid-props-pattern');
e.props_invalid_pattern(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
error(property, 'invalid-props-pattern');
e.props_invalid_pattern(property);
}
}
}
@ -1192,29 +1196,20 @@ export const validation_runes = merge(validation, a11y_validators, {
arg.type === 'CallExpression' &&
(arg.callee.type === 'ArrowFunctionExpression' || arg.callee.type === 'FunctionExpression')
) {
warn(state.analysis.warnings, node, path, 'derived-iife');
w.derived_iife(node);
}
}
},
AssignmentPattern(node, { state, path }) {
if (
node.right.type === 'Identifier' &&
node.right.name === '$bindable' &&
!state.scope.get('bindable')
) {
warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration');
}
},
SlotElement(node, { state, path }) {
SlotElement(node, { state }) {
if (!state.analysis.custom_element) {
warn(state.analysis.warnings, node, path, 'deprecated-slot-element');
w.slot_element_deprecated(node);
}
},
OnDirective(node, { state, path }) {
OnDirective(node, { path }) {
const parent_type = path.at(-1)?.type;
// Don't warn on component events; these might not be under the author's control so the warning would be unactionable
if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
warn(state.analysis.warnings, node, path, 'deprecated-event-handler', node.name);
w.event_directive_deprecated(node, node.name);
}
},
// TODO this is a code smell. need to refactor this stuff

@ -1,5 +1,4 @@
import { walk } from 'zimmerframe';
import { error } from '../../../errors.js';
import * as b from '../../../utils/builders.js';
import { set_scope } from '../../scope.js';
import { template_visitors } from './visitors/template.js';
@ -52,35 +51,41 @@ export function client_component(source, analysis, options) {
get before_init() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'before_init.push should not be called outside create_block');
a.push = () => {
throw new Error('before_init.push should not be called outside create_block');
};
return a;
},
get init() {
/** @type {any[]} */
const a = [];
a.push = () => error(null, 'INTERNAL', 'init.push should not be called outside create_block');
a.push = () => {
throw new Error('init.push should not be called outside create_block');
};
return a;
},
get update() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'update.push should not be called outside create_block');
a.push = () => {
throw new Error('update.push should not be called outside create_block');
};
return a;
},
get after_update() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'after_update.push should not be called outside create_block');
a.push = () => {
throw new Error('after_update.push should not be called outside create_block');
};
return a;
},
get template() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'template.push should not be called outside create_block');
a.push = () => {
throw new Error('template.push should not be called outside create_block');
};
return a;
},
legacy_reactive_statements: new Map(),
@ -209,7 +214,7 @@ export function client_component(source, analysis, options) {
for (const [node] of analysis.reactive_statements) {
const statement = [...state.legacy_reactive_statements].find(([n]) => n === node);
if (statement === undefined) {
error(node, 'INTERNAL', 'Could not find reactive statement');
throw new Error('Could not find reactive statement');
}
instance.body.push(statement[1]);
}
@ -227,18 +232,6 @@ export function client_component(source, analysis, options) {
group_binding_declarations.push(b.const(group.name, b.array([])));
}
// Bind static exports to props so that people can access them with bind:x
const static_bindings = analysis.exports.map(({ name, alias }) => {
return b.stmt(
b.call(
'$.bind_prop',
b.id('$$props'),
b.literal(alias ?? name),
serialize_get_binding(b.id(name), instance_state)
)
);
});
const component_returned_object = analysis.exports.map(({ name, alias }) => {
const expression = serialize_get_binding(b.id(name), instance_state);
@ -255,14 +248,24 @@ export function client_component(source, analysis, options) {
);
if (analysis.runes && options.dev) {
const bindable = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name));
const exports = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name));
/** @type {import('estree').Literal[]} */
const bindable = [];
for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name));
}
}
instance.body.unshift(
b.stmt(b.call('$.validate_prop_bindings', b.id('$$props'), b.array(bindable)))
b.stmt(
b.call(
'$.validate_prop_bindings',
b.id('$$props'),
b.array(bindable),
b.array(exports),
b.id(`${analysis.name}`)
)
)
);
}
@ -348,16 +351,30 @@ export function client_component(source, analysis, options) {
if (options.dev) push_args.push(b.id(analysis.name));
const component_block = b.block([
b.stmt(b.call('$.push', ...push_args)),
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
.../** @type {import('estree').Statement[]} */ (instance.body),
analysis.runes ? b.empty : b.stmt(b.call('$.init')),
.../** @type {import('estree').Statement[]} */ (template.body),
...static_bindings
analysis.runes || !analysis.needs_context ? b.empty : b.stmt(b.call('$.init')),
.../** @type {import('estree').Statement[]} */ (template.body)
]);
if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) {
component_block.body.push(
b.stmt(
b.call(
'$.bind_prop',
b.id('$$props'),
b.literal(alias ?? name),
serialize_get_binding(b.id(name), instance_state)
)
)
);
}
}
const append_styles =
analysis.inject_styles && analysis.css.ast
? () =>
@ -374,11 +391,22 @@ export function client_component(source, analysis, options) {
: () => {};
append_styles();
component_block.body.push(
component_returned_object.length > 0
? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop'))
);
const should_inject_context =
analysis.needs_context ||
analysis.reactive_statements.size > 0 ||
component_returned_object.length > 0 ||
options.dev;
if (should_inject_context) {
component_block.body.unshift(b.stmt(b.call('$.push', ...push_args)));
component_block.body.push(
component_returned_object.length > 0
? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop'))
);
}
if (analysis.uses_rest_props) {
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
@ -412,18 +440,25 @@ export function client_component(source, analysis, options) {
component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props'))));
}
const body = [
...state.hoisted,
...module.body,
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
];
let should_inject_props =
should_inject_context ||
analysis.needs_props ||
analysis.uses_props ||
analysis.uses_rest_props ||
analysis.uses_slots ||
analysis.slot_names.size > 0;
const body = [...state.hoisted, ...module.body];
const component = b.function_declaration(
b.id(analysis.name),
should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')],
component_block
);
if (options.hmr) {
body.push(
component,
b.if(
b.id('import.meta.hot'),
b.block([
@ -441,12 +476,14 @@ export function client_component(source, analysis, options) {
)
)
])
)
),
b.export_default(b.id(analysis.name))
);
} else {
body.push(b.export_default(component));
}
body.push(b.export_default(b.id(analysis.name)));
if (options.dev) {
if (options.filename) {
let filename = options.filename;

@ -5,7 +5,6 @@ import {
is_simple_expression,
object
} from '../../../utils/ast.js';
import { error } from '../../../errors.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
@ -185,7 +184,7 @@ export function serialize_set_binding(node, context, fallback, options) {
}
if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') {
error(node, 'INTERNAL', `Unexpected assignment type ${assignee.type}`);
throw new Error(`Unexpected assignment type ${assignee.type}`);
}
// Handle class private/public state assignment cases

@ -222,7 +222,8 @@ export const javascript_visitors_runes = {
if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
const seen = state.analysis.exports.map(({ name, alias }) => alias ?? name);
/** @type {string[]} */
const seen = [];
for (const property of declarator.id.properties) {
if (property.type === 'Property') {

@ -16,7 +16,6 @@ import {
import { DOMProperties, PassiveEvents, VoidElements } from '../../../constants.js';
import { is_custom_element_node, is_element_node } from '../../../nodes.js';
import * as b from '../../../../utils/builders.js';
import { error } from '../../../../errors.js';
import {
with_loc,
function_visitor,
@ -1245,6 +1244,8 @@ function serialize_event_handler(node, { state, visit }) {
return handler;
} else {
state.analysis.needs_props = true;
// Function + .call to preserve "this" context as much as possible
return b.function(
null,
@ -1792,10 +1793,10 @@ export const template_visitors = {
);
},
ClassDirective(node, { state, next }) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere');
throw new Error('Node should have been handled elsewhere');
},
StyleDirective(node, { state, next }) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere');
throw new Error('Node should have been handled elsewhere');
},
TransitionDirective(node, { state, visit }) {
let flags = node.modifiers.includes('global') ? TRANSITION_GLOBAL : 0;
@ -2791,7 +2792,7 @@ export const template_visitors = {
}
default:
error(node, 'INTERNAL', 'unknown binding ' + node.name);
throw new Error('unknown binding ' + node.name);
}
}

@ -17,7 +17,7 @@ export function transform_component(analysis, source, options) {
return {
js: /** @type {any} */ (null),
css: null,
warnings: transform_warnings(source, options.filename, analysis.warnings),
warnings: /** @type {any} */ (null), // set afterwards
metadata: {
runes: analysis.runes
},
@ -30,20 +30,6 @@ export function transform_component(analysis, source, options) {
? server_component(analysis, options)
: client_component(source, analysis, options);
const basename = (options.filename ?? 'Component').split(/[/\\]/).at(-1);
if (program.body.length > 0) {
program.body[0].leadingComments = [
{
type: 'Line',
value: ` ${basename} (Svelte v${VERSION})`
},
{
type: 'Line',
value: ' Note: compiler output will change before 5.0 is released!'
}
];
}
const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte');
const js = print(program, {
// include source content; makes it easier/more robust looking up the source map code
@ -60,7 +46,7 @@ export function transform_component(analysis, source, options) {
return {
js,
css,
warnings: transform_warnings(source, options.filename, analysis.warnings), // TODO apply preprocessor sourcemap
warnings: /** @type {any} */ (null), // set afterwards. TODO apply preprocessor sourcemap
metadata: {
runes: analysis.runes
},
@ -79,7 +65,7 @@ export function transform_module(analysis, source, options) {
return {
js: /** @type {any} */ (null),
css: null,
warnings: transform_warnings(source, analysis.name, analysis.warnings),
warnings: /** @type {any} */ (null), // set afterwards
metadata: {
runes: true
},
@ -105,45 +91,10 @@ export function transform_module(analysis, source, options) {
return {
js: print(program, {}),
css: null,
warnings: transform_warnings(source, analysis.name, analysis.warnings),
metadata: {
runes: true
},
warnings: /** @type {any} */ (null), // set afterwards
ast: /** @type {any} */ (null) // set afterwards
};
}
/**
* @param {string} source
* @param {string | undefined} name
* @param {import('../types').RawWarning[]} warnings
* @returns {import('#compiler').Warning[]}
*/
function transform_warnings(source, name, warnings) {
if (warnings.length === 0) return [];
const locate = getLocator(source, { offsetLine: 1 });
/** @type {import('#compiler').Warning[]} */
const result = [];
for (const warning of warnings) {
const start =
warning.position &&
/** @type {import('locate-character').Location} */ (locate(warning.position[0]));
const end =
warning.position &&
/** @type {import('locate-character').Location} */ (locate(warning.position[1]));
result.push({
start,
end,
filename: name,
message: warning.message,
code: warning.code
});
}
return result;
}

@ -22,7 +22,6 @@ import {
transform_inspect_rune
} from '../utils.js';
import { create_attribute, is_custom_element_node, is_element_node } from '../../nodes.js';
import { error } from '../../../errors.js';
import { binding_properties } from '../../bindings.js';
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
import {
@ -419,7 +418,7 @@ function serialize_set_binding(node, context, fallback) {
}
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
error(node, 'INTERNAL', `Unexpected assignment type ${node.left.type}`);
throw new Error(`Unexpected assignment type ${node.left.type}`);
}
let left = node.left;
@ -692,8 +691,7 @@ const javascript_visitors_runes = {
}
if (rune === '$props') {
// remove $bindable() from props declaration and handle rest props
let uses_rest_props = false;
// remove $bindable() from props declaration
const id = walk(declarator.id, null, {
AssignmentPattern(node) {
if (
@ -705,26 +703,9 @@ const javascript_visitors_runes = {
: b.id('undefined');
return b.assignment_pattern(node.left, right);
}
},
RestElement(node, { path }) {
if (path.at(-1) === declarator.id) {
uses_rest_props = true;
}
}
});
const exports = /** @type {import('../../types').ComponentAnalysis} */ (
state.analysis
).exports.map(({ name, alias }) => b.literal(alias ?? name));
declarations.push(
b.declarator(
id,
uses_rest_props && exports.length > 0
? b.call('$.rest_props', b.id('$$props'), b.array(exports))
: b.id('$$props')
)
);
declarations.push(b.declarator(id, b.id('$$props')));
continue;
}
@ -1329,10 +1310,10 @@ const template_visitors = {
state.template.push(block_close);
},
ClassDirective(node) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere');
throw new Error('Node should have been handled elsewhere');
},
StyleDirective(node) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere');
throw new Error('Node should have been handled elsewhere');
},
RegularElement(node, context) {
const metadata = {
@ -2124,14 +2105,17 @@ export function server_component(analysis, options) {
get init() {
/** @type {any[]} */
const a = [];
a.push = () => error(null, 'INTERNAL', 'init.push should not be called outside create_block');
a.push = () => {
throw new Error('init.push should not be called outside create_block');
};
return a;
},
get template() {
/** @type {any[]} */
const a = [];
a.push = () =>
error(null, 'INTERNAL', 'template.push should not be called outside create_block');
a.push = () => {
throw new Error('template.push should not be called outside create_block');
};
return a;
},
metadata: {
@ -2199,7 +2183,7 @@ export function server_component(analysis, options) {
for (const [node] of analysis.reactive_statements) {
const statement = [...state.legacy_reactive_statements].find(([n]) => n === node);
if (statement === undefined) {
error(node, 'INTERNAL', 'Could not find reactive statement');
throw new Error('Could not find reactive statement');
}
if (

@ -2,7 +2,7 @@ import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { is_element_node } from './nodes.js';
import * as b from '../utils/builders.js';
import { error } from '../errors.js';
import * as e from '../errors.js';
import { extract_identifiers, extract_identifiers_from_destructuring } from '../utils/ast.js';
import { JsKeywords, Runes } from './constants.js';
@ -70,7 +70,7 @@ export class Scope {
*/
declare(node, kind, declaration_kind, initial = null) {
if (node.name === '$') {
error(node, 'invalid-dollar-binding');
e.dollar_binding_invalid(node);
}
if (
@ -80,7 +80,7 @@ export class Scope {
declaration_kind !== 'rest_param' &&
this.function_depth <= 1
) {
error(node, 'invalid-dollar-prefix');
e.dollar_prefix_invalid(node);
}
if (this.parent) {
@ -95,7 +95,7 @@ export class Scope {
if (this.declarations.has(node.name)) {
// This also errors on var/function types, but that's arguably a good thing
error(node, 'duplicate-declaration', node.name);
e.declaration_duplicate(node, node.name);
}
/** @type {import('#compiler').Binding} */
@ -166,7 +166,7 @@ export class Scope {
get_bindings(node) {
const bindings = this.declarators.get(node);
if (!bindings) {
error(node, 'INTERNAL', 'No binding found for declarator');
throw new Error('No binding found for declarator');
}
return bindings;
}
@ -704,7 +704,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
} else {
extract_identifiers(node).forEach((identifier) => {
const binding = scope.get(identifier.name);
if (binding) {
if (binding && identifier !== binding.node) {
binding.mutated = true;
binding.reassigned = true;
}
@ -767,7 +767,6 @@ export function get_rune(node, scope) {
joined = n.name + joined;
if (joined === '$derived.call') error(node, 'invalid-derived-call');
if (!Runes.includes(/** @type {any} */ (joined))) return null;
const binding = scope.get(n.name);

@ -2,11 +2,13 @@ import type {
Binding,
Css,
Fragment,
OnDirective,
RegularElement,
SlotElement,
SvelteElement,
SvelteNode,
SvelteOptions
SvelteOptions,
Warning
} from '#compiler';
import type { Identifier, LabeledStatement, Program } from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
@ -28,19 +30,12 @@ export interface ReactiveStatement {
dependencies: Binding[];
}
export interface RawWarning {
code: string;
message: string;
position: [number, number] | undefined;
}
/**
* Analysis common to modules and components
*/
export interface Analysis {
module: Js;
name: string; // TODO should this be filename? it's used in `compileModule` as well as `compile`
warnings: RawWarning[];
runes: boolean;
immutable: boolean;
@ -63,6 +58,12 @@ export interface ComponentAnalysis extends Analysis {
uses_slots: boolean;
uses_component_bindings: boolean;
uses_render_tags: boolean;
needs_context: boolean;
needs_props: boolean;
/** Set to the first event directive (on:x) found on a DOM element in the code */
event_directive_node: OnDirective | null;
/** true if uses event attributes (onclick) on a DOM element */
uses_event_attributes: boolean;
custom_element: boolean | SvelteOptions['customElement'];
/** If `true`, should append styles through JavaScript */
inject_styles: boolean;

@ -94,6 +94,7 @@ export interface CompileOptions extends ModuleCompileOptions {
* If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
*
* @default false
* @deprecated This will have no effect in runes mode
*/
accessors?: boolean;
/**
@ -107,6 +108,7 @@ export interface CompileOptions extends ModuleCompileOptions {
* This allows it to be less conservative about checking whether values have changed.
*
* @default false
* @deprecated This will have no effect in runes mode
*/
immutable?: boolean;
/**

@ -198,6 +198,14 @@ export interface LegacyWindow extends BaseElement {
type: 'Window';
}
export interface LegacyComment extends BaseNode {
type: 'Comment';
/** the contents of the comment */
data: string;
/** any svelte-ignore directives — <!-- svelte-ignore a b c --> would result in ['a', 'b', 'c'] */
ignores: string[];
}
type LegacyDirective =
| LegacyAnimation
| LegacyBinding
@ -213,6 +221,7 @@ export type LegacyAttributeLike = LegacyAttribute | LegacySpread | LegacyDirecti
export type LegacyElementLike =
| LegacyBody
| LegacyCatchBlock
| LegacyComment
| LegacyDocument
| LegacyElement
| LegacyHead

@ -131,8 +131,6 @@ export interface Comment extends BaseNode {
type: 'Comment';
/** the contents of the comment */
data: string;
/** any svelte-ignore directives — <!-- svelte-ignore a b c --> would result in ['a', 'b', 'c'] */
ignores: string[];
}
/** A `{@const ...}` tag */

@ -1,12 +1,10 @@
import { error } from '../errors.js';
/**
* @template T
* @param {T} value
* @returns {asserts value is NonNullable<T>}
*/
export function ok(value) {
if (!value) error(null, 'INTERNAL', 'Assertion failed');
if (!value) throw new Error('Assertion failed');
}
/**
@ -16,5 +14,5 @@ export function ok(value) {
* @returns {asserts actual is T}
*/
export function equal(actual, expected) {
if (actual !== expected) error(null, 'INTERNAL', 'Assertion failed');
if (actual !== expected) throw new Error('Assertion failed');
}

@ -15,41 +15,3 @@ export function extract_svelte_ignore(text) {
.filter(Boolean)
: [];
}
/**
* @template {{ leadingComments?: Array<{ value: string }> }} Node
* @param {Node} node
* @returns {string[]}
*/
export function extract_svelte_ignore_from_comments(node) {
return (node.leadingComments || []).flatMap(
/** @param {any} comment */ (comment) => extract_svelte_ignore(comment.value)
);
}
/**
* @param {import('#compiler').TemplateNode} node
* @param {import('#compiler').TemplateNode[]} template_nodes
* @returns {string[]}
*/
export function extract_ignores_above_position(node, template_nodes) {
const previous_node_idx = template_nodes.indexOf(node) - 1;
if (previous_node_idx < 0) {
return [];
}
const ignores = [];
for (let i = previous_node_idx; i >= 0; i--) {
const node = template_nodes[i];
if (node.type !== 'Comment' && node.type !== 'Text') {
return ignores;
}
if (node.type === 'Comment') {
if (node.ignores.length) {
ignores.push(...node.ignores);
}
}
}
return ignores;
}

@ -0,0 +1,9 @@
/**
* @param {string[]} strings
* @param {string} conjunction
*/
export function list(strings, conjunction = 'or') {
if (strings.length === 1) return strings[0];
if (strings.length === 2) return `${strings[0]} ${conjunction} ${strings[1]}`;
return `${strings.slice(0, -1).join(', ')} ${conjunction} ${strings[strings.length - 1]}`;
}

@ -1,4 +1,5 @@
import { error } from './errors.js';
import * as e from './errors.js';
import * as w from './warnings.js';
/**
* @template [Input=any]
@ -13,9 +14,7 @@ const common = {
generate: validator('client', (input, keypath) => {
if (input === 'dom' || input === 'ssr') {
warn(
'`generate: "dom"` and `generate: "ssr"` options have been renamed to "client" and "server" respectively'
);
warn_once(w.options_renamed_ssr_dom);
return input === 'dom' ? 'client' : 'server';
}
@ -40,7 +39,7 @@ export const validate_component_options =
object({
...common,
accessors: boolean(false),
accessors: deprecate(w.options_deprecated_accessors, boolean(false)),
css: validator('external', (input) => {
if (input === true || input === false) {
@ -72,16 +71,13 @@ export const validate_component_options =
discloseVersion: boolean(true),
immutable: deprecate(
'The immutable option has been deprecated. It has no effect in runes mode.',
boolean(false)
),
immutable: deprecate(w.options_deprecated_immutable, boolean(false)),
legacy: object({
componentApi: boolean(false)
}),
loopGuardTimeout: warn_removed('The loopGuardTimeout option has been removed.'),
loopGuardTimeout: warn_removed(w.options_removed_loop_guard_timeout),
name: string(undefined),
@ -103,12 +99,8 @@ export const validate_component_options =
return input;
}),
enableSourcemap: warn_removed(
'The enableSourcemap option has been removed. Source maps are always generated now, and tooling can choose to ignore them.'
),
hydratable: warn_removed(
'The hydratable option has been removed. Svelte components are always hydratable now.'
),
enableSourcemap: warn_removed(w.options_removed_enable_sourcemap),
hydratable: warn_removed(w.options_removed_hydratable),
format: removed(
'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' +
'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)'
@ -141,7 +133,7 @@ export const validate_component_options =
function removed(msg) {
return (input) => {
if (input !== undefined) {
error(null, 'removed-compiler-option', msg);
e.options_removed(null, msg);
}
return /** @type {any} */ (undefined);
};
@ -149,34 +141,33 @@ function removed(msg) {
const warned = new Set();
/** @param {string} message */
function warn(message) {
if (!warned.has(message)) {
warned.add(message);
// eslint-disable-next-line no-console
console.warn(message);
/** @param {(node: null) => void} fn */
function warn_once(fn) {
if (!warned.has(fn)) {
warned.add(fn);
fn(null);
}
}
/**
* @param {string} message
* @param {(node: null) => void} fn
* @returns {Validator}
*/
function warn_removed(message) {
function warn_removed(fn) {
return (input) => {
if (input !== undefined) warn(message);
if (input !== undefined) warn_once(fn);
return /** @type {any} */ (undefined);
};
}
/**
* @param {string} message
* @param {(node: null) => void} fn
* @param {Validator} validator
* @returns {Validator}
*/
function deprecate(message, validator) {
function deprecate(fn, validator) {
return (input, keypath) => {
if (input !== undefined) warn(message);
if (input !== undefined) warn_once(fn);
return validator(input, keypath);
};
}
@ -200,11 +191,7 @@ function object(children, allow_unknown = false) {
if (allow_unknown) {
output[key] = input[key];
} else {
error(
null,
'invalid-compiler-option',
`Unexpected option ${keypath ? `${keypath}.${key}` : key}`
);
e.options_unrecognised(null, `${keypath ? `${keypath}.${key}` : key}`);
}
}
}
@ -307,5 +294,5 @@ function fun(fallback) {
/** @param {string} msg */
function throw_error(msg) {
error(null, 'invalid-compiler-option', msg);
e.options_invalid_value(null, msg);
}

@ -1,336 +1,642 @@
import {
extract_ignores_above_position,
extract_svelte_ignore_from_comments
} from './utils/extract_svelte_ignore.js';
/** @typedef {Record<string, (...args: any[]) => string>} Warnings */
/** @satisfies {Warnings} */
const css = {
/** @param {string} name */
'css-unused-selector': (name) => `Unused CSS selector "${name}"`
};
/** @satisfies {Warnings} */
const attributes = {
'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided',
/** @param {string} name */
'global-event-reference': (name) =>
`You are referencing globalThis.${name}. Did you forget to declare a variable with that name?`,
'illegal-attribute-character': () =>
"Attributes should not contain ':' characters to prevent ambiguity with Svelte directives",
/**
* @param {string} wrong
* @param {string} right
*/
'invalid-html-attribute': (wrong, right) =>
`'${wrong}' is not a valid HTML attribute. Did you mean '${right}'?`
};
/** @satisfies {Warnings} */
const runes = {
/** @param {string} name */
'store-with-rune-name': (name) =>
`It looks like you're using the $${name} rune, but there is a local binding called ${name}. ` +
`Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`,
/** @param {string} name */
'non-state-reference': (name) =>
`${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.`,
'derived-iife': () =>
`Use \`$derived.by(() => {...})\` instead of \`$derived((() => {...})());\``,
'invalid-props-declaration': () =>
`Component properties are declared using $props() in runes mode. Did you forget to call the function?`,
'invalid-bindable-declaration': () =>
`Bindable component properties are declared using $bindable() in runes mode. Did you forget to call the function?`
};
/** @satisfies {Warnings} */
const a11y = {
/** @param {string} name */
'a11y-aria-attributes': (name) => `A11y: <${name}> should not have aria-* attributes`,
/**
* @param {string} attribute
* @param {string | null} [suggestion]
*/
'a11y-unknown-aria-attribute': (attribute, suggestion) =>
`A11y: Unknown aria attribute 'aria-${attribute}'` +
(suggestion ? ` (did you mean '${suggestion}'?)` : ''),
/** @param {string} name */
'a11y-hidden': (name) => `A11y: <${name}> element should not be hidden`,
/**
* @param {import('aria-query').ARIAPropertyDefinition} schema
* @param {string} attribute
*/
'a11y-incorrect-aria-attribute-type': (schema, attribute) => {
let message;
switch (schema.type) {
case 'boolean':
message = `The value of '${attribute}' must be exactly one of true or false`;
break;
case 'id':
message = `The value of '${attribute}' must be a string that represents a DOM element ID`;
break;
case 'idlist':
message = `The value of '${attribute}' must be a space-separated list of strings that represent DOM element IDs`;
break;
case 'tristate':
message = `The value of '${attribute}' must be exactly one of true, false, or mixed`;
break;
case 'token':
message = `The value of '${attribute}' must be exactly one of ${(schema.values || []).join(
', '
)}`;
break;
case 'tokenlist':
message = `The value of '${attribute}' must be a space-separated list of one or more of ${(
schema.values || []
).join(', ')}`;
break;
default:
message = `The value of '${attribute}' must be of type ${schema.type}`;
}
return `A11y: ${message}`;
},
'a11y-aria-activedescendant-has-tabindex': () =>
'A11y: Elements with attribute aria-activedescendant should have tabindex value',
/** @param {string} name */
'a11y-misplaced-role': (name) => `A11y: <${name}> should not have role attribute`,
/** @param {string | boolean} role */
'a11y-no-abstract-role': (role) => `A11y: Abstract role '${role}' is forbidden`,
/**
* @param {string | boolean} role
* @param {string | null} [suggestion]
*/
'a11y-unknown-role': (role, suggestion) =>
`A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : ''),
/** @param {string | boolean} role */
'a11y-no-redundant-roles': (role) => `A11y: Redundant role '${role}'`,
/**
* @param {string} role
* @param {string[]} props
*/
'a11y-role-has-required-aria-props': (role, props) =>
`A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props
.map((name) => `"${name}"`)
.join(', ')}`,
/** @param {string} role */
'a11y-interactive-supports-focus': (role) =>
`A11y: Elements with the '${role}' interactive role must have a tabindex value.`,
/**
* @param {string | boolean} role
* @param {string} element
*/
'a11y-no-interactive-element-to-noninteractive-role': (role, element) =>
`A11y: <${element}> cannot have role '${role}'`,
/**
* @param {string | boolean} role
* @param {string} element
*/
'a11y-no-noninteractive-element-to-interactive-role': (role, element) =>
`A11y: Non-interactive element <${element}> cannot have interactive role '${role}'`,
'a11y-accesskey': () => 'A11y: Avoid using accesskey',
'a11y-autofocus': () => 'A11y: Avoid using autofocus',
'a11y-misplaced-scope': () => 'A11y: The scope attribute should only be used with <th> elements',
'a11y-positive-tabindex': () => 'A11y: avoid tabindex values above zero',
'a11y-click-events-have-key-events': () =>
'A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type="button"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.',
'a11y-no-noninteractive-tabindex': () =>
'A11y: noninteractive element cannot have nonnegative tabIndex value',
/**
* @param {string} attribute
* @param {string} role
* @param {boolean} is_implicit
* @param {string} name
*/
'a11y-role-supports-aria-props': (attribute, role, is_implicit, name) => {
let message = `The attribute '${attribute}' is not supported by the role '${role}'.`;
if (is_implicit) {
message += ` This role is implicit on the element <${name}>.`;
}
return `A11y: ${message}`;
},
/** @param {string} element */
'a11y-no-noninteractive-element-interactions': (element) =>
`A11y: Non-interactive element <${element}> should not be assigned mouse or keyboard event listeners.`,
/**
* @param {string} element
* @param {string[]} handlers
*/
'a11y-no-static-element-interactions': (element, handlers) =>
`A11y: <${element}> with ${handlers.join(', ')} ${
handlers.length === 1 ? 'handler' : 'handlers'
} must have an ARIA role`,
/**
* @param {string} href_attribute
* @param {string} href_value
*/
'a11y-invalid-attribute': (href_attribute, href_value) =>
`A11y: '${href_value}' is not a valid ${href_attribute} attribute`,
/**
* @param {string} name
* @param {string} article
* @param {string} sequence
*/
'a11y-missing-attribute': (name, article, sequence) =>
`A11y: <${name}> element should have ${article} ${sequence} attribute`,
/**
* @param {null | true | string} type
* @param {null | true | string} value
*/
'a11y-autocomplete-valid': (type, value) =>
`A11y: The value '${value}' is not supported by the attribute 'autocomplete' on element <input type="${
type || '...'
}">`,
'a11y-img-redundant-alt': () =>
'A11y: Screenreaders already announce <img> elements as an image.',
'a11y-label-has-associated-control': () =>
'A11y: A form label must be associated with a control.',
'a11y-media-has-caption': () => 'A11y: <video> elements must have a <track kind="captions">',
/** @param {string} name */
'a11y-distracting-elements': (name) => `A11y: Avoid <${name}> elements`,
/** @param {boolean} immediate */
'a11y-structure': (immediate) =>
immediate
? 'A11y: <figcaption> must be an immediate child of <figure>'
: 'A11y: <figcaption> must be first or last child of <figure>',
/**
* @param {string} event
* @param {string} accompanied_by
*/
'a11y-mouse-events-have-key-events': (event, accompanied_by) =>
`A11y: '${event}' event must be accompanied by '${accompanied_by}' event`,
/** @param {string} name */
'a11y-missing-content': (name) => `A11y: <${name}> element should have child content`
};
/** @satisfies {Warnings} */
const state = {
'static-state-reference': () =>
`State referenced in its own scope will never update. Did you mean to reference it inside a closure?`,
/** @param {string} name */
'invalid-rest-eachblock-binding': (name) =>
`The rest operator (...) will create a new object and binding '${name}' with the original object will not work`
};
/** @satisfies {Warnings} */
const performance = {
'avoid-inline-class': () =>
`Avoid 'new class' — instead, declare the class at the top level scope`,
'avoid-nested-class': () => `Avoid declaring classes below the top level scope`
};
/** @satisfies {Warnings} */
const components = {
/** @param {string} name */
'component-name-lowercase': (name) =>
`<${name}> will be treated as an HTML element unless it begins with a capital letter`
};
const legacy = {
'no-reactive-declaration': () =>
`Reactive declarations only exist at the top level of the instance script`,
'module-script-reactive-declaration': () =>
'All dependencies of the reactive declaration are declared in a module script and will not be reactive',
/** @param {string} name */
'unused-export-let': (name) =>
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``,
'deprecated-slot-element': () =>
`Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.`,
/** @param {string} name */
'deprecated-event-handler': (name) =>
`Using on:${name} to listen to the ${name} event is is deprecated. Use the event attribute on${name} instead.`
};
const block = {
'empty-block': () => 'Empty block'
};
const options = {
'missing-custom-element-compile-option': () =>
"The 'customElement' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?"
};
const misc = {
/** @param {string} name */
'invalid-self-closing-tag': (name) =>
`Self-closing HTML tags for non-void elements are ambiguous — use <${name} ...></${name}> rather than <${name} ... />`
};
/** @satisfies {Warnings} */
const warnings = {
...css,
...attributes,
...runes,
...a11y,
...performance,
...state,
...components,
...legacy,
...block,
...options,
...misc
};
/** @typedef {typeof warnings} AllWarnings */
/**
* @template {keyof AllWarnings} T
* @param {import('./phases/types').RawWarning[]} array the array to push the warning to, if not ignored
* @param {{ start?: number, end?: number, type?: string, parent?: import('#compiler').SvelteNode | null, leadingComments?: import('estree').Comment[] } | null} node the node related to the warning
* @param {import('#compiler').SvelteNode[]} path the path to the node, so that we can traverse upwards to find svelte-ignore comments
* @param {T} code the warning code
* @param {Parameters<AllWarnings[T]>} args the arguments to pass to the warning function
* @returns {void}
*/
export function warn(array, node, path, code, ...args) {
const fn = warnings[code];
// Traverse the AST upwards to find any svelte-ignore comments.
// This assumes that people don't have their code littered with warnings,
// at which point this might become inefficient.
/** @type {string[]} */
const ignores = [];
if (node) {
// comments inside JavaScript (estree)
if ('leadingComments' in node) {
ignores.push(...extract_svelte_ignore_from_comments(node));
}
}
for (let i = path.length - 1; i >= 0; i--) {
const current = path[i];
// comments inside JavaScript (estree)
if ('leadingComments' in current) {
ignores.push(...extract_svelte_ignore_from_comments(current));
}
// Svelte nodes
if (current.type === 'Fragment') {
ignores.push(
...extract_ignores_above_position(
/** @type {import('#compiler').TemplateNode} */ (path[i + 1] ?? node),
current.nodes
)
);
}
// Style nodes
if (current.type === 'StyleSheet' && current.content.comment) {
ignores.push(...current.content.comment.ignores);
}
}
if (ignores.includes(code)) return;
const start = node?.start;
const end = node?.end;
array.push({
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { getLocator } from 'locate-character';
/** @typedef {{ start?: number, end?: number }} NodeLike */
/** @type {import('#compiler').Warning[]} */
let warnings = [];
/** @type {string | undefined} */
let filename;
let locator = getLocator('', { offsetLine: 1 });
/**
* @param {{
* source: string;
* filename: string | undefined;
* }} options
* @returns {import('#compiler').Warning[]}
*/
export function reset_warnings(options) {
filename = options.filename;
locator = getLocator(options.source, { offsetLine: 1 });
return warnings = [];
}
/**
* @param {null | NodeLike} node
* @param {string} code
* @param {string} message
*/
function w(node, code, message) {
// @ts-expect-error
if (node?.ignores?.has(code)) return;
warnings.push({
code,
// @ts-expect-error
message: fn(...args),
position: start !== undefined && end !== undefined ? [start, end] : undefined
message,
filename,
start: node?.start !== undefined ? locator(node.start) : undefined,
end: node?.end !== undefined ? locator(node.end) : undefined
});
}
/**
* Avoid using accesskey
* @param {null | NodeLike} node
*/
export function a11y_accesskey(node) {
w(node, "a11y_accesskey", "Avoid using accesskey");
}
/**
* An element with an aria-activedescendant attribute should have a tabindex value
* @param {null | NodeLike} node
*/
export function a11y_aria_activedescendant_has_tabindex(node) {
w(node, "a11y_aria_activedescendant_has_tabindex", "An element with an aria-activedescendant attribute should have a tabindex value");
}
/**
* `<%name%>` should not have aria-* attributes
* @param {null | NodeLike} node
* @param {string} name
*/
export function a11y_aria_attributes(node, name) {
w(node, "a11y_aria_attributes", `\`<${name}>\` should not have aria-* attributes`);
}
/**
* '%value%' is an invalid value for 'autocomplete' on `<input type="%type%">`
* @param {null | NodeLike} node
* @param {string} value
* @param {string} type
*/
export function a11y_autocomplete_valid(node, value, type) {
w(node, "a11y_autocomplete_valid", `'${value}' is an invalid value for 'autocomplete' on \`<input type="${type}">\``);
}
/**
* Avoid using autofocus
* @param {null | NodeLike} node
*/
export function a11y_autofocus(node) {
w(node, "a11y_autofocus", "Avoid using autofocus");
}
/**
* Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.
* @param {null | NodeLike} node
*/
export function a11y_click_events_have_key_events(node) {
w(node, "a11y_click_events_have_key_events", "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.");
}
/**
* Avoid `<%name%>` elements
* @param {null | NodeLike} node
* @param {string} name
*/
export function a11y_distracting_elements(node, name) {
w(node, "a11y_distracting_elements", `Avoid \`<${name}>\` elements`);
}
/**
* `<figcaption>` must be first or last child of `<figure>`
* @param {null | NodeLike} node
*/
export function a11y_figcaption_index(node) {
w(node, "a11y_figcaption_index", "`<figcaption>` must be first or last child of `<figure>`");
}
/**
* `<figcaption>` must be an immediate child of `<figure>`
* @param {null | NodeLike} node
*/
export function a11y_figcaption_parent(node) {
w(node, "a11y_figcaption_parent", "`<figcaption>` must be an immediate child of `<figure>`");
}
/**
* `<%name%>` element should not be hidden
* @param {null | NodeLike} node
* @param {string} name
*/
export function a11y_hidden(node, name) {
w(node, "a11y_hidden", `\`<${name}>\` element should not be hidden`);
}
/**
* Screenreaders already announce `<img>` elements as an image.
* @param {null | NodeLike} node
*/
export function a11y_img_redundant_alt(node) {
w(node, "a11y_img_redundant_alt", "Screenreaders already announce `<img>` elements as an image.");
}
/**
* The value of '%attribute%' must be a %type%
* @param {null | NodeLike} node
* @param {string} attribute
* @param {string} type
*/
export function a11y_incorrect_aria_attribute_type(node, attribute, type) {
w(node, "a11y_incorrect_aria_attribute_type", `The value of '${attribute}' must be a ${type}`);
}
/**
* The value of '%attribute%' must be either 'true' or 'false'. It cannot be empty
* @param {null | NodeLike} node
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_boolean(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_boolean", `The value of '${attribute}' must be either 'true' or 'false'. It cannot be empty`);
}
/**
* The value of '%attribute%' must be a string that represents a DOM element ID
* @param {null | NodeLike} node
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_id(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_id", `The value of '${attribute}' must be a string that represents a DOM element ID`);
}
/**
* The value of '%attribute%' must be a space-separated list of strings that represent DOM element IDs
* @param {null | NodeLike} node
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_idlist(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_idlist", `The value of '${attribute}' must be a space-separated list of strings that represent DOM element IDs`);
}
/**
* The value of '%attribute%' must be an integer
* @param {null | NodeLike} node
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_integer(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_integer", `The value of '${attribute}' must be an integer`);
}
/**
* The value of '%attribute%' must be exactly one of %values%
* @param {null | NodeLike} node
* @param {string} attribute
* @param {string} values
*/
export function a11y_incorrect_aria_attribute_type_token(node, attribute, values) {
w(node, "a11y_incorrect_aria_attribute_type_token", `The value of '${attribute}' must be exactly one of ${values}`);
}
/**
* The value of '%attribute%' must be a space-separated list of one or more of %values%
* @param {null | NodeLike} node
* @param {string} attribute
* @param {string} values
*/
export function a11y_incorrect_aria_attribute_type_tokenlist(node, attribute, values) {
w(node, "a11y_incorrect_aria_attribute_type_tokenlist", `The value of '${attribute}' must be a space-separated list of one or more of ${values}`);
}
/**
* The value of '%attribute%' must be exactly one of true, false, or mixed
* @param {null | NodeLike} node
* @param {string} attribute
*/
export function a11y_incorrect_aria_attribute_type_tristate(node, attribute) {
w(node, "a11y_incorrect_aria_attribute_type_tristate", `The value of '${attribute}' must be exactly one of true, false, or mixed`);
}
/**
* Elements with the '%role%' interactive role must have a tabindex value.
* @param {null | NodeLike} node
* @param {string} role
*/
export function a11y_interactive_supports_focus(node, role) {
w(node, "a11y_interactive_supports_focus", `Elements with the '${role}' interactive role must have a tabindex value.`);
}
/**
* '%href_value%' is not a valid %href_attribute% attribute
* @param {null | NodeLike} node
* @param {string} href_value
* @param {string} href_attribute
*/
export function a11y_invalid_attribute(node, href_value, href_attribute) {
w(node, "a11y_invalid_attribute", `'${href_value}' is not a valid ${href_attribute} attribute`);
}
/**
* A form label must be associated with a control.
* @param {null | NodeLike} node
*/
export function a11y_label_has_associated_control(node) {
w(node, "a11y_label_has_associated_control", "A form label must be associated with a control.");
}
/**
* `<video>` elements must have a `<track kind="captions">`
* @param {null | NodeLike} node
*/
export function a11y_media_has_caption(node) {
w(node, "a11y_media_has_caption", "`<video>` elements must have a `<track kind=\"captions\">`");
}
/**
* `<%name%>` should not have role attribute
* @param {null | NodeLike} node
* @param {string} name
*/
export function a11y_misplaced_role(node, name) {
w(node, "a11y_misplaced_role", `\`<${name}>\` should not have role attribute`);
}
/**
* The scope attribute should only be used with `<th>` elements
* @param {null | NodeLike} node
*/
export function a11y_misplaced_scope(node) {
w(node, "a11y_misplaced_scope", "The scope attribute should only be used with `<th>` elements");
}
/**
* `<%name%>` element should have %article% %sequence% attribute
* @param {null | NodeLike} node
* @param {string} name
* @param {string} article
* @param {string} sequence
*/
export function a11y_missing_attribute(node, name, article, sequence) {
w(node, "a11y_missing_attribute", `\`<${name}>\` element should have ${article} ${sequence} attribute`);
}
/**
* `<%name%>` element should have child content
* @param {null | NodeLike} node
* @param {string} name
*/
export function a11y_missing_content(node, name) {
w(node, "a11y_missing_content", `\`<${name}>\` element should have child content`);
}
/**
* '%event%' event must be accompanied by '%accompanied_by%' event
* @param {null | NodeLike} node
* @param {string} event
* @param {string} accompanied_by
*/
export function a11y_mouse_events_have_key_events(node, event, accompanied_by) {
w(node, "a11y_mouse_events_have_key_events", `'${event}' event must be accompanied by '${accompanied_by}' event`);
}
/**
* Abstract role '%role%' is forbidden
* @param {null | NodeLike} node
* @param {string} role
*/
export function a11y_no_abstract_role(node, role) {
w(node, "a11y_no_abstract_role", `Abstract role '${role}' is forbidden`);
}
/**
* `<%element%>` cannot have role '%role%'
* @param {null | NodeLike} node
* @param {string} element
* @param {string} role
*/
export function a11y_no_interactive_element_to_noninteractive_role(node, element, role) {
w(node, "a11y_no_interactive_element_to_noninteractive_role", `\`<${element}>\` cannot have role '${role}'`);
}
/**
* Non-interactive element `<%element%>` should not be assigned mouse or keyboard event listeners.
* @param {null | NodeLike} node
* @param {string} element
*/
export function a11y_no_noninteractive_element_interactions(node, element) {
w(node, "a11y_no_noninteractive_element_interactions", `Non-interactive element \`<${element}>\` should not be assigned mouse or keyboard event listeners.`);
}
/**
* Non-interactive element `<%element%>` cannot have interactive role '%role%'
* @param {null | NodeLike} node
* @param {string} element
* @param {string} role
*/
export function a11y_no_noninteractive_element_to_interactive_role(node, element, role) {
w(node, "a11y_no_noninteractive_element_to_interactive_role", `Non-interactive element \`<${element}>\` cannot have interactive role '${role}'`);
}
/**
* noninteractive element cannot have nonnegative tabIndex value
* @param {null | NodeLike} node
*/
export function a11y_no_noninteractive_tabindex(node) {
w(node, "a11y_no_noninteractive_tabindex", "noninteractive element cannot have nonnegative tabIndex value");
}
/**
* Redundant role '%role%'
* @param {null | NodeLike} node
* @param {string} role
*/
export function a11y_no_redundant_roles(node, role) {
w(node, "a11y_no_redundant_roles", `Redundant role '${role}'`);
}
/**
* `<%element%>` with a %handler% handler must have an ARIA role
* @param {null | NodeLike} node
* @param {string} element
* @param {string} handler
*/
export function a11y_no_static_element_interactions(node, element, handler) {
w(node, "a11y_no_static_element_interactions", `\`<${element}>\` with a ${handler} handler must have an ARIA role`);
}
/**
* Avoid tabindex values above zero
* @param {null | NodeLike} node
*/
export function a11y_positive_tabindex(node) {
w(node, "a11y_positive_tabindex", "Avoid tabindex values above zero");
}
/**
* Elements with the ARIA role "%role%" must have the following attributes defined: %props%
* @param {null | NodeLike} node
* @param {string} role
* @param {string} props
*/
export function a11y_role_has_required_aria_props(node, role, props) {
w(node, "a11y_role_has_required_aria_props", `Elements with the ARIA role "${role}" must have the following attributes defined: ${props}`);
}
/**
* The attribute '%attribute%' is not supported by the role '%role%'
* @param {null | NodeLike} node
* @param {string} attribute
* @param {string} role
*/
export function a11y_role_supports_aria_props(node, attribute, role) {
w(node, "a11y_role_supports_aria_props", `The attribute '${attribute}' is not supported by the role '${role}'`);
}
/**
* The attribute '%attribute%' is not supported by the role '%role%'. This role is implicit on the element `<%name%>`
* @param {null | NodeLike} node
* @param {string} attribute
* @param {string} role
* @param {string} name
*/
export function a11y_role_supports_aria_props_implicit(node, attribute, role, name) {
w(node, "a11y_role_supports_aria_props_implicit", `The attribute '${attribute}' is not supported by the role '${role}'. This role is implicit on the element \`<${name}>\``);
}
/**
* Unknown aria attribute 'aria-%attribute%'. Did you mean '%suggestion%'?
* @param {null | NodeLike} node
* @param {string} attribute
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_aria_attribute(node, attribute, suggestion) {
w(node, "a11y_unknown_aria_attribute", suggestion ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` : `Unknown aria attribute 'aria-${attribute}'`);
}
/**
* Unknown role '%role%'. Did you mean '%suggestion%'?
* @param {null | NodeLike} node
* @param {string} role
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_role(node, role, suggestion) {
w(node, "a11y_unknown_role", suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`);
}
/**
* The `accessors` option has been deprecated. It will have no effect in runes mode
* @param {null | NodeLike} node
*/
export function options_deprecated_accessors(node) {
w(node, "options_deprecated_accessors", "The `accessors` option has been deprecated. It will have no effect in runes mode");
}
/**
* The `immutable` option has been deprecated. It will have no effect in runes mode
* @param {null | NodeLike} node
*/
export function options_deprecated_immutable(node) {
w(node, "options_deprecated_immutable", "The `immutable` option has been deprecated. It will have no effect in runes mode");
}
/**
* The `customElement` option is used when generating a custom element. Did you forget the `customElement: true` compile option?
* @param {null | NodeLike} node
*/
export function options_missing_custom_element(node) {
w(node, "options_missing_custom_element", "The `customElement` option is used when generating a custom element. Did you forget the `customElement: true` compile option?");
}
/**
* The `enableSourcemap` option has been removed. Source maps are always generated now, and tooling can choose to ignore them
* @param {null | NodeLike} node
*/
export function options_removed_enable_sourcemap(node) {
w(node, "options_removed_enable_sourcemap", "The `enableSourcemap` option has been removed. Source maps are always generated now, and tooling can choose to ignore them");
}
/**
* The `hydratable` option has been removed. Svelte components are always hydratable now
* @param {null | NodeLike} node
*/
export function options_removed_hydratable(node) {
w(node, "options_removed_hydratable", "The `hydratable` option has been removed. Svelte components are always hydratable now");
}
/**
* The `loopGuardTimeout` option has been removed
* @param {null | NodeLike} node
*/
export function options_removed_loop_guard_timeout(node) {
w(node, "options_removed_loop_guard_timeout", "The `loopGuardTimeout` option has been removed");
}
/**
* `generate: "dom"` and `generate: "ssr"` options have been renamed to "client" and "server" respectively
* @param {null | NodeLike} node
*/
export function options_renamed_ssr_dom(node) {
w(node, "options_renamed_ssr_dom", "`generate: \"dom\"` and `generate: \"ssr\"` options have been renamed to \"client\" and \"server\" respectively");
}
/**
* Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`
* @param {null | NodeLike} node
*/
export function derived_iife(node) {
w(node, "derived_iife", "Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`");
}
/**
* Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`
* @param {null | NodeLike} node
* @param {string} name
*/
export function export_let_unused(node, name) {
w(node, "export_let_unused", `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``);
}
/**
* `%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates
* @param {null | NodeLike} node
* @param {string} name
*/
export function non_reactive_update(node, name) {
w(node, "non_reactive_update", `\`${name}\` is updated, but is not declared with \`$state(...)\`. Changing its value will not correctly trigger updates`);
}
/**
* Avoid 'new class' instead, declare the class at the top level scope
* @param {null | NodeLike} node
*/
export function perf_avoid_inline_class(node) {
w(node, "perf_avoid_inline_class", "Avoid 'new class' — instead, declare the class at the top level scope");
}
/**
* Avoid declaring classes below the top level scope
* @param {null | NodeLike} node
*/
export function perf_avoid_nested_class(node) {
w(node, "perf_avoid_nested_class", "Avoid declaring classes below the top level scope");
}
/**
* Reactive declarations only exist at the top level of the instance script
* @param {null | NodeLike} node
*/
export function reactive_declaration_invalid_placement(node) {
w(node, "reactive_declaration_invalid_placement", "Reactive declarations only exist at the top level of the instance script");
}
/**
* All dependencies of the reactive declaration are declared in a module script and will not be reactive
* @param {null | NodeLike} node
*/
export function reactive_declaration_module_script(node) {
w(node, "reactive_declaration_module_script", "All dependencies of the reactive declaration are declared in a module script and will not be reactive");
}
/**
* State referenced in its own scope will never update. Did you mean to reference it inside a closure?
* @param {null | NodeLike} node
*/
export function state_referenced_locally(node) {
w(node, "state_referenced_locally", "State referenced in its own scope will never update. Did you mean to reference it inside a closure?");
}
/**
* It looks like you're using the `$%name%` rune, but there is a local binding called `%name%`. Referencing a local variable with a `$` prefix will create a store subscription. Please rename `%name%` to avoid the ambiguity
* @param {null | NodeLike} node
* @param {string} name
*/
export function store_rune_conflict(node, name) {
w(node, "store_rune_conflict", `It looks like you're using the \`$${name}\` rune, but there is a local binding called \`${name}\`. Referencing a local variable with a \`$\` prefix will create a store subscription. Please rename \`${name}\` to avoid the ambiguity`);
}
/**
* Unused CSS selector "%name%"
* @param {null | NodeLike} node
* @param {string} name
*/
export function css_unused_selector(node, name) {
w(node, "css_unused_selector", `Unused CSS selector "${name}"`);
}
/**
* The "is" attribute is not supported cross-browser and should be avoided
* @param {null | NodeLike} node
*/
export function attribute_avoid_is(node) {
w(node, "attribute_avoid_is", "The \"is\" attribute is not supported cross-browser and should be avoided");
}
/**
* You are referencing `globalThis.%name%`. Did you forget to declare a variable with that name?
* @param {null | NodeLike} node
* @param {string} name
*/
export function attribute_global_event_reference(node, name) {
w(node, "attribute_global_event_reference", `You are referencing \`globalThis.${name}\`. Did you forget to declare a variable with that name?`);
}
/**
* Attributes should not contain ':' characters to prevent ambiguity with Svelte directives
* @param {null | NodeLike} node
*/
export function attribute_illegal_colon(node) {
w(node, "attribute_illegal_colon", "Attributes should not contain ':' characters to prevent ambiguity with Svelte directives");
}
/**
* '%wrong%' is not a valid HTML attribute. Did you mean '%right%'?
* @param {null | NodeLike} node
* @param {string} wrong
* @param {string} right
*/
export function attribute_invalid_property_name(node, wrong, right) {
w(node, "attribute_invalid_property_name", `'${wrong}' is not a valid HTML attribute. Did you mean '${right}'?`);
}
/**
* The rest operator (...) will create a new object and binding '%name%' with the original object will not work
* @param {null | NodeLike} node
* @param {string} name
*/
export function bind_invalid_each_rest(node, name) {
w(node, "bind_invalid_each_rest", `The rest operator (...) will create a new object and binding '${name}' with the original object will not work`);
}
/**
* Empty block
* @param {null | NodeLike} node
*/
export function block_empty(node) {
w(node, "block_empty", "Empty block");
}
/**
* `<%name%>` will be treated as an HTML element unless it begins with a capital letter
* @param {null | NodeLike} node
* @param {string} name
*/
export function component_name_lowercase(node, name) {
w(node, "component_name_lowercase", `\`<${name}>\` will be treated as an HTML element unless it begins with a capital letter`);
}
/**
* Self-closing HTML tags for non-void elements are ambiguous use `<%name% ...></%name%>` rather than `<%name% ... />`
* @param {null | NodeLike} node
* @param {string} name
*/
export function element_invalid_self_closing_tag(node, name) {
w(node, "element_invalid_self_closing_tag", `Self-closing HTML tags for non-void elements are ambiguous — use \`<${name} ...></${name}>\` rather than \`<${name} ... />\``);
}
/**
* Using `on:%name%` to listen to the %name% event is deprecated. Use the event attribute `on%name%` instead.
* @param {null | NodeLike} node
* @param {string} name
*/
export function event_directive_deprecated(node, name) {
w(node, "event_directive_deprecated", `Using \`on:${name}\` to listen to the ${name} event is deprecated. Use the event attribute \`on${name}\` instead.`);
}
/**
* Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead.
* @param {null | NodeLike} node
*/
export function slot_element_deprecated(node) {
w(node, "slot_element_deprecated", "Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead.");
}

@ -158,7 +158,14 @@ export function is_tag_valid_with_parent(tag, parent_tag) {
switch (parent_tag) {
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
case 'select':
return tag === 'option' || tag === 'optgroup' || tag === '#text';
return (
tag === 'option' ||
tag === 'optgroup' ||
tag === '#text' ||
tag === 'hr' ||
tag === 'script' ||
tag === 'template'
);
case 'optgroup':
return tag === 'option' || tag === '#text';
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>

@ -1,6 +1,8 @@
import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/client/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
import { lifecycle_outside_component } from './internal/shared/errors.js';
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@ -18,16 +20,16 @@ import { user_effect } from './internal/client/index.js';
*/
export function onMount(fn) {
if (current_component_context === null) {
throw new Error('onMount can only be used during component initialisation.');
lifecycle_outside_component('onMount');
}
if (current_component_context.r) {
if (current_component_context.l !== null) {
init_update_callbacks(current_component_context).m.push(fn);
} else {
user_effect(() => {
const cleanup = untrack(fn);
if (typeof cleanup === 'function') return /** @type {() => void} */ (cleanup);
});
} else {
init_update_callbacks(current_component_context).m.push(fn);
}
}
@ -43,7 +45,7 @@ export function onMount(fn) {
*/
export function onDestroy(fn) {
if (current_component_context === null) {
throw new Error('onDestroy can only be used during component initialisation.');
lifecycle_outside_component('onDestroy');
}
onMount(() => () => untrack(fn));
@ -87,7 +89,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
export function createEventDispatcher() {
const component_context = current_component_context;
if (component_context === null) {
throw new Error('createEventDispatcher can only be used during component initialisation.');
lifecycle_outside_component('createEventDispatcher');
}
return (type, detail, options) => {
@ -126,11 +128,11 @@ export function createEventDispatcher() {
*/
export function beforeUpdate(fn) {
if (current_component_context === null) {
throw new Error('beforeUpdate can only be used during component initialisation');
lifecycle_outside_component('beforeUpdate');
}
if (current_component_context.r) {
throw new Error('beforeUpdate cannot be used in runes mode');
if (current_component_context.l === null) {
e.lifecycle_legacy_only('beforeUpdate');
}
init_update_callbacks(current_component_context).b.push(fn);
@ -150,11 +152,11 @@ export function beforeUpdate(fn) {
*/
export function afterUpdate(fn) {
if (current_component_context === null) {
throw new Error('afterUpdate can only be used during component initialisation.');
lifecycle_outside_component('afterUpdate');
}
if (current_component_context.r) {
throw new Error('afterUpdate cannot be used in runes mode');
if (current_component_context.l === null) {
e.lifecycle_legacy_only('afterUpdate');
}
init_update_callbacks(current_component_context).a.push(fn);
@ -162,10 +164,11 @@ export function afterUpdate(fn) {
/**
* Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate
* @param {import('./internal/client/types.js').ComponentContext} context
* @param {import('#client').ComponentContext} context
*/
function init_update_callbacks(context) {
return (context.u ??= { a: [], b: [], m: [] });
var l = /** @type {import('#client').ComponentContextLegacy} */ (context).l;
return (l.u ??= { a: [], b: [], m: [] });
}
/**

@ -1,5 +1,6 @@
import { current_component } from './internal/server/context.js';
import { noop } from './internal/shared/utils.js';
import * as e from './internal/server/errors.js';
/** @param {() => void} fn */
export function onDestroy(fn) {
@ -20,15 +21,15 @@ export function createEventDispatcher() {
}
export function mount() {
throw new Error('mount(...) is not available on the server');
e.lifecycle_function_unavailable('mount');
}
export function hydrate() {
throw new Error('hydrate(...) is not available on the server');
e.lifecycle_function_unavailable('hydrate');
}
export function unmount() {
throw new Error('unmount(...) is not available on the server');
e.lifecycle_function_unavailable('unmount');
}
export async function tick() {}

@ -1,5 +1,8 @@
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are).
import './ambient.js';
import type { RemoveBindable } from './internal/types.js';
/**
* @deprecated Svelte components were classes in Svelte 4. In Svelte 5, thy are not anymore.
* Use `mount` or `createRoot` instead to instantiate components.
@ -18,13 +21,37 @@ export interface ComponentConstructorOptions<
$$inline?: boolean;
}
// Utility type for ensuring backwards compatibility on a type level: If there's a default slot, add 'children' to the props if it doesn't exist there already
type PropsWithChildren<Props, Slots> = Props &
(Props extends { children?: any }
? {}
: Slots extends { default: any }
? { children?: Snippet }
: {});
/** Tooling for types uses this for properties are being used with `bind:` */
export type Binding<T> = { 'bind:': T };
/**
* Tooling for types uses this for properties that may be bound to.
* Only use this if you author Svelte component type definition files by hand (we recommend using `@sveltejs/package` instead).
* Example:
* ```ts
* export class MyComponent extends SvelteComponent<{ readonly: string, bindable: Bindable<string> }> {}
* ```
* means you can now do `<MyComponent {readonly} bind:bindable />`
*/
export type Bindable<T> = T | Binding<T>;
type WithBindings<T> = {
[Key in keyof T]: Bindable<T[Key]>;
};
/**
* Utility type for ensuring backwards compatibility on a type level:
* - If there's a default slot, add 'children' to the props
* - All props are bindable
*/
type PropsWithChildren<Props, Slots> = WithBindings<Props> &
(Slots extends { default: any }
? // This is unfortunate because it means "accepts no props" turns into "accepts any prop"
// but the alternative is non-fixable type errors because of the way TypeScript index
// signatures work (they will always take precedence and make an impossible-to-satisfy children type).
Props extends Record<string, never>
? any
: { children?: any }
: {});
/**
* Can be used to create strongly typed Svelte components.
@ -55,7 +82,7 @@ type PropsWithChildren<Props, Slots> = Props &
* for more info.
*/
export class SvelteComponent<
Props extends Record<string, any> = any,
Props extends Record<string, any> = Record<string, any>,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> {
@ -74,7 +101,7 @@ export class SvelteComponent<
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$prop_def: PropsWithChildren<Props, Slots>;
$$prop_def: RemoveBindable<Props>; // Without PropsWithChildren: unnecessary, causes type bugs
/**
* For type checking capabilities only.
* Does not exist at runtime.
@ -119,7 +146,7 @@ export class SvelteComponent<
* @deprecated Use `SvelteComponent` instead. See TODO for more information.
*/
export class SvelteComponentTyped<
Props extends Record<string, any> = any,
Props extends Record<string, any> = Record<string, any>,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends SvelteComponent<Props, Events, Slots> {}
@ -154,7 +181,7 @@ export type ComponentEvents<Comp extends SvelteComponent> =
* ```
*/
export type ComponentProps<Comp extends SvelteComponent> =
Comp extends SvelteComponent<infer Props> ? Props : never;
Comp extends SvelteComponent<infer Props> ? RemoveBindable<Props> : never;
/**
* Convenience type to get the type of a Svelte component. Useful for example in combination with
@ -226,4 +253,3 @@ export interface EventDispatcher<EventMap extends Record<string, any>> {
}
export * from './index-client.js';
import './ambient.js';

@ -4,6 +4,7 @@ import { STATE_SYMBOL } from '../constants.js';
import { render_effect } from '../reactivity/effects.js';
import { current_component_context, untrack } from '../runtime.js';
import { get_prototype_of } from '../utils.js';
import * as w from '../warnings.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
@ -115,10 +116,7 @@ export function add_owner(object, owner, global = false) {
let original = get_owner(metadata);
if (owner.filename !== component.filename) {
let message = `${component.filename} passed a value to ${owner.filename} with \`bind:\`, but the value is owned by ${original.filename}. Consider creating a binding between ${original.filename} and ${component.filename}`;
// eslint-disable-next-line no-console
console.warn(message);
w.ownership_invalid_binding(component.filename, owner.filename, original.filename);
}
}
}
@ -227,17 +225,13 @@ export function check_ownership(metadata) {
if (component && !has_owner(metadata, component)) {
let original = get_owner(metadata);
let message =
// @ts-expect-error
if (original.filename !== component.filename) {
// @ts-expect-error
original.filename !== component.filename
? // @ts-expect-error
`${component.filename} mutated a value owned by ${original.filename}. This is strongly discouraged`
: 'Mutating a value outside the component that created it is strongly discouraged';
// eslint-disable-next-line no-console
console.warn(
`${message}. Consider passing values to child components with \`bind:\`, or use a callback instead.`
);
w.ownership_invalid_mutation(component.filename, original.filename);
} else {
w.ownership_invalid_mutation();
}
// eslint-disable-next-line no-console
console.trace();

@ -6,6 +6,7 @@ import { delegate } from './events.js';
import { autofocus } from './misc.js';
import { effect } from '../../reactivity/effects.js';
import { run } from '../../../shared/utils.js';
import * as w from '../../warnings.js';
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@ -111,7 +112,8 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
var events = [];
for (key in next) {
var value = next[key];
// let instead of var because referenced in a closure
let value = next[key];
if (value === prev?.[key]) continue;
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
@ -119,8 +121,8 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
if (prefix === 'on') {
/** @type {{ capture?: true }} */
var opts = {};
var event_name = key.slice(2);
const opts = {};
let event_name = key.slice(2);
var delegated = DelegatedEvents.includes(event_name);
if (
@ -262,13 +264,10 @@ function check_src_in_dev_hydration(element, attribute, value) {
if (attribute === 'srcset' && srcset_url_equal(element, value)) return;
if (src_url_equal(element.getAttribute(attribute) ?? '', value ?? '')) return;
// eslint-disable-next-line no-console
console.error(
`Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` +
`the ${attribute} value that came from the server will be used. Related element:`,
element,
' Differing value:',
value
w.hydration_attribute_changed(
attribute,
element.outerHTML.replace(element.innerHTML, '...'),
String(value)
);
}

@ -2,6 +2,7 @@ import { DEV } from 'esm-env';
import { render_effect, effect } from '../../../reactivity/effects.js';
import { stringify } from '../../../render.js';
import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js';
/**
* @param {HTMLInputElement} input
@ -12,9 +13,8 @@ import { listen_to_event_and_reset_event } from './shared.js';
export function bind_value(input, get_value, update) {
listen_to_event_and_reset_event(input, 'input', () => {
if (DEV && input.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
}
update(is_numberlike_input(input) ? to_number(input.value) : input.value);
@ -22,9 +22,8 @@ export function bind_value(input, get_value, update) {
render_effect(() => {
if (DEV && input.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
}
var value = get_value();

@ -1,4 +1,5 @@
import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
import * as e from '../errors.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
@ -66,5 +67,5 @@ export function hydrate_anchor(node) {
nodes.push(current);
}
throw new Error('Expected a closing hydration marker');
e.hydration_missing_marker_close();
}

@ -14,9 +14,11 @@ import {
* Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects
*/
export function init() {
const context = /** @type {import('#client').ComponentContext} */ (current_component_context);
const callbacks = context.u;
const context = /** @type {import('#client').ComponentContextLegacy} */ (
current_component_context
);
const callbacks = context.l.u;
if (!callbacks) return;
// beforeUpdate
@ -58,11 +60,11 @@ export function init() {
/**
* Invoke the getter of all signals associated with a component
* so they can be registered to the effect this function is called in.
* @param {import('#client').ComponentContext} context
* @param {import('#client').ComponentContextLegacy} context
*/
function observe_all(context) {
if (context.d) {
for (const signal of context.d) get(signal);
if (context.l.s) {
for (const signal of context.l.s) get(signal);
}
deep_read_state(context.s);

@ -0,0 +1,276 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { DEV } from 'esm-env';
/**
* Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
* @returns {never}
*/
export function bind_invalid_checkbox_value() {
if (DEV) {
const error = new Error(`${"bind_invalid_checkbox_value"}\n${"Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("bind_invalid_checkbox_value");
}
}
/**
* Component %component% has an export named `%key%` that a consumer component is trying to access using `bind:%key%`, which is disallowed. Instead, use `bind:this` (e.g. `<%name% bind:this={component} />`) and then access the property on the bound component instance (e.g. `component.%key%`)
* @param {string} component
* @param {string} key
* @param {string} name
* @returns {never}
*/
export function bind_invalid_export(component, key, name) {
if (DEV) {
const error = new Error(`${"bind_invalid_export"}\n${`Component ${component} has an export named \`${key}\` that a consumer component is trying to access using \`bind:${key}\`, which is disallowed. Instead, use \`bind:this\` (e.g. \`<${name} bind:this={component} />\`) and then access the property on the bound component instance (e.g. \`component.${key}\`)`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("bind_invalid_export");
}
}
/**
* A component is attempting to bind to a non-bindable property `%key%` belonging to %component% (i.e. `<%name% bind:%key%={...}>`). To mark a property as bindable: `let { %key% = $bindable() } = $props()`
* @param {string} key
* @param {string} component
* @param {string} name
* @returns {never}
*/
export function bind_not_bindable(key, component, name) {
if (DEV) {
const error = new Error(`${"bind_not_bindable"}\n${`A component is attempting to bind to a non-bindable property \`${key}\` belonging to ${component} (i.e. \`<${name} bind:${key}={...}>\`). To mark a property as bindable: \`let { ${key} = $bindable() } = $props()\``}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("bind_not_bindable");
}
}
/**
* Keyed each block has duplicate key `%value%` at indexes %a% and %b%
* @param {string} a
* @param {string} b
* @param {string | undefined | null} [value]
* @returns {never}
*/
export function each_key_duplicate(a, b, value) {
if (DEV) {
const error = new Error(`${"each_key_duplicate"}\n${value ? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}` : `Keyed each block has duplicate key at indexes ${a} and ${b}`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("each_key_duplicate");
}
}
/**
* `%rune%` cannot be used inside an effect cleanup function
* @param {string} rune
* @returns {never}
*/
export function effect_in_teardown(rune) {
if (DEV) {
const error = new Error(`${"effect_in_teardown"}\n${`\`${rune}\` cannot be used inside an effect cleanup function`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("effect_in_teardown");
}
}
/**
* `%rune%` can only be used inside an effect (e.g. during component initialisation)
* @param {string} rune
* @returns {never}
*/
export function effect_orphan(rune) {
if (DEV) {
const error = new Error(`${"effect_orphan"}\n${`\`${rune}\` can only be used inside an effect (e.g. during component initialisation)`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("effect_orphan");
}
}
/**
* Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
* @returns {never}
*/
export function effect_update_depth_exceeded() {
if (DEV) {
const error = new Error(`${"effect_update_depth_exceeded"}\n${"Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("effect_update_depth_exceeded");
}
}
/**
* Missing hydration closing marker
* @returns {never}
*/
export function hydration_missing_marker_close() {
if (DEV) {
const error = new Error(`${"hydration_missing_marker_close"}\n${"Missing hydration closing marker"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("hydration_missing_marker_close");
}
}
/**
* Missing hydration opening marker
* @returns {never}
*/
export function hydration_missing_marker_open() {
if (DEV) {
const error = new Error(`${"hydration_missing_marker_open"}\n${"Missing hydration opening marker"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("hydration_missing_marker_open");
}
}
/**
* `%name%(...)` cannot be used in runes mode
* @param {string} name
* @returns {never}
*/
export function lifecycle_legacy_only(name) {
if (DEV) {
const error = new Error(`${"lifecycle_legacy_only"}\n${`\`${name}(...)\` cannot be used in runes mode`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("lifecycle_legacy_only");
}
}
/**
* Cannot do `bind:%key%={undefined}` when `%key%` has a fallback value
* @param {string} key
* @returns {never}
*/
export function props_invalid_value(key) {
if (DEV) {
const error = new Error(`${"props_invalid_value"}\n${`Cannot do \`bind:${key}={undefined}\` when \`${key}\` has a fallback value`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("props_invalid_value");
}
}
/**
* Rest element properties of `$props()` such as `%property%` are readonly
* @param {string} property
* @returns {never}
*/
export function props_rest_readonly(property) {
if (DEV) {
const error = new Error(`${"props_rest_readonly"}\n${`Rest element properties of \`$props()\` such as \`${property}\` are readonly`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("props_rest_readonly");
}
}
/**
* The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
* @param {string} rune
* @returns {never}
*/
export function rune_outside_svelte(rune) {
if (DEV) {
const error = new Error(`${"rune_outside_svelte"}\n${`The \`${rune}\` rune is only available inside \`.svelte\` and \`.svelte.js/ts\` files`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("rune_outside_svelte");
}
}
/**
* Cannot set prototype of `$state` object
* @returns {never}
*/
export function state_prototype_fixed() {
if (DEV) {
const error = new Error(`${"state_prototype_fixed"}\n${"Cannot set prototype of `$state` object"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("state_prototype_fixed");
}
}
/**
* Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. This can lead to unexpected errors and possibly cause infinite loops.
* >
* If the object is not meant to be reactive, declare it without `$state`
* @returns {never}
*/
export function state_unsafe_mutation() {
if (DEV) {
const error = new Error(`${"state_unsafe_mutation"}\n${"Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. This can lead to unexpected errors and possibly cause infinite loops.\n>\nIf the object is not meant to be reactive, declare it without `$state`"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("state_unsafe_mutation");
}
}
/**
* The `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined
* @returns {never}
*/
export function svelte_component_invalid_this_value() {
if (DEV) {
const error = new Error(`${"svelte_component_invalid_this_value"}\n${"The `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("svelte_component_invalid_this_value");
}
}

@ -128,8 +128,7 @@ export {
export {
validate_dynamic_component,
validate_each_keys,
validate_prop_bindings,
validate_store
validate_prop_bindings
} from './validate.js';
export { raf } from './timing.js';
export { proxy, snapshot } from './proxy.js';
@ -147,5 +146,6 @@ export {
validate_component,
validate_dynamic_element_tag,
validate_snippet,
validate_store,
validate_void_dynamic_element
} from '../shared/validate.js';

@ -20,6 +20,7 @@ import { check_ownership, widen_ownership } from './dev/ownership.js';
import { mutable_source, source, set } from './reactivity/sources.js';
import { STATE_SYMBOL } from './constants.js';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
/**
* @template T
@ -334,6 +335,6 @@ const state_proxy_handler = {
if (DEV) {
state_proxy_handler.setPrototypeOf = () => {
throw new Error('Cannot set prototype of $state object');
e.state_prototype_fixed();
};
}

@ -1,4 +1,3 @@
import { DEV } from 'esm-env';
import {
check_dirtiness,
current_component_context,
@ -30,6 +29,7 @@ import {
} from '../constants.js';
import { set } from './sources.js';
import { remove } from '../dom/reconciler.js';
import * as e from '../errors.js';
/**
* @param {import('#client').Effect | null} effect
@ -38,19 +38,11 @@ import { remove } from '../dom/reconciler.js';
*/
export function validate_effect(effect, rune) {
if (effect === null) {
throw new Error(
'ERR_SVELTE_ORPHAN_EFFECT' +
(DEV
? `: ${rune} can only be used inside an effect (e.g. during component initialisation)`
: '')
);
e.effect_orphan(rune);
}
if (is_destroying_effect) {
throw new Error(
'ERR_SVELTE_EFFECT_IN_TEARDOWN' +
(DEV ? `: ${rune} cannot be used inside an effect cleanup function.` : '')
);
e.effect_in_teardown(rune);
}
}
@ -182,11 +174,11 @@ export function effect(fn) {
* @param {() => void | (() => void)} fn
*/
export function legacy_pre_effect(deps, fn) {
var context = /** @type {import('#client').ComponentContext} */ (current_component_context);
var context = /** @type {import('#client').ComponentContextLegacy} */ (current_component_context);
/** @type {{ effect: null | import('#client').Effect, ran: boolean }} */
var token = { effect: null, ran: false };
context.l1.push(token);
context.l.r1.push(token);
token.effect = render_effect(() => {
deps();
@ -196,19 +188,19 @@ export function legacy_pre_effect(deps, fn) {
if (token.ran) return;
token.ran = true;
set(context.l2, true);
set(context.l.r2, true);
untrack(fn);
});
}
export function legacy_pre_effect_reset() {
var context = /** @type {import('#client').ComponentContext} */ (current_component_context);
var context = /** @type {import('#client').ComponentContextLegacy} */ (current_component_context);
render_effect(() => {
if (!get(context.l2)) return;
if (!get(context.l.r2)) return;
// Run dirty `$:` statements
for (var token of context.l1) {
for (var token of context.l.r1) {
var effect = token.effect;
if (check_dirtiness(effect)) {
@ -218,7 +210,7 @@ export function legacy_pre_effect_reset() {
token.ran = false;
}
context.l2.v = false; // set directly to avoid rerunning this effect
context.l.r2.v = false; // set directly to avoid rerunning this effect
});
}

@ -11,6 +11,7 @@ import { derived } from './deriveds.js';
import { get, is_signals_recorded, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import { inspect_fn } from '../dev/inspect.js';
import * as e from '../errors.js';
/**
* @param {((value?: number) => number)} fn
@ -46,9 +47,8 @@ const rest_props_handler = {
},
set(target, key) {
if (DEV) {
throw new Error(
`Rest element properties of $props() such as ${target.name}.${String(key)} are readonly`
);
// TODO should this happen in prod too?
e.props_rest_readonly(`${target.name}.${String(key)}`);
}
return false;
@ -196,13 +196,7 @@ export function prop(props, key, flags, fallback) {
if (prop_value === undefined && fallback !== undefined) {
if (setter && runes) {
// TODO consolidate all these random runtime errors
throw new Error(
'ERR_SVELTE_BINDING_FALLBACK' +
(DEV
? `: Cannot pass undefined to bind:${key} because the property contains a fallback value. Pass a different value than undefined to ${key}.`
: '')
);
e.props_invalid_value(key);
}
prop_value = get_fallback();

@ -19,6 +19,7 @@ import {
import { equals, safe_equals } from './equality.js';
import { CLEAN, DERIVED, DIRTY, BRANCH_EFFECT } from '../constants.js';
import { UNINITIALIZED } from '../../../constants.js';
import * as e from '../errors.js';
/**
* @template V
@ -55,8 +56,8 @@ export function mutable_source(initial_value) {
// bind the signal to the component context, in case we need to
// track updates to trigger beforeUpdate/afterUpdate callbacks
if (current_component_context) {
(current_component_context.d ??= []).push(s);
if (current_component_context !== null && current_component_context.l !== null) {
(current_component_context.l.s ??= []).push(s);
}
return s;
@ -91,14 +92,7 @@ export function set(signal, value) {
is_runes() &&
(current_reaction.f & DERIVED) !== 0
) {
throw new Error(
'ERR_SVELTE_UNSAFE_MUTATION' +
(DEV
? ": Unsafe mutations during Svelte's render or derived phase are not permitted in runes mode. " +
'This can lead to unexpected errors and possibly cause infinite loops.\n\nIf this mutation is not meant ' +
'to be reactive do not use the "$state" rune for that declaration.'
: '')
);
e.state_unsafe_mutation();
}
if (!signal.equals(value)) {

@ -19,6 +19,9 @@ import {
import { array_from } from './utils.js';
import { handle_event_propagation } from './dom/elements/events.js';
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js';
import * as e from './errors.js';
import { validate_component } from '../shared/validate.js';
/** @type {Set<string>} */
export const all_registered_events = new Set();
@ -92,7 +95,7 @@ export function stringify(value) {
* @param {{
* target: Document | Element | ShadowRoot;
* anchor?: Node;
* props?: Props;
* props?: import('../types.js').RemoveBindable<Props>;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>;
* intro?: boolean;
@ -100,6 +103,10 @@ export function stringify(value) {
* @returns {Exports}
*/
export function mount(component, options) {
if (DEV) {
validate_component(component);
}
const anchor = options.anchor ?? options.target.appendChild(empty());
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => _mount(component, { ...options, anchor }), false);
@ -114,15 +121,19 @@ export function mount(component, options) {
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
* @param {{
* target: Document | Element | ShadowRoot;
* props?: Props;
* props?: import('../types.js').RemoveBindable<Props>;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* recover?: boolean;
* }} options
* @returns {Exports}
*/
export function hydrate(component, options) {
if (DEV) {
validate_component(component);
}
const target = options.target;
const previous_hydrate_nodes = hydrate_nodes;
@ -142,7 +153,7 @@ export function hydrate(component, options) {
}
if (!node) {
throw new Error('Missing hydration marker');
e.hydration_missing_marker_open();
}
const anchor = hydrate_anchor(node);
@ -157,14 +168,7 @@ export function hydrate(component, options) {
}, false);
} catch (error) {
if (!hydrated && options.recover !== false) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
(DEV
? ': Hydration failed because the initial UI does not match what was rendered on the server.'
: ''),
error
);
w.hydration_mismatch();
clear_text_content(target);
@ -181,24 +185,19 @@ export function hydrate(component, options) {
}
/**
* @template {Record<string, any>} Props
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} Component
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>>} Component
* @param {{
* target: Document | Element | ShadowRoot;
* anchor: Node;
* props?: Props;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* props?: any;
* events?: any;
* context?: Map<any, any>;
* intro?: boolean;
* }} options
* @returns {Exports}
*/
function _mount(
Component,
{ target, anchor, props = /** @type {Props} */ ({}), events, context, intro = false }
) {
function _mount(Component, { target, anchor, props = {}, events, context, intro = false }) {
init_operations();
const registered_events = new Set();
@ -274,6 +273,7 @@ function _mount(
target.removeEventListener(event_name, bound_event_listener);
}
root_event_handles.delete(event_handle);
mounted_components.delete(component);
};
});
@ -294,8 +294,9 @@ let mounted_components = new WeakMap();
export function unmount(component) {
const fn = mounted_components.get(component);
if (DEV && !fn) {
w.lifecycle_double_unmount();
// eslint-disable-next-line no-console
console.warn('Tried to unmount a component that was not mounted.');
console.trace('stack trace');
}
fn?.();
}

@ -22,6 +22,8 @@ import { add_owner } from './dev/ownership.js';
import { mutate, set, source } from './reactivity/sources.js';
import { update_derived } from './reactivity/deriveds.js';
import { inspect_captured_signals, inspect_fn, set_inspect_fn } from './dev/inspect.js';
import * as e from './errors.js';
import { lifecycle_outside_component } from '../shared/errors.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
@ -115,7 +117,7 @@ export function set_current_component_context(context) {
/** @returns {boolean} */
export function is_runes() {
return current_component_context !== null && current_component_context.r;
return current_component_context !== null && current_component_context.l === null;
}
/**
@ -412,13 +414,7 @@ export function execute_effect(effect) {
function infinite_loop_guard() {
if (flush_count > 1000) {
flush_count = 0;
throw new Error(
'ERR_SVELTE_TOO_MANY_UPDATES' +
(DEV
? ': Maximum update depth exceeded. This can happen when a reactive block or effect ' +
'repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops.'
: '')
);
e.effect_update_depth_exceeded();
}
flush_count++;
}
@ -880,12 +876,12 @@ export function is_signal(val) {
* @returns {T}
*/
export function getContext(key) {
const context_map = get_or_init_context_map();
const context_map = get_or_init_context_map('getContext');
const result = /** @type {T} */ (context_map.get(key));
if (DEV) {
// @ts-expect-error
const fn = current_component_context?.function;
const fn = current_component_context.function;
if (fn) {
add_owner(result, fn, true);
}
@ -908,7 +904,7 @@ export function getContext(key) {
* @returns {T}
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map();
const context_map = get_or_init_context_map('setContext');
context_map.set(key, context);
return context;
}
@ -922,7 +918,7 @@ export function setContext(key, context) {
* @returns {boolean}
*/
export function hasContext(key) {
const context_map = get_or_init_context_map();
const context_map = get_or_init_context_map('hasContext');
return context_map.has(key);
}
@ -936,7 +932,7 @@ export function hasContext(key) {
* @returns {T}
*/
export function getAllContexts() {
const context_map = get_or_init_context_map();
const context_map = get_or_init_context_map('getAllContexts');
if (DEV) {
// @ts-expect-error
@ -951,16 +947,18 @@ export function getAllContexts() {
return /** @type {T} */ (context_map);
}
/** @returns {Map<unknown, unknown>} */
function get_or_init_context_map() {
const component_context = current_component_context;
if (component_context === null) {
throw new Error(
'ERR_SVELTE_ORPHAN_CONTEXT' +
(DEV ? 'Context can only be used during component initialisation.' : '')
);
/**
* @param {string} name
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (current_component_context === null) {
lifecycle_outside_component(name);
}
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
return (current_component_context.c ??= new Map(
get_parent_context(current_component_context) || undefined
));
}
/**
@ -1043,29 +1041,24 @@ export async function value_or_fallback_async(value, fallback) {
*/
export function push(props, runes = false, fn) {
current_component_context = {
// exports (and props, if `accessors: true`)
x: null,
// context
p: current_component_context,
c: null,
// effects
e: null,
// mounted
m: false,
// parent
p: current_component_context,
// signals
d: null,
// props
s: props,
// runes
r: runes,
// legacy $:
l1: [],
l2: source(false),
// update_callbacks
u: null
x: null,
l: null
};
if (!runes) {
current_component_context.l = {
s: null,
u: null,
r1: [],
r2: source(false)
};
}
if (DEV) {
// component function
// @ts-expect-error
@ -1193,13 +1186,13 @@ if (DEV) {
let value; // let's hope noone modifies this global, but belts and braces
Object.defineProperty(globalThis, rune, {
configurable: true,
// eslint-disable-next-line getter-return
get: () => {
if (value !== undefined) {
return value;
}
throw new Error(
`The ${rune} rune is only available inside .svelte and .svelte.js/ts files`
);
e.rune_outside_svelte(rune);
},
set: (v) => {
value = v;

@ -1,3 +1,4 @@
import type { Bindable, Binding } from '../../index.js';
import type { Store } from '#shared';
import { STATE_SYMBOL } from './constants.js';
import type { Effect, Source, Value } from './reactivity/types.js';
@ -10,37 +11,51 @@ export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>;
// when the JS VM JITs the code.
export type ComponentContext = {
/** local signals (needed for beforeUpdate/afterUpdate) */
d: null | Source[];
/** props */
s: Record<string, unknown>;
/** exports (and props, if `accessors: true`) */
x: Record<string, any> | null;
/** deferred effects */
e: null | Array<() => void | (() => void)>;
/** mounted */
m: boolean;
/** parent */
p: null | ComponentContext;
/** context */
c: null | Map<unknown, unknown>;
/** runes */
r: boolean;
/** legacy mode: if `$:` statements are allowed to run (ensures they only run once per render) */
l1: any[];
/** legacy mode: if `$:` statements are allowed to run (ensures they only run once per render) */
l2: Source<boolean>;
/** update_callbacks */
u: null | {
/** afterUpdate callbacks */
a: Array<() => void>;
/** beforeUpdate callbacks */
b: Array<() => void>;
/** onMount callbacks */
m: Array<() => any>;
/** deferred effects */
e: null | Array<() => void | (() => void)>;
/** mounted */
m: boolean;
/**
* props needed for legacy mode lifecycle functions, and for `createEventDispatcher`
* @deprecated remove in 6.0
*/
s: Record<string, unknown>;
/**
* exports (and props, if `accessors: true`) needed for `createEventDispatcher`
* @deprecated remove in 6.0
*/
x: Record<string, any> | null;
/**
* legacy stuff
* @deprecated remove in 6.0
*/
l: null | {
/** local signals (needed for beforeUpdate/afterUpdate) */
s: null | Source[];
/** update_callbacks */
u: null | {
/** afterUpdate callbacks */
a: Array<() => void>;
/** beforeUpdate callbacks */
b: Array<() => void>;
/** onMount callbacks */
m: Array<() => any>;
};
/** `$:` statements */
r1: any[];
/** This tracks whether `$:` statements have run in the current cycle, to ensure they only run once */
r2: Source<boolean>;
};
};
export type ComponentContextLegacy = ComponentContext & {
l: NonNullable<ComponentContext['l']>;
};
export type Equals = (this: Value, value: unknown) => boolean;
export type TemplateNode = Text | Element | Comment;

@ -1,5 +1,6 @@
import { untrack } from './runtime.js';
import { get_descriptor, is_array } from './utils.js';
import * as e from './errors.js';
/** regex of all html void element names */
const void_element_names =
@ -10,35 +11,27 @@ function is_void(tag) {
return void_element_names.test(tag) || tag.toLowerCase() === '!doctype';
}
/**
* @param {any} store
* @param {string} name
*/
export function validate_store(store, name) {
if (store != null && typeof store.subscribe !== 'function') {
throw new Error(`'${name}' is not a store with a 'subscribe' method`);
}
}
/**
* @param {() => any} component_fn
* @returns {any}
*/
export function validate_dynamic_component(component_fn) {
const error_message = 'this={...} of <svelte:component> should specify a Svelte component.';
try {
const instance = component_fn();
if (instance !== undefined && typeof instance !== 'object') {
throw new Error(error_message);
e.svelte_component_invalid_this_value();
}
return instance;
} catch (err) {
const { message } = /** @type {Error} */ (err);
if (typeof message === 'string' && message.indexOf('is not a function') !== -1) {
throw new Error(error_message);
} else {
throw err;
e.svelte_component_invalid_this_value();
}
throw err;
}
}
@ -59,43 +52,37 @@ export function validate_each_keys(collection, key_fn) {
for (let i = 0; i < length; i++) {
const key = key_fn(array[i], i);
if (keys.has(key)) {
throw new Error(
`Cannot have duplicate keys in a keyed each: Keys at index ${keys.get(
key
)} and ${i} with value '${array[i]}' are duplicates`
);
const a = String(keys.get(key));
const b = String(i);
/** @type {string | null} */
let k = String(array[i]);
if (k.startsWith('[object ')) k = null;
e.each_key_duplicate(a, b, k);
}
keys.set(key, i);
}
}
/**
* @param {number} timeout
* @returns {() => void}
* */
export function loop_guard(timeout) {
const start = Date.now();
return () => {
if (Date.now() - start > timeout) {
throw new Error('Infinite loop detected');
}
};
}
/**
* @param {Record<string, any>} $$props
* @param {string[]} bindable
* @param {string[]} exports
* @param {Function & { filename: string }} component
*/
export function validate_prop_bindings($$props, bindable) {
export function validate_prop_bindings($$props, bindable, exports, component) {
for (const key in $$props) {
if (!bindable.includes(key)) {
var setter = get_descriptor($$props, key)?.set;
var setter = get_descriptor($$props, key)?.set;
var name = component.name;
if (setter) {
if (exports.includes(key)) {
e.bind_invalid_export(component.filename, key, name);
}
if (setter) {
throw new Error(
`Cannot use bind:${key} on this component because the property was not declared as bindable. ` +
`To mark a property as bindable, use the $bindable() rune like this: \`let { ${key} = $bindable() } = $props()\``
);
if (!bindable.includes(key)) {
e.bind_not_bindable(key, component.filename, name);
}
}
}

@ -0,0 +1,74 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
* @param {string} attribute
* @param {string} html
* @param {string} value
*/
export function hydration_attribute_changed(attribute, html, value) {
if (DEV) {
console.warn(`%c[svelte] ${"hydration_attribute_changed"}\n%c${`The \`${attribute}\` attribute on \`${html}\` changed its value between server and client renders. The client value, \`${value}\`, will be ignored in favour of the server value`}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("hydration_attribute_changed");
}
}
/**
* Hydration failed because the initial UI does not match what was rendered on the server
*/
export function hydration_mismatch() {
if (DEV) {
console.warn(`%c[svelte] ${"hydration_mismatch"}\n%c${"Hydration failed because the initial UI does not match what was rendered on the server"}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("hydration_mismatch");
}
}
/**
* Tried to unmount a component that was not mounted
*/
export function lifecycle_double_unmount() {
if (DEV) {
console.warn(`%c[svelte] ${"lifecycle_double_unmount"}\n%c${"Tried to unmount a component that was not mounted"}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("lifecycle_double_unmount");
}
}
/**
* %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
* @param {string} parent
* @param {string} child
* @param {string} owner
*/
export function ownership_invalid_binding(parent, child, owner) {
if (DEV) {
console.warn(`%c[svelte] ${"ownership_invalid_binding"}\n%c${`${parent} passed a value to ${child} with \`bind:\`, but the value is owned by ${owner}. Consider creating a binding between ${owner} and ${parent}`}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("ownership_invalid_binding");
}
}
/**
* %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
* @param {string | undefined | null} [component]
* @param {string | undefined | null} [owner]
*/
export function ownership_invalid_mutation(component, owner) {
if (DEV) {
console.warn(`%c[svelte] ${"ownership_invalid_mutation"}\n%c${`${component} mutated a value owned by ${owner}. This is strongly discouraged. Consider passing values to child components with \`bind:\`, or use a callback instead`}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("ownership_invalid_mutation");
}
}

@ -1,5 +1,6 @@
import { DEV } from 'esm-env';
import { on_destroy } from './index.js';
import * as e from '../shared/errors.js';
/** @type {import('#server').Component | null} */
export var current_component = null;
@ -10,7 +11,7 @@ export var current_component = null;
* @returns {T}
*/
export function getContext(key) {
const context_map = getAllContexts();
const context_map = get_or_init_context_map('getContext');
const result = /** @type {T} */ (context_map.get(key));
return result;
@ -23,7 +24,7 @@ export function getContext(key) {
* @returns {T}
*/
export function setContext(key, context) {
getAllContexts().set(key, context);
get_or_init_context_map('setContext').set(key, context);
return context;
}
@ -32,21 +33,24 @@ export function setContext(key, context) {
* @returns {boolean}
*/
export function hasContext(key) {
return getAllContexts().has(key);
return get_or_init_context_map('hasContext').has(key);
}
/** @returns {Map<any, any>} */
export function getAllContexts() {
const context = current_component;
return get_or_init_context_map('getAllContexts');
}
if (context === null) {
throw new Error(
'ERR_SVELTE_ORPHAN_CONTEXT' +
(DEV ? 'Context can only be used during component initialisation.' : '')
);
/**
* @param {string} name
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (current_component === null) {
e.lifecycle_outside_component(name);
}
return (context.c ??= new Map(get_parent_context(context) || undefined));
return (current_component.c ??= new Map(get_parent_context(current_component) || undefined));
}
export function push() {

@ -0,0 +1,13 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
/**
* `%name%(...)` is not available on the server
* @param {string} name
* @returns {never}
*/
export function lifecycle_function_unavailable(name) {
const error = new Error(`${"lifecycle_function_unavailable"}\n${`\`${name}(...)\` is not available on the server`}`);
error.name = 'Svelte error';
throw error;
}

@ -11,6 +11,7 @@ import {
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { validate_store } from '../shared/validate.js';
/**
* @typedef {{
@ -443,16 +444,6 @@ export function store_get(store_values, store_name, store) {
return store_values[store_name][2];
}
/**
* @param {any} store
* @param {string} name
*/
export function validate_store(store, name) {
if (store != null && typeof store.subscribe !== 'function') {
throw new Error(`'${name}' is not a store with a 'subscribe' method`);
}
}
/**
* Sets the new value of a store and returns that value.
* @template V
@ -631,19 +622,6 @@ export function ensure_array_like(array_like_or_iterator) {
: Array.from(array_like_or_iterator);
}
/**
* @param {number} timeout
* @returns {() => void}
* */
export function loop_guard(timeout) {
const start = Date.now();
return () => {
if (Date.now() - start > timeout) {
throw new Error('Infinite loop detected');
}
};
}
/**
* @param {any[]} args
* @param {Function} [inspect]

@ -0,0 +1,85 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { DEV } from 'esm-env';
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name
* @returns {never}
*/
export function lifecycle_outside_component(name) {
if (DEV) {
const error = new Error(`${"lifecycle_outside_component"}\n${`\`${name}(...)\` can only be used during component initialisation`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("lifecycle_outside_component");
}
}
/**
* The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`
* @returns {never}
*/
export function render_tag_invalid_argument() {
if (DEV) {
const error = new Error(`${"render_tag_invalid_argument"}\n${"The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("render_tag_invalid_argument");
}
}
/**
* A snippet must be rendered with `{@render ...}`
* @returns {never}
*/
export function snippet_used_as_component() {
if (DEV) {
const error = new Error(`${"snippet_used_as_component"}\n${"A snippet must be rendered with `{@render ...}`"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("snippet_used_as_component");
}
}
/**
* `%name%` is not a store with a `subscribe` method
* @param {string} name
* @returns {never}
*/
export function store_invalid_shape(name) {
if (DEV) {
const error = new Error(`${"store_invalid_shape"}\n${`\`${name}\` is not a store with a \`subscribe\` method`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("store_invalid_shape");
}
}
/**
* The `this` prop on `<svelte:element>` must be a string, if defined
* @returns {never}
*/
export function svelte_element_invalid_this_value() {
if (DEV) {
const error = new Error(`${"svelte_element_invalid_this_value"}\n${"The `this` prop on `<svelte:element>` must be a string, if defined"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("svelte_element_invalid_this_value");
}
}

@ -1,4 +1,6 @@
import { is_void } from '../../compiler/phases/1-parse/utils/names.js';
import * as w from './warnings.js';
import * as e from './errors.js';
const snippet_symbol = Symbol.for('svelte.snippet');
@ -16,11 +18,9 @@ export function add_snippet_symbol(fn) {
*/
export function validate_snippet(snippet_fn) {
if (snippet_fn && snippet_fn[snippet_symbol] !== true) {
throw new Error(
'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' +
'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.'
);
e.render_tag_invalid_argument();
}
return snippet_fn;
}
@ -30,8 +30,9 @@ export function validate_snippet(snippet_fn) {
*/
export function validate_component(component_fn) {
if (component_fn?.[snippet_symbol] === true) {
throw new Error('A snippet must be rendered with `{@render ...}`');
e.snippet_used_as_component();
}
return component_fn;
}
@ -42,8 +43,7 @@ export function validate_component(component_fn) {
export function validate_void_dynamic_element(tag_fn) {
const tag = tag_fn();
if (tag && is_void(tag)) {
// eslint-disable-next-line no-console
console.warn(`<svelte:element this="${tag}"> is self-closing and cannot have content.`);
w.dynamic_void_element_content(false, tag);
}
}
@ -52,6 +52,16 @@ export function validate_dynamic_element_tag(tag_fn) {
const tag = tag_fn();
const is_string = typeof tag === 'string';
if (tag && !is_string) {
throw new Error('<svelte:element> expects "this" attribute to be a string.');
e.svelte_element_invalid_this_value();
}
}
/**
* @param {any} store
* @param {string} name
*/
export function validate_store(store, name) {
if (store != null && typeof store.subscribe !== 'function') {
e.store_invalid_shape(name);
}
}

@ -0,0 +1,21 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* `<svelte:element this="%tag%">` is a void element it cannot have content
* @param {boolean} trace
* @param {string} tag
*/
export function dynamic_void_element_content(trace, tag) {
if (DEV) {
console.warn(`%c[svelte] ${"dynamic_void_element_content"}\n%c${`\`<svelte:element this="${tag}">\` is a void element — it cannot have content`}`, bold, normal);
if (trace) console.trace('stack trace');
} else {
// TODO print a link to the documentation
console.warn("dynamic_void_element_content");
}
}

@ -1,2 +1,8 @@
import type { Bindable } from '../index.js';
/** Anything except a function */
export type NotFunction<T> = T extends Function ? never : T;
export type RemoveBindable<Props extends Record<string, any>> = {
[Key in keyof Props]: Props[Key] extends Bindable<infer Value> ? Value : Props[Key];
};

@ -1,4 +1,5 @@
import { proxy } from '../internal/client/proxy.js';
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
import { define_property } from '../internal/client/utils.js';
@ -128,3 +129,14 @@ class Svelte4Component {
this.#instance.$destroy();
}
}
/**
* Runs the given function once immediately on the server, and works like `$effect.pre` on the client.
*
* @deprecated Use this only as a temporary solution to migrate your component code to Svelte 5.
* @param {() => void | (() => void)} fn
* @returns {void}
*/
export function run(fn) {
user_pre_effect(fn);
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save