Merge branch 'main' into deprecations

pull/11277/head
Simon Holthausen 1 year ago
commit 92250b59fa

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: run render functions for dynamic void elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow events to continue propagating following an error

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: resolve type definition error in `svelte/compiler`

@ -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-flies-wash",
"beige-mirrors-listen", "beige-mirrors-listen",
"beige-rabbits-shave", "beige-rabbits-shave",
"beige-seas-share",
"big-cars-serve", "big-cars-serve",
"big-eggs-flash", "big-eggs-flash",
"big-eyes-carry", "big-eyes-carry",
@ -106,6 +107,7 @@
"few-clouds-shop", "few-clouds-shop",
"few-mugs-fail", "few-mugs-fail",
"few-teachers-know", "few-teachers-know",
"fifty-masks-give",
"fifty-rice-wait", "fifty-rice-wait",
"fifty-steaks-float", "fifty-steaks-float",
"five-tigers-search", "five-tigers-search",
@ -197,6 +199,7 @@
"light-badgers-glow", "light-badgers-glow",
"light-days-clean", "light-days-clean",
"light-humans-hang", "light-humans-hang",
"light-penguins-invent",
"light-pens-watch", "light-pens-watch",
"little-pans-jog", "little-pans-jog",
"long-buckets-lay", "long-buckets-lay",
@ -211,6 +214,7 @@
"lovely-houses-own", "lovely-houses-own",
"lovely-items-turn", "lovely-items-turn",
"lovely-rules-eat", "lovely-rules-eat",
"lucky-colts-remember",
"lucky-schools-hang", "lucky-schools-hang",
"lucky-toes-begin", "lucky-toes-begin",
"many-rockets-give", "many-rockets-give",
@ -259,6 +263,7 @@
"orange-yaks-protect", "orange-yaks-protect",
"pink-bikes-agree", "pink-bikes-agree",
"pink-mayflies-tie", "pink-mayflies-tie",
"plenty-starfishes-dress",
"polite-dolphins-care", "polite-dolphins-care",
"polite-pumpkins-guess", "polite-pumpkins-guess",
"polite-ravens-study", "polite-ravens-study",
@ -303,6 +308,7 @@
"selfish-socks-smile", "selfish-socks-smile",
"selfish-spies-help", "selfish-spies-help",
"selfish-tools-hide", "selfish-tools-hide",
"serious-crabs-punch",
"serious-gorillas-eat", "serious-gorillas-eat",
"serious-kids-deliver", "serious-kids-deliver",
"serious-needles-joke", "serious-needles-joke",
@ -320,12 +326,14 @@
"sharp-kids-happen", "sharp-kids-happen",
"sharp-tomatoes-learn", "sharp-tomatoes-learn",
"shiny-baboons-play", "shiny-baboons-play",
"shiny-mayflies-clean",
"shiny-rats-heal", "shiny-rats-heal",
"shiny-shrimps-march", "shiny-shrimps-march",
"short-buses-camp", "short-buses-camp",
"short-countries-rush", "short-countries-rush",
"shy-fishes-drive", "shy-fishes-drive",
"silent-apes-report", "silent-apes-report",
"silent-hats-stare",
"silly-laws-happen", "silly-laws-happen",
"silly-lies-film", "silly-lies-film",
"silly-ways-wash", "silly-ways-wash",
@ -341,6 +349,7 @@
"slow-kids-sparkle", "slow-kids-sparkle",
"slow-plums-chew", "slow-plums-chew",
"slow-wombats-reply", "slow-wombats-reply",
"small-apples-eat",
"small-papayas-laugh", "small-papayas-laugh",
"small-sheep-type", "small-sheep-type",
"small-spiders-fail", "small-spiders-fail",
@ -404,6 +413,7 @@
"thick-cycles-rule", "thick-cycles-rule",
"thick-pans-tell", "thick-pans-tell",
"thick-shirts-deliver", "thick-shirts-deliver",
"thick-swans-type",
"thin-foxes-lick", "thin-foxes-lick",
"thirty-flowers-sit", "thirty-flowers-sit",
"thirty-ghosts-fix", "thirty-ghosts-fix",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: only destroy snippets when they have changed

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add type arguments to Map and Set

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: implement `:global {...}` CSS blocks

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add read-only `bind:focused`

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

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

@ -1,5 +1,37 @@
# svelte # svelte
## 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
- fix: run render functions for dynamic void elements ([#11258](https://github.com/sveltejs/svelte/pull/11258))
- fix: allow events to continue propagating following an error ([#11263](https://github.com/sveltejs/svelte/pull/11263))
- fix: resolve type definition error in `svelte/compiler` ([#11283](https://github.com/sveltejs/svelte/pull/11283))
- feat: include `script` and `svelte:options` attributes in ast ([#11241](https://github.com/sveltejs/svelte/pull/11241))
- fix: only destroy snippets when they have changed ([#11267](https://github.com/sveltejs/svelte/pull/11267))
- fix: add type arguments to Map and Set ([#10820](https://github.com/sveltejs/svelte/pull/10820))
- feat: implement `:global {...}` CSS blocks ([#11276](https://github.com/sveltejs/svelte/pull/11276))
- feat: add read-only `bind:focused` ([#11271](https://github.com/sveltejs/svelte/pull/11271))
## 5.0.0-next.110 ## 5.0.0-next.110
### Patch Changes ### Patch Changes

@ -0,0 +1,59 @@
## empty_attribute_shorthand
Attribute shorthand cannot be empty
## duplicate_attribute
Attributes need to be unique
## invalid_event_attribute_value
Event attribute must be a JavaScript expression, not a string
## invalid_attribute_name
'%name%' is not a valid attribute name
## 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?
## animation_duplicate
An element can only have one 'animate' directive
## invalid_event_modifier
Valid event modifiers are %list%
## invalid_component_event_modifier
Event modifiers other than 'once' can only be used on DOM elements
## invalid_event_modifier_combination
The '%modifier1%' and '%modifier2%' modifiers cannot be used together
## transition_duplicate
Cannot use multiple `%type%:` directives on a single element
## transition_conflict
Cannot use `%type%:` alongside existing `%existing%:` directive
## invalid_let_directive_placement
`let:` directive at invalid position
## invalid_style_directive_modifier
Invalid 'style:' modifier. Valid modifiers are: 'important'
## invalid_sequence_expression
Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses

@ -0,0 +1,35 @@
## invalid_binding_expression
Can only bind to an Identifier or MemberExpression
## invalid_binding_value
Can only bind to state or props
## bind_invalid_target
`bind:%name%` can only be used with %elements%
## bind_invalid
`bind:%name%` is not a valid binding
## bind_invalid_detailed
`bind:%name%` is not a valid binding. %explanation%
## invalid_type_attribute
'type' attribute must be a static text value if input uses two-way binding
## invalid_multiple_attribute
'multiple' attribute must be static if select uses two-way binding
## missing_contenteditable_attribute
'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings
## dynamic_contenteditable_attribute
'contenteditable' attribute cannot be dynamic if element uses two-way binding

@ -0,0 +1,7 @@
## invalid_compiler_option
Invalid compiler option: %msg%
## removed_compiler_option
Invalid compiler option: %msg%

@ -0,0 +1,3 @@
## invalid_component_directive
This type of directive is not valid on components

@ -0,0 +1,3 @@
## invalid_const_placement
{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>

@ -0,0 +1,51 @@
## invalid_css_empty_declaration
Declaration cannot be empty
## invalid_css_global_block_list
A :global {...} block cannot be part of a selector list with more than one item
## invalid_css_global_block_modifier
A :global {...} block cannot modify an existing selector
## invalid_css_global_block_combinator
A :global {...} block cannot follow a %name% combinator
## invalid_css_global_block_declaration
A :global {...} block can only contain rules, not declarations
## invalid_css_global_placement
:global(...) can be at the start or end of a selector sequence, but not in the middle
## invalid_css_global_selector
:global(...) must contain exactly one selector
## invalid_css_global_selector_list
:global(...) must not contain type or universal selectors when used in a compound selector
## invalid_css_type_selector_placement
:global(...) must not be followed with a type selector
## invalid_css_selector
Invalid selector
## invalid_css_identifier
Expected a valid CSS identifier
## invalid_nesting_selector
Nesting selectors can only be used inside a rule
## invalid_css_declaration
Declaration cannot be empty

@ -0,0 +1,27 @@
## invalid_textarea_content
A `<textarea>` can have either a value attribute or (equivalently) child content, but not both
## invalid_void_content
Void elements cannot have children or closing tags
## invalid_element_content
<%name%> cannot have children
## invalid_tag_name
Expected valid tag name
## invalid_node_placement
%thing% is invalid inside <%parent%>
## illegal_title_attribute
`<title>` cannot have attributes nor directives
## invalid_title_content
`<title>` can only contain text and {tags}

@ -0,0 +1,3 @@
## cyclical_reactive_declaration
Cyclical dependency detected: %cycle%

@ -0,0 +1,147 @@
## unclosed_element
`<%name%>` was left open
## unclosed_block
Block was left open
## unexpected_block_close
Unexpected block closing tag
## unexpected_eof
Unexpected end of input
## js_parse_error
%message%
## expected_token
Expected token %token%
## unexpected_reserved_word
'%word%' is a reserved word in JavaScript and cannot be used here
## missing_whitespace
Expected whitespace
## expected_pattern
Expected identifier or destructure pattern
## invalid_script_context
If the context attribute is supplied, its value must be "module"
## invalid_elseif
'elseif' should be 'else if'
## invalid_continuing_block_placement
{:...} block is invalid at this position (did you forget to close the preceeding element or block?)
## invalid_block_missing_parent
%child% block must be a child of %parent%
## duplicate_block_part
%name% cannot appear more than once within a block
## expected_block_type
Expected 'if', 'each', 'await', 'key' or 'snippet'
## expected_identifier
Expected an identifier
## invalid_debug
{@debug ...} arguments must be identifiers, not arbitrary expressions
## invalid_const
{@const ...} must be an assignment
## invalid_block_placement
{#%name% ...} block cannot be %location%
## invalid_tag_placement
{@%name% ...} tag cannot be %location%
## missing_attribute_value
Expected attribute value
## unclosed_attribute_value
Expected closing %delimiter% character
## invalid_directive_value
Directive value must be a JavaScript expression enclosed in curly braces
## empty_directive_name
%type% name cannot be empty
## invalid_closing_tag
</%name%> attempted to close an element that was not open
## invalid_closing_tag_after_autoclose
</%name%> attempted to close element that was already automatically closed by <%reason%> (cannot nest <%reason%> inside <%name%>)
## invalid_dollar_binding
The $ name is reserved, and cannot be used for variables and imports
## invalid_dollar_prefix
The $ prefix is reserved, and cannot be used for variables and imports
## invalid_dollar_global
The $ name is reserved. To reference a global variable called $, use globalThis.$
## illegal_subscription
Cannot reference store value inside `<script context="module">`
## duplicate_style_element
A component can have a single top-level `<style>` element
## duplicate_script_element
A component can have a single top-level `<script>` element and/or a single top-level `<script context="module">` element
## invalid_render_expression
{@render ...} tags can only contain call expressions
## invalid_render_arguments
expected at most one argument
## invalid_render_call
Calling a snippet function using apply, bind or call is not allowed
## invalid_render_spread_argument
cannot use spread arguments in {@render ...} tags
## invalid_snippet_rest_parameter
snippets do not support rest parameters; use an array instead

@ -0,0 +1,95 @@
## invalid_legacy_props
Cannot use `$$props` in runes mode
## invalid_legacy_rest_props
Cannot use `$$restProps` in runes mode
## invalid_legacy_reactive_statement
`$:` is not allowed in runes mode, use `$derived` or `$effect` instead
## invalid_legacy_export
Cannot use `export let` in runes mode — use $props instead
## invalid_rune_usage
Cannot use %rune% rune in non-runes mode
## invalid_state_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
## invalid_derived_export
Cannot export derived state from a module. To expose the current derived value, export a function returning its value
## invalid_props_id
`$props()` can only be used with an object destructuring pattern
## invalid_props_pattern
`$props()` assignment must not contain nested properties or computed keys
## invalid_props_location
`$props()` can only be used at the top level of components as a variable declaration initializer
## invalid_bindable_location
`$bindable()` can only be used inside a `$props()` declaration
## invalid_state_location
`%rune%(...)` can only be used as a variable declaration initializer or a class field
## invalid_effect_location
`$effect()` can only be used as an expression statement
## invalid_host_location
`$host()` can only be used inside custom element component instances
## invalid_assignment
Cannot assign to %thing%
## invalid_binding
Cannot bind to %thing%
## invalid_rune_args
`%rune%` cannot be called with arguments
## invalid_rune_args_length
`%rune%` must be called with %args%
## invalid_runes_mode_import
%name% cannot be used in runes mode
## duplicate_props_rune
Cannot use `$props()` more than once
## invalid_each_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`)
## invalid_snippet_assignment
Cannot reassign or bind to snippet parameter
## invalid_derived_call
`$derived.call(...)` has been replaced with `$derived.by(...)`
## conflicting_property_name
Cannot have a property and a component export with the same name

@ -0,0 +1,31 @@
## invalid_slot_element_attribute
`<slot>` can only receive attributes and (optionally) let directives
## invalid_slot_attribute
slot attribute must be a static value
## invalid_slot_name_default
`default` is a reserved word — it cannot be used as a slot name
## invalid_slot_name
slot attribute must be a static value
## invalid_slot_placement
Element with a slot='...' attribute must be a child of a component or a descendant of a custom element
## duplicate_slot_name
Duplicate slot name '%name%' in <%component%>
## invalid_default_slot_content
Found default slot content alongside an explicit slot="default"
## conflicting_children_snippet
Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block

@ -0,0 +1,99 @@
## invalid_svelte_option_attribute
`<svelte:options>` can only receive static attributes
## invalid_svelte_option_namespace
Unsupported `<svelte:option>` value for "namespace". Valid values are "html", "svg" or "foreign"
## tag_option_deprecated
"tag" option is deprecated — use "customElement" instead
## invalid_svelte_option_runes
Unsupported `<svelte:option>` value for "runes". Valid values are true or false
## invalid_svelte_option_accessors
Unsupported `<svelte:option>` value for "accessors". Valid values are true or false
## invalid_svelte_option_preserveWhitespace
Unsupported `<svelte:option>` value for "preserveWhitespace". Valid values are true or false
## invalid_svelte_option_immutable
Unsupported `<svelte:option>` value for "immutable". Valid values are true or false
## invalid_tag_property
Tag name must be two or more words joined by the "-" character
## invalid_svelte_option_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: .. } } }
## invalid_customElement_props_attribute
"props" must be a statically analyzable object literal of the form "{ [key: string]: { attribute?: string; reflect?: boolean; type?: "String" | "Boolean" | "Number" | "Array" | "Object" }"
## invalid_customElement_shadow_attribute
"shadow" must be either "open" or "none"
## unknown_svelte_option_attribute
`<svelte:options>` unknown attribute '%name%'
## illegal_svelte_head_attribute
`<svelte:head>` cannot have attributes nor directives
## invalid_svelte_fragment_attribute
`<svelte:fragment>` can only have a slot attribute and (optionally) a let: directive
## invalid_svelte_fragment_slot
`<svelte:fragment>` slot attribute must have a static value
## invalid_svelte_fragment_placement
`<svelte:fragment>` must be the direct child of a component
## invalid_svelte_element_placement
<%name%> tags cannot be inside elements or blocks
## duplicate_svelte_element
A component can only have one <%name%> element
## invalid_self_placement
`<svelte:self>` components can only exist inside {#if} blocks, {#each} blocks, {#snippet} blocks or slots passed to components
## missing_svelte_element_definition
`<svelte:element>` must have a 'this' attribute
## missing_svelte_component_definition
`<svelte:component>` must have a 'this' attribute
## invalid_svelte_element_definition
Invalid element definition — must be an {expression}
## invalid_svelte_component_definition
Invalid component definition — must be an {expression}
## invalid_svelte_tag
Valid `<svelte:...>` tag names are %list%
## conflicting_slot_usage
Cannot use `<slot>` syntax and `{@render ...}` tags in the same component. Migrate towards `{@render ...}` tags completely.

@ -0,0 +1,19 @@
## illegal_global
`%name%` is an illegal variable name. To reference a global variable called `%name%`, use `globalThis.%name%`
## duplicate_declaration
`%name%` has already been declared
## default_export
A component cannot have a default export
## illegal_variable_declaration
Cannot declare same variable name which is imported inside `<script context="module">`
## illegal_store_subscription
Cannot subscribe to stores that are not declared at the top level of the component

@ -0,0 +1,171 @@
## a11y_aria_attributes
<%name%> should not have aria-* attributes
## a11y_unknown_aria_attribute
Unknown aria attribute 'aria-%attribute%'
## a11y_unknown_aria_attribute_suggestion
Unknown aria attribute 'aria-%attribute%'. Did you mean '%suggestion%'?
## a11y_hidden
<%name%> element should not be hidden
## a11y_incorrect_aria_attribute_type_boolean
The value of '%attribute%' must be either 'true' or 'false'
## a11y_incorrect_aria_attribute_type_integer
The value of '%attribute%' must be an integer
## 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_tristate
The value of '%attribute%' must be exactly one of true, false, or mixed
## 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
The value of '%attribute%' must be of type %type%
## a11y_aria_activedescendant_has_tabindex
Elements with attribute aria-activedescendant should have tabindex value
## a11y_misplaced_role
<%name%> should not have role attribute
## a11y_no_abstract_role
Abstract role '%role%' is forbidden
## a11y_unknown_role
Unknown role '%role%'
## a11y_unknown_role_suggestion
Unknown role '%role%'. Did you mean '%suggestion%'?
## a11y_no_redundant_roles
Redundant role '%role%'
## a11y_role_has_required_aria_props
Elements with the ARIA role "%role%" must have the following attributes defined: %props%
## a11y_interactive_supports_focus
Elements with the '%role%' interactive role must have a tabindex value.
## a11y_no_interactive_element_to_noninteractive_role
<%element%> cannot have role '%role%'
## a11y_no_noninteractive_element_to_interactive_role
Non-interactive element <%element%> cannot have interactive role '%role%'
## a11y_accesskey
Avoid using accesskey
## a11y_autofocus
Avoid using autofocus
## a11y_misplaced_scope
The scope attribute should only be used with <th> elements
## a11y_positive_tabindex
Avoid tabindex values above zero
## 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_no_noninteractive_tabindex
noninteractive element cannot have nonnegative tabIndex value
## 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_no_noninteractive_element_interactions
Non-interactive element <%element%> should not be assigned mouse or keyboard event listeners.
## a11y_no_static_element_interactions
<%element%> with a %handler% handler must have an ARIA role
## a11y_invalid_attribute
'%href_value%' is not a valid %href_attribute% attribute
## a11y_missing_attribute
<%name%> element should have %article% %sequence% attribute
## a11y_autocomplete_valid
The value '%value%' is not supported by the attribute 'autocomplete' on element <input type="%type%">
## a11y_img_redundant_alt
Screenreaders already announce <img> elements as an image.
## 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_distracting_elements
Avoid <%name%> elements
## a11y_figcaption_parent
`<figcaption>` must be an immediate child of `<figure>`
## a11y_figcaption_index
`<figcaption>` must be first or last child of `<figure>`
## a11y_mouse_events_have_key_events
'%event%' event must be accompanied by '%accompanied_by%' event
## a11y_missing_content
<%name%> element should have child content

@ -0,0 +1,15 @@
## avoid_is
The "is" attribute is not supported cross-browser and should be avoided
## global_event_reference
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
## invalid_html_attribute
'%wrong%' is not a valid HTML attribute. Did you mean '%right%'?

@ -0,0 +1,3 @@
## component_name_lowercase
<%name%> will be treated as an HTML element unless it begins with a capital letter

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

@ -0,0 +1,27 @@
## 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
## unused_export_let
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.
## deprecated_event_handler
Using on:%name% to listen to the %name% event is is deprecated. Use the event attribute on%name% instead.
## deprecated_accessors
The accessors option has been deprecated. It will have no effect in runes mode.
## deprecated_immutable
The immutable option has been deprecated. It will have no effect in runes mode.

@ -0,0 +1,3 @@
## invalid_self_closing_tag
Self-closing HTML tags for non-void elements are ambiguous — use <%name% ...></%name%> rather than <%name% ... />

@ -0,0 +1,3 @@
## missing_custom_element_compile_option
The 'customElement' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?

@ -0,0 +1,7 @@
## 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

@ -0,0 +1,19 @@
## store_with_rune_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
## non_state_reference
`%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?

@ -0,0 +1,7 @@
## static_state_reference
State referenced in its own scope will never update. Did you mean to reference it inside a closure?
## invalid_rest_eachblock_binding
The rest operator (...) will create a new object and binding '%name%' with the original object will not work

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

@ -0,0 +1,223 @@
// @ts-check
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
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');
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}`);
}
seen.add(code);
messages[category][code] = text.trim();
}
}
}
function transform(name, dest) {
const source = fs.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8');
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 message = category[code];
const vars = [];
for (const match of message.matchAll(/%(\w+)%/g)) {
const name = match[1];
if (!vars.includes(name)) {
vars.push(match[1]);
}
}
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 message
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}
if (line.includes('PARAMETER')) {
return vars.map((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;
if (/%\w+%/.test(message)) {
const parts = message.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)
});
}
}
return {
type: 'TemplateLiteral',
expressions,
quasis
};
}
return {
type: 'Literal',
value: 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');

@ -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);
}

@ -1,488 +1,16 @@
/** @typedef {{ start?: number, end?: number }} NodeLike */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
/** @typedef {Record<string, (...args: any[]) => string>} Errors */
/**
* @param {Array<string | number>} items
* @param {string} conjunction
*/
function list(items, conjunction = 'or') {
if (items.length === 1) return items[0];
return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
}
/** @satisfies {Errors} */
const internal = {
/** @param {string} message */
TODO: (message) => `TODO ${message}`,
/** @param {string} message */
INTERNAL: (message) =>
`Internal compiler error: ${message}. Please report this to https://github.com/sveltejs/svelte/issues`
};
/** @satisfies {Errors} */
const parse = {
/** @param {string} name */
'unclosed-element': (name) => `<${name}> was left open`,
'unclosed-block': () => `Block was left open`,
'unexpected-block-close': () => `Unexpected block closing tag`,
/** @param {string} [expected] */
'unexpected-eof': (expected) =>
`Unexpected end of input` + (expected ? ` (expected ${expected})` : ''),
/** @param {string} message */
'js-parse-error': (message) => message,
/** @param {string} token */
'expected-token': (token) => `Expected token ${token}`,
/** @param {string} word */
'unexpected-reserved-word': (word) =>
`'${word}' is a reserved word in JavaScript and cannot be used here`,
'missing-whitespace': () => `Expected whitespace`,
'expected-pattern': () => `Expected identifier or destructure pattern`,
'invalid-script-context': () =>
`If the context attribute is supplied, its value must be "module"`,
'invalid-elseif': () => `'elseif' should be 'else if'`,
'invalid-continuing-block-placement': () =>
`{:...} block is invalid at this position (did you forget to close the preceeding element or block?)`,
/**
* @param {string} child
* @param {string} parent
*/
'invalid-block-missing-parent': (child, parent) => `${child} block must be a child of ${parent}`,
/** @param {string} name */
'duplicate-block-part': (name) => `${name} cannot appear more than once within a block`,
'expected-block-type': () => `Expected 'if', 'each', 'await', 'key' or 'snippet'`,
'expected-identifier': () => `Expected an identifier`,
'invalid-debug': () => `{@debug ...} arguments must be identifiers, not arbitrary expressions`,
'invalid-const': () => `{@const ...} must be an assignment`,
/**
* @param {string} location
* @param {string} name
*/
'invalid-block-placement': (location, name) => `{#${name} ...} block cannot be ${location}`,
/**
* @param {string} location
* @param {string} name
*/
'invalid-tag-placement': (location, name) => `{@${name} ...} tag cannot be ${location}`,
'missing-attribute-value': () => `Expected attribute value`,
/** @param {string} delimiter */
'unclosed-attribute-value': (delimiter) => `Expected closing ${delimiter} character`,
'invalid-directive-value': () =>
`Directive value must be a JavaScript expression enclosed in curly braces`,
/** @param {string} type */
'empty-directive-name': (type) => `${type} name cannot be empty`,
/** @param {string} name */
'invalid-closing-tag': (name) => `</${name}> attempted to close an element that was not open`,
/**
* @param {string} name
* @param {string} reason
*/
'invalid-closing-tag-after-autoclose': (name, reason) =>
`</${name}> attempted to close element that was already automatically closed by <${reason}> (cannot nest <${reason}> inside <${name}>)`,
'invalid-dollar-binding': () =>
`The $ name is reserved, and cannot be used for variables and imports`,
'invalid-dollar-prefix': () =>
`The $ prefix is reserved, and cannot be used for variables and imports`,
'invalid-dollar-global': () =>
`The $ name is reserved. To reference a global variable called $, use globalThis.$`,
'illegal-subscription': () => `Cannot reference store value inside <script context="module">`,
'duplicate-style-element': () => `A component can have a single top-level <style> element`,
'duplicate-script-element': () =>
`A component can have a single top-level <script> element and/or a single top-level <script context="module"> element`,
'invalid-render-expression': () => '{@render ...} tags can only contain call expressions',
'invalid-render-arguments': () => 'expected at most one argument',
'invalid-render-call': () =>
'Calling a snippet function using apply, bind or call is not allowed',
'invalid-render-spread-argument': () => 'cannot use spread arguments in {@render ...} tags',
'invalid-snippet-rest-parameter': () =>
'snippets do not support rest parameters; use an array instead'
};
/** @satisfies {Errors} */
const css = {
/** @param {string} message */
'css-parse-error': (message) => message,
'invalid-css-empty-declaration': () => `Declaration cannot be empty`,
'invalid-css-global-placement': () =>
`:global(...) can be at the start or end of a selector sequence, but not in the middle`,
'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`,
'invalid-css-global-selector-list': () =>
`:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-type-selector-placement': () =>
`:global(...) must not be followed with a type selector`,
'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`,
'invalid-css-declaration': () => 'Declaration cannot be empty'
};
/** @satisfies {Errors} */
const special_elements = {
'invalid-svelte-option-attribute': () => `<svelte:options> can only receive static attributes`,
'invalid-svelte-option-namespace': () =>
`Unsupported <svelte:option> value for "namespace". Valid values are "html", "svg" or "foreign".`,
'tag-option-deprecated': () => `"tag" option is deprecated — use "customElement" instead`,
'invalid-svelte-option-runes': () =>
`Unsupported <svelte:option> value for "runes". Valid values are true or false.`,
'invalid-svelte-option-accessors': () =>
'Unsupported <svelte:option> value for "accessors". Valid values are true or false.',
'invalid-svelte-option-preserveWhitespace': () =>
'Unsupported <svelte:option> value for "preserveWhitespace". Valid values are true or false.',
'invalid-svelte-option-immutable': () =>
'Unsupported <svelte:option> value for "immutable". Valid values are true or false.',
'invalid-tag-property': () => 'tag name must be two or more words joined by the "-" character',
'invalid-svelte-option-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: .. } } }',
'invalid-customElement-props-attribute': () =>
'"props" must be a statically analyzable object literal of the form ' +
'"{ [key: string]: { attribute?: string; reflect?: boolean; type?: "String" | "Boolean" | "Number" | "Array" | "Object" }"',
'invalid-customElement-shadow-attribute': () => '"shadow" must be either "open" or "none"',
'unknown-svelte-option-attribute': /** @param {string} name */ (name) =>
`<svelte:options> unknown attribute '${name}'`,
'illegal-svelte-head-attribute': () => '<svelte:head> cannot have attributes nor directives',
'invalid-svelte-fragment-attribute': () =>
`<svelte:fragment> can only have a slot attribute and (optionally) a let: directive`,
'invalid-svelte-fragment-slot': () => `<svelte:fragment> slot attribute must have a static value`,
'invalid-svelte-fragment-placement': () =>
`<svelte:fragment> must be the direct child of a component`,
/** @param {string} name */
'invalid-svelte-element-placement': (name) =>
`<${name}> tags cannot be inside elements or blocks`,
/** @param {string} name */
'duplicate-svelte-element': (name) => `A component can only have one <${name}> element`,
'invalid-self-placement': () =>
`<svelte:self> components can only exist inside {#if} blocks, {#each} blocks, {#snippet} blocks or slots passed to components`,
'missing-svelte-element-definition': () => `<svelte:element> must have a 'this' attribute`,
'missing-svelte-component-definition': () => `<svelte:component> must have a 'this' attribute`,
'invalid-svelte-element-definition': () => `Invalid element definition — must be an {expression}`,
'invalid-svelte-component-definition': () =>
`Invalid component definition — must be an {expression}`,
/**
* @param {string[]} tags
* @param {string | null} match
*/
'invalid-svelte-tag': (tags, match) =>
`Valid <svelte:...> tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`,
'conflicting-slot-usage': () =>
`Cannot use <slot> syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.`
};
/** @satisfies {Errors} */
const runes = {
'invalid-legacy-props': () => `Cannot use $$props in runes mode`,
'invalid-legacy-rest-props': () => `Cannot use $$restProps in runes mode`,
'invalid-legacy-reactive-statement': () =>
`$: is not allowed in runes mode, use $derived or $effect instead`,
'invalid-legacy-export': () => `Cannot use \`export let\` in runes mode — use $props instead`,
/** @param {string} rune */
'invalid-rune-usage': (rune) => `Cannot use ${rune} rune in non-runes mode`,
'invalid-state-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`,
'invalid-derived-export': () =>
`Cannot export derived state from a module. To expose the current derived value, export a function returning its value`,
'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`,
'invalid-props-pattern': () =>
`$props() assignment must not contain nested properties or computed keys`,
'invalid-props-location': () =>
`$props() can only be used at the top level of components as a variable declaration initializer`,
'invalid-bindable-location': () => `$bindable() can only be used inside a $props() declaration`,
/** @param {string} rune */
'invalid-state-location': (rune) =>
`${rune}(...) can only be used as a variable declaration initializer or a class field`,
'invalid-effect-location': () => `$effect() can only be used as an expression statement`,
'invalid-host-location': () =>
`$host() can only be used inside custom element component instances`,
/**
* @param {boolean} is_binding
* @param {boolean} show_details
*/
'invalid-const-assignment': (is_binding, show_details) =>
`Invalid ${is_binding ? 'binding' : 'assignment'} to const variable${
show_details
? ' ($derived values, let: directives, :then/:catch variables and @const declarations count as const)'
: ''
}`,
'invalid-derived-assignment': () => `Invalid assignment to derived state`,
'invalid-derived-binding': () => `Invalid binding to derived state`,
/**
* @param {string} rune
* @param {Array<number | string>} args
*/
'invalid-rune-args-length': (rune, args) =>
`${rune} can only be called with ${list(args, 'or')} ${
args.length === 1 && args[0] === 1 ? 'argument' : 'arguments'
}`,
/** @param {string} name */
'invalid-runes-mode-import': (name) => `${name} cannot be used in runes mode`,
'duplicate-props-rune': () => `Cannot use $props() more than once`,
'invalid-each-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')`,
'invalid-snippet-assignment': () => `Cannot reassign or bind to snippet parameter`,
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`,
'conflicting-property-name': () =>
`Cannot have a property and a component export with the same name`
};
/** @satisfies {Errors} */
const elements = {
'invalid-textarea-content': () =>
`A <textarea> can have either a value attribute or (equivalently) child content, but not both`,
'invalid-void-content': () => `Void elements cannot have children or closing tags`,
/** @param {string} name */
'invalid-element-content': (name) => `<${name}> cannot have children`,
'invalid-tag-name': () => 'Expected valid tag name',
/**
* @param {string} node
* @param {string} parent
*/
'invalid-node-placement': (node, parent) => `${node} is invalid inside <${parent}>`,
'illegal-title-attribute': () => '<title> cannot have attributes nor directives',
'invalid-title-content': () => '<title> can only contain text and {tags}'
};
/** @satisfies {Errors} */
const components = {
'invalid-component-directive': () => `This type of directive is not valid on components`
};
/** @satisfies {Errors} */
const attributes = {
'empty-attribute-shorthand': () => `Attribute shorthand cannot be empty`,
'duplicate-attribute': () => `Attributes need to be unique`,
'invalid-event-attribute-value': () =>
`Event attribute must be a JavaScript expression, not a string`,
/** @param {string} name */
'invalid-attribute-name': (name) => `'${name}' is not a valid attribute name`,
/** @param {'no-each' | 'each-key' | 'child'} type */
'invalid-animation': (type) =>
type === 'no-each'
? `An element that uses the animate directive must be the immediate child of a keyed each block`
: type === 'each-key'
? `An element that uses the animate directive must be used inside a keyed each block. Did you forget to add a key to your each block?`
: `An element that uses the animate directive must be the sole child of a keyed each block`,
'duplicate-animation': () => `An element can only have one 'animate' directive`,
/** @param {string[] | undefined} [modifiers] */
'invalid-event-modifier': (modifiers) =>
modifiers
? `Valid event modifiers are ${modifiers.slice(0, -1).join(', ')} or ${modifiers.slice(-1)}`
: `Event modifiers other than 'once' can only be used on DOM elements`,
/**
* @param {string} modifier1
* @param {string} modifier2
*/
'invalid-event-modifier-combination': (modifier1, modifier2) =>
`The '${modifier1}' and '${modifier2}' modifiers cannot be used together`,
/**
* @param {string} directive1
* @param {string} directive2
*/
'duplicate-transition': (directive1, directive2) => {
/** @param {string} _directive */
function describe(_directive) {
return _directive === 'transition' ? "a 'transition'" : `an '${_directive}'`;
}
return directive1 === directive2
? `An element can only have one '${directive1}' directive`
: `An element cannot have both ${describe(directive1)} directive and ${describe(
directive2
)} directive`;
},
'invalid-let-directive-placement': () => 'let directive at invalid position',
'invalid-style-directive-modifier': () =>
`Invalid 'style:' modifier. Valid modifiers are: 'important'`,
'invalid-sequence-expression': () =>
`Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses`
};
/** @satisfies {Errors} */
const slots = {
'invalid-slot-element-attribute': () =>
`<slot> can only receive attributes and (optionally) let directives`,
'invalid-slot-attribute': () => `slot attribute must be a static value`,
/** @param {boolean} is_default */
'invalid-slot-name': (is_default) =>
is_default
? `default is a reserved word — it cannot be used as a slot name`
: `slot attribute must be a static value`,
'invalid-slot-placement': () =>
`Element with a slot='...' attribute must be a child of a component or a descendant of a custom element`,
/** @param {string} name @param {string} component */
'duplicate-slot-name': (name, component) => `Duplicate slot name '${name}' in <${component}>`,
'invalid-default-slot-content': () =>
`Found default slot content alongside an explicit slot="default"`,
'conflicting-children-snippet': () =>
`Cannot use explicit children snippet at the same time as implicit children content. Remove either the non-whitespace content or the children snippet block`
};
/** @satisfies {Errors} */
const bindings = {
'invalid-binding-expression': () => `Can only bind to an Identifier or MemberExpression`,
'invalid-binding-value': () => `Can only bind to state or props`,
/**
* @param {string} binding
* @param {string} [elements]
* @param {string} [post]
*/
'invalid-binding': (binding, elements, post = '') =>
(elements
? `'${binding}' binding can only be used with ${elements}`
: `'${binding}' is not a valid binding`) + post,
'invalid-type-attribute': () =>
`'type' attribute must be a static text value if input uses two-way binding`,
'invalid-multiple-attribute': () =>
`'multiple' attribute must be static if select uses two-way binding`,
'missing-contenteditable-attribute': () =>
`'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings`,
'dynamic-contenteditable-attribute': () =>
`'contenteditable' attribute cannot be dynamic if element uses two-way binding`
};
/** @satisfies {Errors} */
const variables = {
'illegal-global': /** @param {string} name */ (name) =>
`${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`,
/** @param {string} name */
'duplicate-declaration': (name) => `'${name}' has already been declared`,
'default-export': () => `A component cannot have a default export`,
'illegal-variable-declaration': () =>
'Cannot declare same variable name which is imported inside <script context="module">',
'illegal-store-subscription': () =>
'Cannot subscribe to stores that are not declared at the top level of the component'
};
/** @satisfies {Errors} */
const legacy_reactivity = {
'cyclical-reactive-declaration': /** @param {string[]} cycle */ (cycle) =>
`Cyclical dependency detected: ${cycle.join(' → ')}`
};
/** @satisfies {Errors} */
const compiler_options = {
/** @param {string} msg */
'invalid-compiler-option': (msg) => `Invalid compiler option: ${msg}`,
/** @param {string} msg */
'removed-compiler-option': (msg) => `Invalid compiler option: ${msg}`
};
/** @satisfies {Errors} */
const const_tag = {
'invalid-const-placement': () =>
`{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment> or <Component>`
};
/** @satisfies {Errors} */
const errors = {
...internal,
...parse,
...css,
...special_elements,
...runes,
...elements,
...components,
...attributes,
...slots,
...bindings,
...variables,
...compiler_options,
...legacy_reactivity,
...const_tag
// missing_contenteditable_attribute: {
// code: 'missing-contenteditable-attribute',
// message:
// "'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings"
// },
// textarea_duplicate_value: {
// code: 'textarea-duplicate-value',
// message:
// 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
// },
// invalid_attribute_head: {
// code: 'invalid-attribute',
// message: '<svelte:head> should not have any attributes or directives'
// },
// invalid_action: {
// code: 'invalid-action',
// message: 'Actions can only be applied to DOM elements, not components'
// },
// invalid_class: {
// code: 'invalid-class',
// message: 'Classes can only be applied to DOM elements, not components'
// },
// invalid_transition: {
// code: 'invalid-transition',
// message: 'Transitions can only be applied to DOM elements, not components'
// },
// invalid_let: {
// code: 'invalid-let',
// message: 'let directive value must be an identifier or an object/array pattern'
// },
// invalid_slot_directive: {
// code: 'invalid-slot-directive',
// message: '<slot> cannot have directives'
// },
// dynamic_slot_name: {
// code: 'dynamic-slot-name',
// message: '<slot> name cannot be dynamic'
// },
// invalid_slot_attribute_value_missing: {
// code: 'invalid-slot-attribute',
// message: 'slot attribute value is missing'
// },
// illegal_structure_title: {
// code: 'illegal-structure',
// message: '<title> can only contain text and {tags}'
// },
// duplicate_transition: /**
// * @param {string} directive
// * @param {string} parent_directive
// */ (directive, parent_directive) => {
// /** @param {string} _directive */
// function describe(_directive) {
// return _directive === 'transition' ? "a 'transition'" : `an '${_directive}'`;
// }
// const message =
// directive === parent_directive
// ? `An element can only have one '${directive}' directive`
// : `An element cannot have both ${describe(parent_directive)} directive and ${describe(
// directive
// )} directive`;
// return {
// code: 'duplicate-transition',
// message
// };
// },
// default_export: {
// code: 'default-export',
// message: 'A component cannot have a default export'
// },
// illegal_declaration: {
// code: 'illegal-declaration',
// message: 'The $ prefix is reserved, and cannot be used for variable and import names'
// },
// invalid_directive_value: {
// code: 'invalid-directive-value',
// message:
// 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
// },
};
/** @typedef {{ start?: number, end?: number }} NodeLike */
// interface is duplicated between here (used internally) and ./interfaces.js // interface is duplicated between here (used internally) and ./interfaces.js
// (exposed publicly), and I'm not sure how to avoid that // (exposed publicly), and I'm not sure how to avoid that
export class CompileError extends Error { export class CompileError extends Error {
name = 'CompileError'; name = 'CompileError';
/** @type {import('#compiler').CompileError['filename']} */ /** @type {import('#compiler').CompileError['filename']} */
filename = undefined; filename = undefined;
/** @type {import('#compiler').CompileError['position']} */ /** @type {import('#compiler').CompileError['position']} */
position = undefined; position = undefined;
/** @type {import('#compiler').CompileError['start']} */ /** @type {import('#compiler').CompileError['start']} */
start = undefined; start = undefined;
/** @type {import('#compiler').CompileError['end']} */ /** @type {import('#compiler').CompileError['end']} */
end = undefined; end = undefined;
@ -516,24 +44,14 @@ export class CompileError extends Error {
} }
/** /**
* @template {Exclude<keyof typeof errors, 'TODO'>} T * @param {null | number | NodeLike} node
* @param {NodeLike | number | null} node * @param {string} code
* @param {T} code * @param {string} message
* @param {Parameters<typeof errors[T]>} args
* @returns {never} * @returns {never}
*/ */
export function error(node, code, ...args) { function e(node, code, message) {
const fn = errors[code];
// @ts-expect-error
const message = fn(...args);
const start = typeof node === 'number' ? node : node?.start; const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end; const end = typeof node === 'number' ? node : node?.end;
throw new CompileError( throw new CompileError(code, message, start !== undefined && end !== undefined ? [start, end] : undefined);
code,
message,
start !== undefined && end !== undefined ? [start, end] : undefined
);
} }

@ -1,5 +1,5 @@
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import { walk } from 'zimmerframe'; import { walk as zimmerframe_walk } from 'zimmerframe';
import { CompileError } from './errors.js'; import { CompileError } from './errors.js';
import { convert } from './legacy.js'; import { convert } from './legacy.js';
import { parse as parse_acorn } from './phases/1-parse/acorn.js'; import { parse as parse_acorn } from './phases/1-parse/acorn.js';
@ -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 { analyze_component, analyze_module } from './phases/2-analyze/index.js';
import { transform_component, transform_module } from './phases/3-transform/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 { validate_component_options, validate_module_options } from './validate-options.js';
import { reset_warnings } from './warnings.js';
export { default as preprocess } from './preprocess/index.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) { export function compile(source, options) {
try { try {
const warnings = reset_warnings({ source, filename: options.filename });
const validated = validate_component_options(options, ''); const validated = validate_component_options(options, '');
let parsed = _parse(source); let parsed = _parse(source);
@ -44,6 +46,7 @@ export function compile(source, options) {
const analysis = analyze_component(parsed, source, combined_options); const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options); const result = transform_component(analysis, source, combined_options);
result.warnings = warnings;
result.ast = to_public_ast(source, parsed, options.modernAst); result.ast = to_public_ast(source, parsed, options.modernAst);
return result; return result;
} catch (e) { } catch (e) {
@ -65,9 +68,12 @@ export function compile(source, options) {
*/ */
export function compileModule(source, options) { export function compileModule(source, options) {
try { try {
const warnings = reset_warnings({ source, filename: options.filename });
const validated = validate_module_options(options, ''); const validated = validate_module_options(options, '');
const analysis = analyze_module(parse_acorn(source, false), validated); 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) { } catch (e) {
if (e instanceof CompileError) { if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source); handle_compile_error(e, options.filename, source);
@ -98,6 +104,32 @@ function handle_compile_error(error, filename, source) {
throw error; 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. * The parse function parses a component, returning only its abstract syntax tree.
* *
@ -133,7 +165,7 @@ export function parse(source, options = {}) {
function to_public_ast(source, ast, modern) { function to_public_ast(source, ast, modern) {
if (modern) { if (modern) {
// remove things that we don't want to treat as public API // remove things that we don't want to treat as public API
return walk(ast, null, { return zimmerframe_walk(ast, null, {
_(node, { next }) { _(node, { next }) {
// @ts-ignore // @ts-ignore
delete node.parent; delete node.parent;
@ -151,14 +183,12 @@ function to_public_ast(source, ast, modern) {
* @deprecated Replace this with `import { walk } from 'estree-walker'` * @deprecated Replace this with `import { walk } from 'estree-walker'`
* @returns {never} * @returns {never}
*/ */
function _walk() { export function walk() {
throw new Error( throw new Error(
`'svelte/compiler' no longer exports a \`walk\` utility — please import it directly from 'estree-walker' instead` `'svelte/compiler' no longer exports a \`walk\` utility — please import it directly from 'estree-walker' instead`
); );
} }
export { _walk as walk };
export { CompileError } from './errors.js'; export { CompileError } from './errors.js';
export { VERSION } from '../version.js'; export { VERSION } from '../version.js';

@ -4,6 +4,7 @@ import {
regex_not_whitespace, regex_not_whitespace,
regex_starts_with_whitespaces regex_starts_with_whitespaces
} from './phases/patterns.js'; } 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. * 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) { ClassDirective(node) {
return { ...node, type: 'Class' }; return { ...node, type: 'Class' };
}, },
ComplexSelector(node, { visit }) { Comment(node) {
return {
...node,
ignores: extract_svelte_ignore(node.data)
};
},
ComplexSelector(node) {
const children = []; const children = [];
for (const child of node.children) { for (const child of node.children) {

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

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

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

@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js'; 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-]+$/; 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) { for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') { if (attribute.type !== 'Attribute') {
error(attribute, 'invalid-svelte-option-attribute'); e.invalid_svelte_option_attribute(attribute);
} }
const { name } = attribute; const { name } = attribute;
switch (name) { switch (name) {
case 'runes': { case 'runes': {
const value = get_static_value(attribute, () => component_options.runes = get_boolean_value(attribute, e.invalid_svelte_option_runes);
error(attribute, 'invalid-svelte-option-runes')
);
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-runes');
}
component_options.runes = value;
break; break;
} }
case 'tag': { case 'tag': {
error(attribute, 'tag-option-deprecated'); e.tag_option_deprecated(attribute);
break; // eslint doesn't know this is unnecessary break; // eslint doesn't know this is unnecessary
} }
case 'customElement': { case 'customElement': {
@ -48,9 +42,9 @@ export default function read_options(node) {
const { value } = attribute; const { value } = attribute;
if (value === true) { if (value === true) {
error(attribute, 'invalid-svelte-option-customElement'); e.invalid_svelte_option_customElement(attribute);
} else if (value[0].type === 'Text') { } else if (value[0].type === 'Text') {
const tag = get_static_value(attribute, () => error(attribute, 'invalid-tag-property')); const tag = get_static_value(attribute, () => e.invalid_tag_property(attribute));
validate_tag(attribute, tag); validate_tag(attribute, tag);
ce.tag = tag; ce.tag = tag;
component_options.customElement = ce; 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) { if (value[0].expression.type === 'Literal' && value[0].expression.value === null) {
break; break;
} }
error(attribute, 'invalid-svelte-option-customElement'); e.invalid_svelte_option_customElement(attribute);
} }
/** @type {Array<[string, any]>} */ /** @type {Array<[string, any]>} */
@ -72,7 +66,7 @@ export default function read_options(node) {
property.computed || property.computed ||
property.key.type !== 'Identifier' property.key.type !== 'Identifier'
) { ) {
error(attribute, 'invalid-svelte-option-customElement'); e.invalid_svelte_option_customElement(attribute);
} }
properties.push([property.key.name, property.value]); properties.push([property.key.name, property.value]);
} }
@ -83,13 +77,13 @@ export default function read_options(node) {
validate_tag(tag, tag_value); validate_tag(tag, tag_value);
ce.tag = tag_value; ce.tag = tag_value;
} else { } else {
error(attribute, 'invalid-svelte-option-customElement'); e.invalid_svelte_option_customElement(attribute);
} }
const props = properties.find(([name]) => name === 'props')?.[1]; const props = properties.find(([name]) => name === 'props')?.[1];
if (props) { if (props) {
if (props.type !== 'ObjectExpression') { if (props.type !== 'ObjectExpression') {
error(attribute, 'invalid-customElement-props-attribute'); e.invalid_customElement_props_attribute(attribute);
} }
ce.props = {}; ce.props = {};
for (const property of /** @type {import('estree').ObjectExpression} */ (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.key.type !== 'Identifier' ||
property.value.type !== 'ObjectExpression' property.value.type !== 'ObjectExpression'
) { ) {
error(attribute, 'invalid-customElement-props-attribute'); e.invalid_customElement_props_attribute(attribute);
} }
ce.props[property.key.name] = {}; ce.props[property.key.name] = {};
for (const prop of property.value.properties) { for (const prop of property.value.properties) {
@ -110,7 +104,7 @@ export default function read_options(node) {
prop.key.type !== 'Identifier' || prop.key.type !== 'Identifier' ||
prop.value.type !== 'Literal' prop.value.type !== 'Literal'
) { ) {
error(attribute, 'invalid-customElement-props-attribute'); e.invalid_customElement_props_attribute(attribute);
} }
if (prop.key.name === 'type') { if (prop.key.name === 'type') {
@ -119,21 +113,21 @@ export default function read_options(node) {
/** @type {string} */ (prop.value.value) /** @type {string} */ (prop.value.value)
) === -1 ) === -1
) { ) {
error(attribute, 'invalid-customElement-props-attribute'); e.invalid_customElement_props_attribute(attribute);
} }
ce.props[property.key.name].type = /** @type {any} */ (prop.value.value); ce.props[property.key.name].type = /** @type {any} */ (prop.value.value);
} else if (prop.key.name === 'reflect') { } else if (prop.key.name === 'reflect') {
if (typeof prop.value.value !== 'boolean') { if (typeof prop.value.value !== 'boolean') {
error(attribute, 'invalid-customElement-props-attribute'); e.invalid_customElement_props_attribute(attribute);
} }
ce.props[property.key.name].reflect = prop.value.value; ce.props[property.key.name].reflect = prop.value.value;
} else if (prop.key.name === 'attribute') { } else if (prop.key.name === 'attribute') {
if (typeof prop.value.value !== 'string') { if (typeof prop.value.value !== 'string') {
error(attribute, 'invalid-customElement-props-attribute'); e.invalid_customElement_props_attribute(attribute);
} }
ce.props[property.key.name].attribute = prop.value.value; ce.props[property.key.name].attribute = prop.value.value;
} else { } else {
error(attribute, 'invalid-customElement-props-attribute'); e.invalid_customElement_props_attribute(attribute);
} }
} }
} }
@ -143,7 +137,7 @@ export default function read_options(node) {
if (shadow) { if (shadow) {
const shadowdom = shadow?.value; const shadowdom = shadow?.value;
if (shadowdom !== 'open' && shadowdom !== 'none') { if (shadowdom !== 'open' && shadowdom !== 'none') {
error(shadow, 'invalid-customElement-shadow-attribute'); e.invalid_customElement_shadow_attribute(shadow);
} }
ce.shadow = shadowdom; ce.shadow = shadowdom;
} }
@ -158,10 +152,10 @@ export default function read_options(node) {
} }
case 'namespace': { case 'namespace': {
const value = get_static_value(attribute, () => const value = get_static_value(attribute, () =>
error(attribute, 'invalid-svelte-option-namespace') e.invalid_svelte_option_namespace(attribute)
); );
if (typeof value !== 'string') { if (typeof value !== 'string') {
error(attribute, 'invalid-svelte-option-namespace'); e.invalid_svelte_option_namespace(attribute);
} }
if (value === namespace_svg) { if (value === namespace_svg) {
@ -169,43 +163,34 @@ export default function read_options(node) {
} else if (value === 'html' || value === 'svg' || value === 'foreign') { } else if (value === 'html' || value === 'svg' || value === 'foreign') {
component_options.namespace = value; component_options.namespace = value;
} else { } else {
error(attribute, 'invalid-svelte-option-namespace'); e.invalid_svelte_option_namespace(attribute);
} }
break; break;
} }
case 'immutable': { case 'immutable': {
const value = get_static_value(attribute, () => component_options.immutable = get_boolean_value(
error(attribute, 'invalid-svelte-option-immutable') attribute,
e.invalid_svelte_option_immutable
); );
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-immutable');
}
component_options.immutable = value;
break; break;
} }
case 'preserveWhitespace': { case 'preserveWhitespace': {
const value = get_static_value(attribute, () => component_options.preserveWhitespace = get_boolean_value(
error(attribute, 'invalid-svelte-option-preserveWhitespace') attribute,
e.invalid_svelte_option_preserveWhitespace
); );
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-preserveWhitespace');
}
component_options.preserveWhitespace = value;
break; break;
} }
case 'accessors': { case 'accessors': {
const value = get_static_value(attribute, () => component_options.accessors = get_boolean_value(
error(attribute, 'invalid-svelte-option-accessors') attribute,
e.invalid_svelte_option_accessors
); );
if (typeof value !== 'boolean') {
error(attribute, 'invalid-svelte-option-accessors');
}
component_options.accessors = value;
break; break;
} }
default: default:
error(attribute, 'unknown-svelte-option-attribute', name); e.unknown_svelte_option_attribute(attribute, name);
} }
} }
@ -230,6 +215,18 @@ function get_static_value(attribute, error) {
return chunk.expression.value; return chunk.expression.value;
} }
/**
* @param {any} attribute
* @param {(attribute: any) => never} error
*/
function get_boolean_value(attribute, error) {
const value = get_static_value(attribute, () => error(attribute));
if (typeof value !== 'boolean') {
error(attribute);
}
return value;
}
/** /**
* @param {any} attribute * @param {any} attribute
* @param {string} tag * @param {string} tag
@ -237,10 +234,10 @@ function get_static_value(attribute, error) {
*/ */
function validate_tag(attribute, tag) { function validate_tag(attribute, tag) {
if (typeof tag !== 'string' && tag !== null) { if (typeof tag !== 'string' && tag !== null) {
error(attribute, 'invalid-tag-property'); e.invalid_tag_property(attribute);
} }
if (tag && !regex_valid_tag_name.test(tag)) { if (tag && !regex_valid_tag_name.test(tag)) {
error(attribute, 'invalid-tag-property'); e.invalid_tag_property(attribute);
} }
// TODO do we still need this? // TODO do we still need this?
// if (tag && !component.compile_options.customElement) { // if (tag && !component.compile_options.customElement) {

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

@ -1,9 +1,8 @@
import { error } from '../../../errors.js'; import * as e from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/; const REGEX_MATCHER = /^[~^$*|]?=/;
const REGEX_CLOSING_BRACKET = /[\s\]]/; const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/; const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF = const REGEX_NTH_OF =
@ -65,7 +64,7 @@ function read_body(parser, close) {
} }
} }
error(parser.template.length, 'expected-token', close); e.expected_token(parser.template.length, close);
} }
/** /**
@ -116,7 +115,8 @@ function read_rule(parser) {
end: parser.index, end: parser.index,
metadata: { metadata: {
parent_rule: null, parent_rule: null,
has_local_selectors: false has_local_selectors: false,
is_global_block: false
} }
}; };
} }
@ -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);
} }
/** /**
@ -252,8 +252,6 @@ function read_selector(parser, inside_pseudo_class = false) {
if (parser.eat('(')) { if (parser.eat('(')) {
args = read_selector_list(parser, true); args = read_selector_list(parser, true);
parser.eat(')', true); parser.eat(')', true);
} else if (name === 'global') {
error(parser.index, 'invalid-css-global-selector');
} }
relative_selector.selectors.push({ relative_selector.selectors.push({
@ -354,7 +352,7 @@ function read_selector(parser, inside_pseudo_class = false) {
if (combinator) { if (combinator) {
if (relative_selector.selectors.length === 0) { if (relative_selector.selectors.length === 0) {
if (!inside_pseudo_class) { if (!inside_pseudo_class) {
error(start, 'invalid-css-selector'); e.invalid_css_selector(start);
} }
} else { } else {
relative_selector.end = index; relative_selector.end = index;
@ -367,12 +365,12 @@ function read_selector(parser, inside_pseudo_class = false) {
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) { if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
error(parser.index, 'invalid-css-selector'); e.invalid_css_selector(parser.index);
} }
} }
} }
error(parser.template.length, 'unexpected-eof'); e.unexpected_eof(parser.template.length);
} }
/** /**
@ -478,7 +476,7 @@ function read_declaration(parser) {
const value = read_value(parser); const value = read_value(parser);
if (!value && !property.startsWith('--')) { if (!value && !property.startsWith('--')) {
error(parser.index, 'invalid-css-declaration'); e.invalid_css_declaration(parser.index);
} }
const end = parser.index; const end = parser.index;
@ -533,7 +531,7 @@ function read_value(parser) {
parser.index++; parser.index++;
} }
error(parser.template.length, 'unexpected-eof'); e.unexpected_eof(parser.template.length);
} }
/** /**
@ -566,7 +564,7 @@ function read_attribute_value(parser) {
parser.index++; parser.index++;
} }
error(parser.template.length, 'unexpected-eof'); e.unexpected_eof(parser.template.length);
} }
/** /**
@ -579,7 +577,7 @@ function read_identifier(parser) {
let identifier = ''; let identifier = '';
if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) { if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
error(start, 'invalid-css-identifier'); e.invalid_css_identifier(start);
} }
let escaped = false; let escaped = false;
@ -604,7 +602,7 @@ function read_identifier(parser) {
} }
if (identifier === '') { if (identifier === '') {
error(start, 'invalid-css-identifier'); e.invalid_css_identifier(start);
} }
return identifier; 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 { is_void } from '../utils/names.js';
import read_expression from '../read/expression.js'; import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js'; import { read_script } from '../read/script.js';
import read_style from '../read/style.js'; import read_style from '../read/style.js';
import { closing_tag_omitted, decode_character_references } from '../utils/html.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_fragment } from '../utils/create.js';
import { create_attribute } from '../../nodes.js'; import { create_attribute } from '../../nodes.js';
@ -87,8 +85,7 @@ export default function tag(parser) {
type: 'Comment', type: 'Comment',
start, start,
end: parser.index, end: parser.index,
data, data
ignores: extract_svelte_ignore(data)
}); });
return; return;
@ -104,19 +101,18 @@ export default function tag(parser) {
['svelte:options', 'svelte:window', 'svelte:body', 'svelte:document'].includes(name) && ['svelte:options', 'svelte:window', 'svelte:body', 'svelte:document'].includes(name) &&
/** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes.length /** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes.length
) { ) {
error( e.invalid_element_content(
/** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes[0].start, /** @type {import('#compiler').ElementLike} */ (parent).fragment.nodes[0].start,
'invalid-element-content',
name name
); );
} }
} else { } else {
if (name in parser.meta_tags) { if (name in parser.meta_tags) {
error(start, 'duplicate-svelte-element', name); e.duplicate_svelte_element(start, name);
} }
if (parent.type !== 'Root') { if (parent.type !== 'Root') {
error(start, 'invalid-svelte-element-placement', name); e.invalid_svelte_element_placement(start, name);
} }
parser.meta_tags[name] = true; parser.meta_tags[name] = true;
@ -168,7 +164,7 @@ export default function tag(parser) {
if (is_closing_tag) { if (is_closing_tag) {
if (is_void(name)) { if (is_void(name)) {
error(start, 'invalid-void-content'); e.invalid_void_content(start);
} }
parser.eat('>', true); parser.eat('>', true);
@ -177,14 +173,9 @@ export default function tag(parser) {
while (/** @type {import('#compiler').RegularElement} */ (parent).name !== name) { while (/** @type {import('#compiler').RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') { if (parent.type !== 'RegularElement') {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) { if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
error( e.invalid_closing_tag_after_autoclose(start, name, parser.last_auto_closed_tag.reason);
start,
'invalid-closing-tag-after-autoclose',
name,
parser.last_auto_closed_tag.reason
);
} else { } else {
error(start, 'invalid-closing-tag', name); e.invalid_closing_tag(start, name);
} }
} }
@ -225,7 +216,7 @@ export default function tag(parser) {
while ((attribute = read(parser))) { while ((attribute = read(parser))) {
if (attribute.type === 'Attribute' || attribute.type === 'BindDirective') { if (attribute.type === 'Attribute' || attribute.type === 'BindDirective') {
if (unique_names.includes(attribute.name)) { if (unique_names.includes(attribute.name)) {
error(attribute.start, 'duplicate-attribute'); e.duplicate_attribute(attribute.start);
// <svelte:element bind:this this=..> is allowed // <svelte:element bind:this this=..> is allowed
} else if (attribute.name !== 'this') { } else if (attribute.name !== 'this') {
unique_names.push(attribute.name); unique_names.push(attribute.name);
@ -242,7 +233,7 @@ export default function tag(parser) {
(attr) => attr.type === 'Attribute' && attr.name === 'this' (attr) => attr.type === 'Attribute' && attr.name === 'this'
); );
if (index === -1) { if (index === -1) {
error(start, 'missing-svelte-component-definition'); e.missing_svelte_component_definition(start);
} }
const definition = /** @type {import('#compiler').Attribute} */ ( const definition = /** @type {import('#compiler').Attribute} */ (
@ -253,7 +244,7 @@ export default function tag(parser) {
definition.value.length !== 1 || definition.value.length !== 1 ||
definition.value[0].type === 'Text' definition.value[0].type === 'Text'
) { ) {
error(definition.start, 'invalid-svelte-component-definition'); e.invalid_svelte_component_definition(definition.start);
} }
element.expression = definition.value[0].expression; element.expression = definition.value[0].expression;
@ -265,14 +256,14 @@ export default function tag(parser) {
(attr) => attr.type === 'Attribute' && attr.name === 'this' (attr) => attr.type === 'Attribute' && attr.name === 'this'
); );
if (index === -1) { if (index === -1) {
error(start, 'missing-svelte-element-definition'); e.missing_svelte_element_definition(start);
} }
const definition = /** @type {import('#compiler').Attribute} */ ( const definition = /** @type {import('#compiler').Attribute} */ (
element.attributes.splice(index, 1)[0] element.attributes.splice(index, 1)[0]
); );
if (definition.value === true || definition.value.length !== 1) { if (definition.value === true || definition.value.length !== 1) {
error(definition.start, 'invalid-svelte-element-definition'); e.invalid_svelte_element_definition(definition.start);
} }
const chunk = definition.value[0]; const chunk = definition.value[0];
element.tag = element.tag =
@ -311,17 +302,17 @@ export default function tag(parser) {
} }
if (content.context === 'module') { if (content.context === 'module') {
if (current.module) error(start, 'duplicate-script-element'); if (current.module) e.duplicate_script_element(start);
current.module = content; current.module = content;
} else { } else {
if (current.instance) error(start, 'duplicate-script-element'); if (current.instance) e.duplicate_script_element(start);
current.instance = content; current.instance = content;
} }
} else { } else {
const content = read_style(parser, start, element.attributes); const content = read_style(parser, start, element.attributes);
content.content.comment = prev_comment; content.content.comment = prev_comment;
if (current.css) error(start, 'duplicate-style-element'); if (current.css) e.duplicate_style_element(start);
current.css = content; current.css = content;
} }
return; return;
@ -396,7 +387,7 @@ function read_tag_name(parser) {
} }
if (!legal) { if (!legal) {
error(start, 'invalid-self-placement'); e.invalid_self_placement(start);
} }
return 'svelte:self'; return 'svelte:self';
@ -412,12 +403,12 @@ function read_tag_name(parser) {
if (meta_tags.has(name)) return name; if (meta_tags.has(name)) return name;
if (name.startsWith('svelte:')) { if (name.startsWith('svelte:')) {
const match = fuzzymatch(name.slice(7), valid_meta_tags); const list = `${valid_meta_tags.slice(0, -1).join(', ')} or ${valid_meta_tags[valid_meta_tags.length - 1]}`;
error(start, 'invalid-svelte-tag', valid_meta_tags, match); e.invalid_svelte_tag(start, list);
} }
if (!valid_tag_name.test(name)) { if (!valid_tag_name.test(name)) {
error(start, 'invalid-tag-name'); e.invalid_tag_name(start);
} }
return name; return name;
@ -445,7 +436,7 @@ function read_static_attribute(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
let raw = parser.match_regex(regex_attribute_value); let raw = parser.match_regex(regex_attribute_value);
if (!raw) { if (!raw) {
error(parser.index, 'missing-attribute-value'); e.missing_attribute_value(parser.index);
} }
parser.index += raw.length; parser.index += raw.length;
@ -468,7 +459,7 @@ function read_static_attribute(parser) {
} }
if (parser.match_regex(regex_starts_with_quote_characters)) { 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); return create_attribute(name, start, parser.index, value);
@ -509,7 +500,7 @@ function read_attribute(parser) {
const name = parser.read_identifier(); const name = parser.read_identifier();
if (name === null) { if (name === null) {
error(start, 'empty-attribute-shorthand'); e.empty_attribute_shorthand(start);
} }
parser.allow_whitespace(); parser.allow_whitespace();
@ -554,14 +545,14 @@ function read_attribute(parser) {
value = read_attribute_value(parser); value = read_attribute_value(parser);
end = parser.index; end = parser.index;
} else if (parser.match_regex(regex_starts_with_quote_characters)) { } else if (parser.match_regex(regex_starts_with_quote_characters)) {
error(parser.index, 'expected-token', '='); e.expected_token(parser.index, '=');
} }
if (type) { if (type) {
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|'); const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
if (directive_name === '') { if (directive_name === '') {
error(start + colon_index + 1, 'empty-directive-name', type); e.empty_directive_name(start + colon_index + 1, type);
} }
if (type === 'StyleDirective') { if (type === 'StyleDirective') {
@ -586,7 +577,7 @@ function read_attribute(parser) {
const attribute_contains_text = const attribute_contains_text =
/** @type {any[]} */ (value).length > 1 || first_value.type === 'Text'; /** @type {any[]} */ (value).length > 1 || first_value.type === 'Text';
if (attribute_contains_text) { if (attribute_contains_text) {
error(/** @type {number} */ (first_value.start), 'invalid-directive-value'); e.invalid_directive_value(/** @type {number} */ (first_value.start));
} else { } else {
expression = first_value.expression; expression = first_value.expression;
} }
@ -679,22 +670,22 @@ function read_attribute_value(parser) {
}, },
'in attribute value' 'in attribute value'
); );
} catch (/** @type {any} e */ e) { } catch (/** @type {any} */ error) {
if (e.code === 'js-parse-error') { if (error.code === 'js_parse_error') {
// if the attribute value didn't close + self-closing tag // if the attribute value didn't close + self-closing tag
// eg: `<Component test={{a:1} />` // eg: `<Component test={{a:1} />`
// acorn may throw a `Unterminated regular expression` because of `/>` // 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) === '/>') { if (pos !== undefined && parser.template.slice(pos - 1, pos + 1) === '/>') {
parser.index = pos; parser.index = pos;
error(pos, 'unclosed-attribute-value', quote_mark || '}'); e.unclosed_attribute_value(pos, quote_mark || '}');
} }
} }
throw e; throw error;
} }
if (value.length === 0 && !quote_mark) { if (value.length === 0 && !quote_mark) {
error(parser.index, 'missing-attribute-value'); e.missing_attribute_value(parser.index);
} }
if (quote_mark) parser.index += 1; if (quote_mark) parser.index += 1;
@ -741,12 +732,12 @@ function read_sequence(parser, done, location) {
const index = parser.index - 1; const index = parser.index - 1;
parser.eat('#'); parser.eat('#');
const name = parser.read_until(/[^a-z]/); const name = parser.read_until(/[^a-z]/);
error(index, 'invalid-block-placement', location, name); e.invalid_block_placement(index, name, location);
} else if (parser.match('@')) { } else if (parser.match('@')) {
const index = parser.index - 1; const index = parser.index - 1;
parser.eat('@'); parser.eat('@');
const name = parser.read_until(/[^a-z]/); const name = parser.read_until(/[^a-z]/);
error(index, 'invalid-tag-placement', location, name); e.invalid_tag_placement(index, name, location);
} }
flush(parser.index - 1); 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_pattern from '../read/context.js';
import read_expression from '../read/expression.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 { create_fragment } from '../utils/create.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -147,7 +147,7 @@ function open(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
index = parser.read_identifier(); index = parser.read_identifier();
if (!index) { if (!index) {
error(parser.index, 'expected-identifier'); e.expected_identifier(parser.index);
} }
parser.allow_whitespace(); parser.allow_whitespace();
@ -265,7 +265,7 @@ function open(parser) {
const name_end = parser.index; const name_end = parser.index;
if (name === null) { if (name === null) {
error(parser.index, 'expected-identifier'); e.expected_identifier(parser.index);
} }
parser.eat('(', true); parser.eat('(', true);
@ -320,7 +320,7 @@ function open(parser) {
return; return;
} }
error(parser.index, 'expected-block-type'); e.expected_block_type(parser.index);
} }
/** @param {import('../index.js').Parser} parser */ /** @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 const block = parser.current(); // TODO type should not be TemplateNode, that's much too broad
if (block.type === 'IfBlock') { if (block.type === 'IfBlock') {
if (!parser.eat('else')) error(start, 'expected-token', '{:else} or {:else if}'); if (!parser.eat('else')) e.expected_token(start, '{:else} or {:else if}');
if (parser.eat('if')) error(start, 'invalid-elseif'); if (parser.eat('if')) e.invalid_elseif(start);
parser.allow_whitespace(); parser.allow_whitespace();
@ -373,7 +373,7 @@ function next(parser) {
} }
if (block.type === 'EachBlock') { 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.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
@ -389,7 +389,7 @@ function next(parser) {
if (block.type === 'AwaitBlock') { if (block.type === 'AwaitBlock') {
if (parser.eat('then')) { if (parser.eat('then')) {
if (block.then) { if (block.then) {
error(start, 'duplicate-block-part', '{:then}'); e.duplicate_block_part(start, '{:then}');
} }
if (!parser.eat('}')) { if (!parser.eat('}')) {
@ -408,7 +408,7 @@ function next(parser) {
if (parser.eat('catch')) { if (parser.eat('catch')) {
if (block.catch) { if (block.catch) {
error(start, 'duplicate-block-part', '{:catch}'); e.duplicate_block_part(start, '{:catch}');
} }
if (!parser.eat('}')) { if (!parser.eat('}')) {
@ -425,10 +425,10 @@ function next(parser) {
return; return;
} }
error(start, 'expected-token', '{:then ...} or {:catch ...}'); e.expected_token(start, '{:then ...} or {:catch ...}');
} }
error(start, 'invalid-continuing-block-placement'); e.invalid_continuing_block_placement(start);
} }
/** @param {import('../index.js').Parser} parser */ /** @param {import('../index.js').Parser} parser */
@ -466,11 +466,11 @@ function close(parser) {
case 'RegularElement': case 'RegularElement':
// TODO handle implicitly closed elements // TODO handle implicitly closed elements
error(start, 'unexpected-block-close'); e.unexpected_block_close(start);
break; break;
default: default:
error(start, 'unexpected-block-close'); e.unexpected_block_close(start);
} }
parser.allow_whitespace(); parser.allow_whitespace();
@ -522,7 +522,7 @@ function special(parser) {
identifiers.forEach( identifiers.forEach(
/** @param {any} node */ (node) => { /** @param {any} node */ (node) => {
if (node.type !== 'Identifier') { if (node.type !== 'Identifier') {
error(/** @type {number} */ (node.start), 'invalid-debug'); e.invalid_debug(/** @type {number} */ (node.start));
} }
} }
); );
@ -583,7 +583,7 @@ function special(parser) {
expression.expression.type !== 'CallExpression' || expression.expression.type !== 'CallExpression' ||
!expression.expression.optional) !expression.expression.optional)
) { ) {
error(expression, 'invalid-render-expression'); e.invalid_render_expression(expression);
} }
parser.allow_whitespace(); parser.allow_whitespace();

@ -7,11 +7,12 @@ import {
regex_starts_with_vowel, regex_starts_with_vowel,
regex_whitespaces regex_whitespaces
} from '../patterns.js'; } from '../patterns.js';
import { warn } from '../../warnings.js'; import * as w from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js'; import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { is_event_attribute, is_text_attribute } from '../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../utils/ast.js';
import { ContentEditableBindings } from '../constants.js'; import { ContentEditableBindings } from '../constants.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { list } from '../../utils/string.js';
const aria_roles = roles_map.keys(); const aria_roles = roles_map.keys();
const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract); const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract);
@ -593,40 +594,53 @@ function is_parent(parent, elements) {
} }
/** /**
* @param {import('#compiler').Attribute} attribute
* @param {import('aria-query').ARIAProperty} name
* @param {import('aria-query').ARIAPropertyDefinition} schema * @param {import('aria-query').ARIAPropertyDefinition} schema
* @param {string | boolean} value * @param {string | true | null} value
* @returns {boolean}
*/ */
function is_valid_aria_attribute_value(schema, value) { function validate_aria_attribute_value(attribute, name, schema, value) {
switch (schema.type) { const type = schema.type;
case 'boolean': const is_string = typeof value === 'string';
return typeof value === 'boolean';
case 'string': if (value === null) return;
case 'id': if (value === true) value = 'true'; // TODO this is actually incorrect, and we should fix it
return typeof value === 'string';
case 'tristate': if (type === 'boolean' && value !== 'true' && value !== 'false') {
return typeof value === 'boolean' || value === 'mixed'; w.a11y_incorrect_aria_attribute_type_boolean(attribute, name);
case 'integer': } else if (type === 'integer' && !Number.isInteger(+value)) {
case 'number': w.a11y_incorrect_aria_attribute_type_integer(attribute, name);
return typeof value !== 'boolean' && isNaN(Number(value)) === false; } else if (type === 'number' && isNaN(+value)) {
case 'token': // single token w.a11y_incorrect_aria_attribute_type(attribute, name, 'number');
return ( } else if ((type === 'string' || type === 'id') && !is_string) {
(schema.values || []).indexOf(typeof value === 'string' ? value.toLowerCase() : value) > -1 w.a11y_incorrect_aria_attribute_type(attribute, name, 'string');
); } else if (type === 'idlist' && !is_string) {
case 'idlist': // if list of ids, split each w.a11y_incorrect_aria_attribute_type_idlist(attribute, name);
return ( } else if (type === 'token') {
typeof value === 'string' && const values = (schema.values ?? []).map((value) => value.toString());
value.split(regex_whitespaces).every((id) => typeof id === 'string') 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 ( } else if (type === 'tokenlist') {
typeof value === 'string' && const values = (schema.values ?? []).map((value) => value.toString());
if (
value value
.toLowerCase()
.split(regex_whitespaces) .split(regex_whitespaces)
.every((token) => (schema.values || []).indexOf(token.toLowerCase()) > -1) .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 +656,8 @@ function warn_missing_attribute(node, attributes, name = node.name) {
attributes.length > 1 attributes.length > 1
? attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` ? attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}`
: attributes[0]; : attributes[0];
return /** @type {const} */ ([node, 'a11y-missing-attribute', name, article, sequence]);
w.a11y_missing_attribute(node, name, article, sequence);
} }
/** /**
@ -667,22 +682,11 @@ function get_static_text_value(attribute) {
/** /**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('./types.js').AnalysisState} state * @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 // foreign namespace means elements can have completely different meanings, therefore we don't check them
if (state.options.namespace === 'foreign') return; 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>} */ /** @type {Map<string, import('#compiler').Attribute>} */
const attribute_map = new Map(); const attribute_map = new Map();
@ -729,28 +733,35 @@ function check_element(node, state, path) {
if (name.startsWith('aria-')) { if (name.startsWith('aria-')) {
if (invisible_elements.includes(node.name)) { if (invisible_elements.includes(node.name)) {
// aria-unsupported-elements // aria-unsupported-elements
push_warning(attribute, 'a11y-aria-attributes', node.name); w.a11y_aria_attributes(attribute, node.name);
} }
const type = name.slice(5); const type = name.slice(5);
if (!aria_attributes.includes(type)) { if (!aria_attributes.includes(type)) {
const match = fuzzymatch(type, aria_attributes); const match = fuzzymatch(type, aria_attributes);
push_warning(attribute, 'a11y-unknown-aria-attribute', type, match); if (match) {
// TODO allow 'overloads' in messages, so that we can use the same code with and without suggestions
w.a11y_unknown_aria_attribute_suggestion(attribute, type, match);
} else {
w.a11y_unknown_aria_attribute(attribute, type);
}
} }
if (name === 'aria-hidden' && regex_heading_tags.test(node.name)) { 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 // aria-proptypes
let value = get_static_value(attribute); 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)); const schema = aria.get(/** @type {import('aria-query').ARIAProperty} */ (name));
if (schema !== undefined && !is_valid_aria_attribute_value(schema, value)) { if (schema !== undefined) {
push_warning(attribute, 'a11y-incorrect-aria-attribute-type', schema, name); validate_aria_attribute_value(
} attribute,
/** @type {import('aria-query').ARIAProperty} */ (name),
schema,
value
);
} }
// aria-activedescendant-has-tabindex // aria-activedescendant-has-tabindex
@ -760,7 +771,7 @@ function check_element(node, state, path) {
!is_interactive_element(node.name, attribute_map) && !is_interactive_element(node.name, attribute_map) &&
!attribute_map.has('tabindex') !attribute_map.has('tabindex')
) { ) {
push_warning(attribute, 'a11y-aria-activedescendant-has-tabindex'); w.a11y_aria_activedescendant_has_tabindex(attribute);
} }
} }
@ -768,7 +779,7 @@ function check_element(node, state, path) {
if (name === 'role') { if (name === 'role') {
if (invisible_elements.includes(node.name)) { if (invisible_elements.includes(node.name)) {
// aria-unsupported-elements // aria-unsupported-elements
push_warning(attribute, 'a11y-misplaced-role', node.name); w.a11y_misplaced_role(attribute, node.name);
} }
const value = get_static_value(attribute); const value = get_static_value(attribute);
@ -778,10 +789,14 @@ function check_element(node, state, path) {
/** @type {import('aria-query').ARIARoleDefinitionKey} current_role */ (c_r); /** @type {import('aria-query').ARIARoleDefinitionKey} current_role */ (c_r);
if (current_role && is_abstract_role(current_role)) { 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)) { } else if (current_role && !aria_roles.includes(current_role)) {
const match = fuzzymatch(current_role, aria_roles); const match = fuzzymatch(current_role, aria_roles);
push_warning(attribute, 'a11y-unknown-role', current_role, match); if (match) {
w.a11y_unknown_role_suggestion(attribute, current_role, match);
} else {
w.a11y_unknown_role(attribute, current_role);
}
} }
// no-redundant-roles // no-redundant-roles
@ -790,7 +805,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 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) !['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. // Footers and headers are special cases, and should not have redundant roles unless they are the children of sections or articles.
@ -799,7 +814,7 @@ function check_element(node, state, path) {
const has_nested_redundant_role = const has_nested_redundant_role =
current_role === a11y_nested_implicit_semantics.get(node.name); current_role === a11y_nested_implicit_semantics.get(node.name);
if (has_nested_redundant_role) { if (has_nested_redundant_role) {
push_warning(attribute, 'a11y-no-redundant-roles', current_role); w.a11y_no_redundant_roles(attribute, current_role);
} }
} }
@ -815,11 +830,13 @@ function check_element(node, state, path) {
(prop) => !attributes.find((a) => a.name === prop) (prop) => !attributes.find((a) => a.name === prop)
); );
if (has_missing_props) { if (has_missing_props) {
push_warning( w.a11y_role_has_required_aria_props(
attribute, attribute,
'a11y-role-has-required-aria-props',
current_role, current_role,
required_role_props list(
required_role_props.map((v) => `"${v}"`),
'and'
)
); );
} }
} }
@ -838,7 +855,7 @@ function check_element(node, state, path) {
a11y_interactive_handlers.includes(handler) a11y_interactive_handlers.includes(handler)
); );
if (has_interactive_handlers) { if (has_interactive_handlers) {
push_warning(node, 'a11y-interactive-supports-focus', current_role); w.a11y_interactive_supports_focus(node, current_role);
} }
} }
@ -847,12 +864,7 @@ function check_element(node, state, path) {
is_interactive_element(node.name, attribute_map) && is_interactive_element(node.name, attribute_map) &&
(is_non_interactive_roles(current_role) || is_presentation_role(current_role)) (is_non_interactive_roles(current_role) || is_presentation_role(current_role))
) { ) {
push_warning( w.a11y_no_interactive_element_to_noninteractive_role(node, node.name, current_role);
node,
'a11y-no-interactive-element-to-noninteractive-role',
current_role,
node.name
);
} }
// no-noninteractive-element-to-interactive-role // no-noninteractive-element-to-interactive-role
@ -863,12 +875,7 @@ function check_element(node, state, path) {
current_role current_role
) )
) { ) {
push_warning( w.a11y_no_noninteractive_element_to_interactive_role(node, node.name, current_role);
node,
'a11y-no-noninteractive-element-to-interactive-role',
current_role,
node.name
);
} }
} }
} }
@ -876,17 +883,17 @@ function check_element(node, state, path) {
// no-access-key // no-access-key
if (name === 'accesskey') { if (name === 'accesskey') {
push_warning(attribute, 'a11y-accesskey'); w.a11y_accesskey(attribute);
} }
// no-autofocus // no-autofocus
if (name === 'autofocus') { if (name === 'autofocus') {
push_warning(attribute, 'a11y-autofocus'); w.a11y_autofocus(attribute);
} }
// scope // scope
if (name === 'scope' && !is_dynamic_element && node.name !== 'th') { if (name === 'scope' && !is_dynamic_element && node.name !== 'th') {
push_warning(attribute, 'a11y-misplaced-scope'); w.a11y_misplaced_scope(attribute);
} }
// tabindex-no-positive // tabindex-no-positive
@ -894,7 +901,7 @@ function check_element(node, state, path) {
const value = get_static_value(attribute); const value = get_static_value(attribute);
// @ts-ignore todo is tabindex=true correct case? // @ts-ignore todo is tabindex=true correct case?
if (!isNaN(value) && +value > 0) { if (!isNaN(value) && +value > 0) {
push_warning(attribute, 'a11y-positive-tabindex'); w.a11y_positive_tabindex(attribute);
} }
} }
} }
@ -918,7 +925,7 @@ function check_element(node, state, path) {
const has_key_event = const has_key_event =
handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress'); handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress');
if (!has_key_event) { if (!has_key_event) {
push_warning(node, 'a11y-click-events-have-key-events'); w.a11y_click_events_have_key_events(node);
} }
} }
} }
@ -936,7 +943,7 @@ function check_element(node, state, path) {
const tab_index = attribute_map.get('tabindex'); const tab_index = attribute_map.get('tabindex');
const tab_index_value = get_static_text_value(tab_index); const tab_index_value = get_static_text_value(tab_index);
if (tab_index && (tab_index_value === null || Number(tab_index_value) >= 0)) { 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 +958,11 @@ function check_element(node, state, path) {
if ( if (
invalid_aria_props.includes(/** @type {import('aria-query').ARIAProperty} */ (attr.name)) invalid_aria_props.includes(/** @type {import('aria-query').ARIAProperty} */ (attr.name))
) { ) {
push_warning( if (is_implicit) {
attr, w.a11y_role_supports_aria_props_implicit(attr, attr.name, role_value, node.name);
'a11y-role-supports-aria-props', } else {
attr.name, w.a11y_role_supports_aria_props(attr, attr.name, role_value);
role_value, }
is_implicit,
node.name
);
} }
} }
} }
@ -976,7 +980,7 @@ function check_element(node, state, path) {
a11y_recommended_interactive_handlers.includes(handler) a11y_recommended_interactive_handlers.includes(handler)
); );
if (has_interactive_handlers) { 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 +999,16 @@ function check_element(node, state, path) {
a11y_interactive_handlers.includes(handler) a11y_interactive_handlers.includes(handler)
); );
if (interactive_handlers.length > 0) { 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')) { 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')) { 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 // element-specific checks
@ -1023,14 +1027,14 @@ function check_element(node, state, path) {
const href_value = get_static_text_value(href); const href_value = get_static_text_value(href);
if (href_value !== null) { if (href_value !== null) {
if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) { 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) { } else if (!has_spread) {
const id_attribute = get_static_value(attribute_map.get('id')); const id_attribute = get_static_value(attribute_map.get('id'));
const name_attribute = get_static_value(attribute_map.get('name')); const name_attribute = get_static_value(attribute_map.get('name'));
if (!id_attribute && !name_attribute) { if (!id_attribute && !name_attribute) {
push_warning(...warn_missing_attribute(node, ['href'])); warn_missing_attribute(node, ['href']);
} }
} }
} else if (!has_spread) { } else if (!has_spread) {
@ -1038,7 +1042,7 @@ function check_element(node, state, path) {
if (required_attributes) { if (required_attributes) {
const has_attribute = required_attributes.some((name) => attribute_map.has(name)); const has_attribute = required_attributes.some((name) => attribute_map.has(name));
if (!has_attribute) { if (!has_attribute) {
push_warning(...warn_missing_attribute(node, required_attributes)); warn_missing_attribute(node, required_attributes);
} }
} }
} }
@ -1050,7 +1054,7 @@ function check_element(node, state, path) {
const required_attributes = ['alt', 'aria-label', 'aria-labelledby']; const required_attributes = ['alt', 'aria-label', 'aria-labelledby'];
const has_attribute = required_attributes.some((name) => attribute_map.has(name)); const has_attribute = required_attributes.some((name) => attribute_map.has(name));
if (!has_attribute) { 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 // autocomplete-valid
@ -1058,7 +1062,11 @@ function check_element(node, state, path) {
if (type && autocomplete) { if (type && autocomplete) {
const autocomplete_value = get_static_value(autocomplete); const autocomplete_value = get_static_value(autocomplete);
if (!is_valid_autocomplete(autocomplete_value)) { 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 +1076,7 @@ function check_element(node, state, path) {
const aria_hidden = get_static_value(attribute_map.get('aria-hidden')); const aria_hidden = get_static_value(attribute_map.get('aria-hidden'));
if (alt_attribute && !aria_hidden) { if (alt_attribute && !aria_hidden) {
if (/\b(image|picture|photo)\b/i.test(alt_attribute)) { 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 +1106,7 @@ function check_element(node, state, path) {
return has; return has;
}; };
if (!attribute_map.has('for') && !has_input_child(node)) { 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 +1128,13 @@ function check_element(node, state, path) {
); );
} }
if (!has_caption) { if (!has_caption) {
push_warning(node, 'a11y-media-has-caption'); w.a11y_media_has_caption(node);
} }
} }
if (node.name === 'figcaption') { if (node.name === 'figcaption') {
if (!is_parent(node.parent, ['figure'])) { if (!is_parent(node.parent, ['figure'])) {
push_warning(node, 'a11y-structure', true); w.a11y_figcaption_parent(node);
} }
} }
@ -1140,13 +1148,13 @@ function check_element(node, state, path) {
(child) => child.type === 'RegularElement' && child.name === 'figcaption' (child) => child.type === 'RegularElement' && child.name === 'figcaption'
); );
if (index !== -1 && index !== 0 && index !== children.length - 1) { 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)) { if (a11y_distracting_elements.includes(node.name)) {
// no-distracting-elements // no-distracting-elements
push_warning(node, 'a11y-distracting-elements', node.name); w.a11y_distracting_elements(node, node.name);
} }
// Check content // Check content
@ -1156,7 +1164,7 @@ function check_element(node, state, path) {
a11y_required_content.includes(node.name) && a11y_required_content.includes(node.name) &&
node.fragment.nodes.length === 0 node.fragment.nodes.length === 0
) { ) {
push_warning(node, 'a11y-missing-content', node.name); w.a11y_missing_content(node, node.name);
} }
} }
@ -1165,9 +1173,9 @@ function check_element(node, state, path) {
*/ */
export const a11y_validators = { export const a11y_validators = {
RegularElement(node, context) { RegularElement(node, context) {
check_element(node, context.state, context.path); check_element(node, context.state);
}, },
SvelteElement(node, context) { SvelteElement(node, context) {
check_element(node, context.state, context.path); check_element(node, context.state);
} }
}; };

@ -1,5 +1,5 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { error } from '../../../errors.js'; import * as e from '../../../errors.js';
import { is_keyframes_node } from '../../css.js'; import { is_keyframes_node } from '../../css.js';
import { merge } from '../../visitors.js'; import { merge } from '../../visitors.js';
@ -69,6 +69,17 @@ const analysis_visitors = {
Rule(node, context) { Rule(node, context) {
node.metadata.parent_rule = context.state.rule; node.metadata.parent_rule = context.state.rule;
// `:global {...}` or `div :global {...}`
node.metadata.is_global_block = node.prelude.children.some((selector) => {
const last = selector.children[selector.children.length - 1];
const s = last.selectors[last.selectors.length - 1];
if (s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null) {
return true;
}
});
context.next({ context.next({
...context.state, ...context.state,
rule: node rule: node
@ -84,6 +95,34 @@ const analysis_visitors = {
/** @type {Visitors} */ /** @type {Visitors} */
const validation_visitors = { const validation_visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
if (node.prelude.children.length > 1) {
e.invalid_css_global_block_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) {
e.invalid_css_global_block_modifier(
relative_selector.selectors[relative_selector.selectors.length - 1]
);
}
if (relative_selector.combinator && relative_selector.combinator.name !== ' ') {
e.invalid_css_global_block_combinator(relative_selector, relative_selector.combinator.name);
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (declaration) {
e.invalid_css_global_block_declaration(declaration);
}
}
context.next();
},
ComplexSelector(node, context) { ComplexSelector(node, context) {
// ensure `:global(...)` is not used in the middle of a selector // ensure `:global(...)` is not used in the middle of a selector
{ {
@ -93,7 +132,7 @@ const validation_visitors = {
if (a !== b) { if (a !== b) {
for (let i = a; i <= b; i += 1) { for (let i = a; i <= b; i += 1) {
if (is_global(node.children[i])) { if (is_global(node.children[i])) {
error(node.children[i].selectors[0], 'invalid-css-global-placement'); e.invalid_css_global_placement(node.children[i].selectors[0]);
} }
} }
} }
@ -108,12 +147,12 @@ const validation_visitors = {
const child = selector.args?.children[0].children[0]; const child = selector.args?.children[0].children[0];
// ensure `:global(element)` to be at the first position in a compound selector // ensure `:global(element)` to be at the first position in a compound selector
if (child?.selectors[0].type === 'TypeSelector' && i !== 0) { if (child?.selectors[0].type === 'TypeSelector' && i !== 0) {
error(selector, 'invalid-css-global-selector-list'); e.invalid_css_global_selector_list(selector);
} }
// ensure `:global(.class)` is not followed by a type selector, eg: `:global(.class)element` // ensure `:global(.class)` is not followed by a type selector, eg: `:global(.class)element`
if (relative_selector.selectors[i + 1]?.type === 'TypeSelector') { if (relative_selector.selectors[i + 1]?.type === 'TypeSelector') {
error(relative_selector.selectors[i + 1], 'invalid-css-type-selector-placement'); e.invalid_css_type_selector_placement(relative_selector.selectors[i + 1]);
} }
// ensure `:global(...)`contains a single selector // ensure `:global(...)`contains a single selector
@ -123,7 +162,7 @@ const validation_visitors = {
selector.args.children.length > 1 && selector.args.children.length > 1 &&
(node.children.length > 1 || relative_selector.selectors.length > 1) (node.children.length > 1 || relative_selector.selectors.length > 1)
) { ) {
error(selector, 'invalid-css-global-selector'); e.invalid_css_global_selector(selector);
} }
} }
} }
@ -132,7 +171,7 @@ const validation_visitors = {
NestingSelector(node, context) { NestingSelector(node, context) {
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule); const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) { if (!rule.metadata.parent_rule) {
error(node, 'invalid-nesting-selector'); e.invalid_nesting_selector(node);
} }
} }
}; };

@ -1,7 +1,6 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js'; import { get_possible_values } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js';
/** /**
* @typedef {{ * @typedef {{
@ -60,6 +59,13 @@ export function prune(stylesheet, element) {
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */ /** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = { const visitors = {
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
},
ComplexSelector(node, context) { ComplexSelector(node, context) {
const selectors = truncate(node); const selectors = truncate(node);
const inner = selectors[selectors.length - 1]; const inner = selectors[selectors.length - 1];

@ -1,16 +1,15 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { warn } from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js'; import { is_keyframes_node } from '../../css.js';
/** /**
* @param {import('#compiler').Css.StyleSheet} stylesheet * @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').RawWarning[]} warnings
*/ */
export function warn_unused(stylesheet, warnings) { export function warn_unused(stylesheet) {
walk(stylesheet, { warnings, stylesheet }, visitors); 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 = { const visitors = {
Atrule(node, context) { Atrule(node, context) {
if (!is_keyframes_node(node)) { if (!is_keyframes_node(node)) {
@ -26,9 +25,16 @@ const visitors = {
if (!node.metadata.used) { if (!node.metadata.used) {
const content = context.state.stylesheet.content; const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start); 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(); context.next();
},
Rule(node, context) {
if (node.metadata.is_global_block) {
context.visit(node.prelude);
} else {
context.next();
}
} }
}; };

@ -1,6 +1,7 @@
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { error } from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { import {
extract_identifiers, extract_identifiers,
extract_all_identifiers_from_expression, 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 { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import { merge } from '../visitors.js'; import { merge } from '../visitors.js';
import { validation_legacy, validation_runes, validation_runes_js } from './validation.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 check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { regex_starts_with_newline } from '../patterns.js'; import { regex_starts_with_newline } from '../patterns.js';
import { create_attribute, is_element_node } from '../nodes.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 { prune } from './css/css-prune.js';
import { hash } from './utils.js'; import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.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 * @param {import('#compiler').Script | null} script
@ -229,20 +230,13 @@ export function analyze_module(ast, options) {
for (const [name, references] of scope.references) { for (const [name, references] of scope.references) {
if (name[0] !== '$' || ReservedKeywords.includes(name)) continue; if (name[0] !== '$' || ReservedKeywords.includes(name)) continue;
if (name === '$' || name[1] === '$') { if (name === '$' || name[1] === '$') {
error(references[0].node, 'illegal-global', name); e.illegal_global(references[0].node, name);
} }
} }
/** @type {import('../types').RawWarning[]} */
const warnings = [];
const analysis = {
warnings
};
walk( walk(
/** @type {import('estree').Node} */ (ast), /** @type {import('estree').Node} */ (ast),
{ scope, analysis }, { scope },
// @ts-expect-error TODO clean this mess up // @ts-expect-error TODO clean this mess up
merge(set_scope(scopes), validation_runes_js, runes_scope_js_tweaker) merge(set_scope(scopes), validation_runes_js, runes_scope_js_tweaker)
); );
@ -250,7 +244,6 @@ export function analyze_module(ast, options) {
return { return {
module: { ast, scope, scopes }, module: { ast, scope, scopes },
name: options.filename || 'module', name: options.filename || 'module',
warnings,
accessors: false, accessors: false,
runes: true, runes: true,
immutable: true immutable: true
@ -274,14 +267,11 @@ export function analyze_component(root, source, options) {
/** @type {import('../types.js').Template} */ /** @type {import('../types.js').Template} */
const template = { ast: root.fragment, scope, scopes }; const template = { ast: root.fragment, scope, scopes };
/** @type {import('../types').RawWarning[]} */
const warnings = [];
// create synthetic bindings for store subscriptions // create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) { for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || ReservedKeywords.includes(name)) continue; if (name[0] !== '$' || ReservedKeywords.includes(name)) continue;
if (name === '$' || name[1] === '$') { if (name === '$' || name[1] === '$') {
error(references[0].node, 'illegal-global', name); e.illegal_global(references[0].node, name);
} }
const store_name = name.slice(1); const store_name = name.slice(1);
@ -321,16 +311,16 @@ export function analyze_component(root, source, options) {
} }
if (is_nested_store_subscription_node) { if (is_nested_store_subscription_node) {
error(is_nested_store_subscription_node, 'illegal-store-subscription'); e.illegal_store_subscription(is_nested_store_subscription_node);
} }
if (options.runes !== false) { if (options.runes !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) { if (declaration === null && /[a-z]/.test(store_name[0])) {
error(references[0].node, 'illegal-global', name); e.illegal_global(references[0].node, name);
} else if (declaration !== null && Runes.includes(/** @type {any} */ (name))) { } else if (declaration !== null && Runes.includes(/** @type {any} */ (name))) {
for (const { node, path } of references) { for (const { node, path } of references) {
if (path.at(-1)?.type === 'CallExpression') { if (path.at(-1)?.type === 'CallExpression') {
warn(warnings, node, [], 'store-with-rune-name', store_name); w.store_with_rune_name(node, store_name);
} }
} }
} }
@ -345,7 +335,7 @@ export function analyze_component(root, source, options) {
// const state = $state(0) is valid // const state = $state(0) is valid
get_rune(/** @type {import('estree').Node} */ (path.at(-1)), module.scope) === null get_rune(/** @type {import('estree').Node} */ (path.at(-1)), module.scope) === null
) { ) {
error(node, 'illegal-subscription'); e.illegal_subscription(node);
} }
} }
} }
@ -390,7 +380,6 @@ export function analyze_component(root, source, options) {
reactive_statements: new Map(), reactive_statements: new Map(),
binding_groups: new Map(), binding_groups: new Map(),
slot_names: new Map(), slot_names: new Map(),
warnings,
css: { css: {
ast: root.css, ast: root.css,
hash: root.css hash: root.css
@ -409,15 +398,15 @@ export function analyze_component(root, source, options) {
if (root.options) { if (root.options) {
for (const attribute of root.options.attributes) { for (const attribute of root.options.attributes) {
if (attribute.name === 'accessors') { if (attribute.name === 'accessors') {
warn(analysis.warnings, attribute, [], 'deprecated-accessors'); w.deprecated_accessors(attribute);
} }
if (attribute.name === 'customElement' && !options.customElement) { if (attribute.name === 'customElement' && !options.customElement) {
warn(analysis.warnings, attribute, [], 'missing-custom-element-compile-option'); w.missing_custom_element_compile_option(attribute);
} }
if (attribute.name === 'immutable') { if (attribute.name === 'immutable') {
warn(analysis.warnings, attribute, [], 'deprecated-immutable'); w.deprecated_immutable(attribute);
} }
} }
} }
@ -425,12 +414,12 @@ export function analyze_component(root, source, options) {
if (analysis.runes) { if (analysis.runes) {
const props_refs = module.scope.references.get('$$props'); const props_refs = module.scope.references.get('$$props');
if (props_refs) { if (props_refs) {
error(props_refs[0].node, 'invalid-legacy-props'); e.invalid_legacy_props(props_refs[0].node);
} }
const rest_props_refs = module.scope.references.get('$$restProps'); const rest_props_refs = module.scope.references.get('$$restProps');
if (rest_props_refs) { if (rest_props_refs) {
error(rest_props_refs[0].node, 'invalid-legacy-rest-props'); e.invalid_legacy_rest_props(rest_props_refs[0].node);
} }
for (const { ast, scope, scopes } of [module, instance, template]) { for (const { ast, scope, scopes } of [module, instance, template]) {
@ -445,7 +434,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
private_derived_state: [], private_derived_state: [],
function_depth: scope.function_depth function_depth: scope.function_depth,
ignores: new Set()
}; };
walk( walk(
@ -463,7 +453,7 @@ export function analyze_component(root, source, options) {
({ alias, name }) => (binding.prop_alias ?? binding.node.name) === (alias ?? name) ({ alias, name }) => (binding.prop_alias ?? binding.node.name) === (alias ?? name)
) )
) { ) {
error(binding.node, 'conflicting-property-name'); e.conflicting_property_name(binding.node);
} }
} }
} }
@ -487,7 +477,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
private_derived_state: [], private_derived_state: [],
function_depth: scope.function_depth function_depth: scope.function_depth,
ignores: new Set()
}; };
walk( walk(
@ -507,7 +498,7 @@ export function analyze_component(root, source, options) {
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier' (r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
); );
if (!references.length && !instance.scope.declarations.has(`$${name}`)) { if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
warn(warnings, binding.node, [], 'unused-export-let', name); w.unused_export_let(binding.node, name);
} }
} }
} }
@ -516,7 +507,7 @@ export function analyze_component(root, source, options) {
} }
if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) { if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) {
error(analysis.slot_names.values().next().value, 'conflicting-slot-usage'); e.conflicting_slot_usage(analysis.slot_names.values().next().value);
} }
// warn on any nonstate declarations that are a) reassigned and b) referenced in the template // warn on any nonstate declarations that are a) reassigned and b) referenced in the template
@ -547,7 +538,7 @@ export function analyze_component(root, source, options) {
type === 'AwaitBlock' || type === 'AwaitBlock' ||
type === 'KeyBlock' type === 'KeyBlock'
) { ) {
warn(warnings, binding.node, [], 'non-state-reference', name); w.non_state_reference(binding.node, name);
continue outer; continue outer;
} }
} }
@ -555,7 +546,7 @@ export function analyze_component(root, source, options) {
} }
} }
warn(warnings, binding.node, [], 'non-state-reference', name); w.non_state_reference(binding.node, name);
continue outer; continue outer;
} }
} }
@ -569,7 +560,13 @@ export function analyze_component(root, source, options) {
for (const element of analysis.elements) { for (const element of analysis.elements) {
prune(analysis.css.ast, element); 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) { outer: for (const element of analysis.elements) {
if (element.metadata.scoped) { if (element.metadata.scoped) {
@ -691,7 +688,7 @@ const legacy_scope_tweaker = {
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const' (d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
) )
) { ) {
warn(state.analysis.warnings, node, path, 'module-script-reactive-declaration'); w.module_script_reactive_declaration(node);
} }
if ( if (
@ -877,7 +874,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 }>} */
const runes_scope_js_tweaker = { const runes_scope_js_tweaker = {
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return; if (node.init?.type !== 'CallExpression') return;
@ -1057,6 +1054,65 @@ const function_visitor = (node, context) => {
/** @type {import('./types').Visitors} */ /** @type {import('./types').Visitors} */
const common_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) { Attribute(node, context) {
if (node.value === true) return; if (node.value === true) return;
@ -1154,7 +1210,7 @@ const common_visitors = {
binding.kind === 'derived') && binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth context.state.function_depth === binding.scope.function_depth
) { ) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference'); w.static_state_reference(node);
} }
} }
}, },
@ -1437,7 +1493,7 @@ function order_reactive_statements(unsorted_reactive_declarations) {
const cycle = check_graph_for_cycles(edges); const cycle = check_graph_for_cycles(edges);
if (cycle?.length) { if (cycle?.length) {
const declaration = /** @type {Tuple[]} */ (lookup.get(cycle[0]))[0]; const declaration = /** @type {Tuple[]} */ (lookup.get(cycle[0]))[0];
error(declaration[0], 'cyclical-reactive-declaration', cycle); e.cyclical_reactive_declaration(declaration[0], cycle.join(' → '));
} }
// We use a map and take advantage of the fact that the spec says insertion order is preserved when iterating // 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; expression: ExpressionTag | ClassDirective | SpreadAttribute | null;
private_derived_state: string[]; private_derived_state: string[];
function_depth: number; function_depth: number;
ignores: Set<string>;
} }
export interface LegacyAnalysisState extends AnalysisState { export interface LegacyAnalysisState extends AnalysisState {

@ -3,7 +3,7 @@ import {
interactive_elements, interactive_elements,
is_tag_valid_with_parent is_tag_valid_with_parent
} from '../../../constants.js'; } from '../../../constants.js';
import { error } from '../../errors.js'; import * as e from '../../errors.js';
import { import {
extract_identifiers, extract_identifiers,
get_parent, get_parent,
@ -12,7 +12,7 @@ import {
object, object,
unwrap_optional unwrap_optional
} from '../../utils/ast.js'; } from '../../utils/ast.js';
import { warn } from '../../warnings.js'; import * as w from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js'; import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { binding_properties } from '../bindings.js'; import { binding_properties } from '../bindings.js';
import { import {
@ -44,14 +44,14 @@ function validate_component(node, context) {
attribute.type !== 'OnDirective' && attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective' attribute.type !== 'BindDirective'
) { ) {
error(attribute, 'invalid-component-directive'); e.invalid_component_directive(attribute);
} }
if ( if (
attribute.type === 'OnDirective' && attribute.type === 'OnDirective' &&
(attribute.modifiers.length > 1 || attribute.modifiers.some((m) => m !== 'once')) (attribute.modifiers.length > 1 || attribute.modifiers.some((m) => m !== 'once'))
) { ) {
error(attribute, 'invalid-event-modifier'); e.invalid_component_event_modifier(attribute);
} }
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
@ -62,12 +62,12 @@ function validate_component(node, context) {
while (--i > 0) { while (--i > 0) {
const char = context.state.analysis.source[i]; const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression'); if (char === '{') e.invalid_sequence_expression(expression);
} }
} }
} }
validate_attribute_name(attribute, context); validate_attribute_name(attribute);
if (attribute.name === 'slot') { if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute); validate_slot_attribute(context, attribute);
@ -93,8 +93,12 @@ const react_attributes = new Map([
*/ */
function validate_element(node, context) { function validate_element(node, context) {
let has_animate_directive = false; 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) { for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
@ -107,18 +111,18 @@ function validate_element(node, context) {
while (--i > 0) { while (--i > 0) {
const char = context.state.analysis.source[i]; const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression'); if (char === '{') e.invalid_sequence_expression(expression);
} }
} }
} }
if (regex_illegal_attribute_character.test(attribute.name)) { if (regex_illegal_attribute_character.test(attribute.name)) {
error(attribute, 'invalid-attribute-name', attribute.name); e.invalid_attribute_name(attribute, attribute.name);
} }
if (attribute.name.startsWith('on') && attribute.name.length > 2) { if (attribute.name.startsWith('on') && attribute.name.length > 2) {
if (!is_expression) { if (!is_expression) {
error(attribute, 'invalid-event-attribute-value'); e.invalid_event_attribute_value(attribute);
} }
const value = attribute.value[0].expression; const value = attribute.value[0].expression;
@ -127,13 +131,7 @@ function validate_element(node, context) {
value.name === attribute.name && value.name === attribute.name &&
!context.state.scope.get(value.name) !context.state.scope.get(value.name)
) { ) {
warn( w.global_event_reference(attribute, attribute.name);
context.state.analysis.warnings,
attribute,
context.path,
'global-event-reference',
attribute.name
);
} }
} }
@ -143,28 +141,21 @@ function validate_element(node, context) {
} }
if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') { if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') {
warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is'); w.avoid_is(attribute);
} }
const correct_name = react_attributes.get(attribute.name); const correct_name = react_attributes.get(attribute.name);
if (correct_name) { if (correct_name) {
warn( w.invalid_html_attribute(attribute, attribute.name, correct_name);
context.state.analysis.warnings,
attribute,
context.path,
'invalid-html-attribute',
attribute.name,
correct_name
);
} }
validate_attribute_name(attribute, context); validate_attribute_name(attribute);
} else if (attribute.type === 'AnimateDirective') { } else if (attribute.type === 'AnimateDirective') {
const parent = context.path.at(-2); const parent = context.path.at(-2);
if (parent?.type !== 'EachBlock') { if (parent?.type !== 'EachBlock') {
error(attribute, 'invalid-animation', 'no-each'); e.animation_invalid_placement(attribute);
} else if (!parent.key) { } else if (!parent.key) {
error(attribute, 'invalid-animation', 'each-key'); e.animation_missing_key(attribute);
} else if ( } else if (
parent.body.nodes.filter( parent.body.nodes.filter(
(n) => (n) =>
@ -173,34 +164,39 @@ function validate_element(node, context) {
(n.type !== 'Text' || n.data.trim() !== '') (n.type !== 'Text' || n.data.trim() !== '')
).length > 1 ).length > 1
) { ) {
error(attribute, 'invalid-animation', 'child'); e.animation_invalid_placement(attribute);
} }
if (has_animate_directive) { if (has_animate_directive) {
error(attribute, 'duplicate-animation'); e.animation_duplicate(attribute);
} else { } else {
has_animate_directive = true; has_animate_directive = true;
} }
} else if (attribute.type === 'TransitionDirective') { } else if (attribute.type === 'TransitionDirective') {
if ((attribute.outro && has_out_transition) || (attribute.intro && has_in_transition)) { const existing = /** @type {import('#compiler').TransitionDirective | null} */ (
/** @param {boolean} _in @param {boolean} _out */ (attribute.intro && in_transition) || (attribute.outro && out_transition)
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)
); );
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; if (attribute.intro) in_transition = attribute;
has_out_transition = has_out_transition || attribute.outro; if (attribute.outro) out_transition = attribute;
} else if (attribute.type === 'OnDirective') { } else if (attribute.type === 'OnDirective') {
let has_passive_modifier = false; let has_passive_modifier = false;
let conflicting_passive_modifier = ''; let conflicting_passive_modifier = '';
for (const modifier of attribute.modifiers) { for (const modifier of attribute.modifiers) {
if (!EventModifiers.includes(modifier)) { if (!EventModifiers.includes(modifier)) {
error(attribute, 'invalid-event-modifier', EventModifiers); const list = `${EventModifiers.slice(0, -1).join(', ')} or ${EventModifiers.at(-1)}`;
e.invalid_event_modifier(attribute, list);
} }
if (modifier === 'passive') { if (modifier === 'passive') {
has_passive_modifier = true; has_passive_modifier = true;
@ -208,12 +204,7 @@ function validate_element(node, context) {
conflicting_passive_modifier = modifier; conflicting_passive_modifier = modifier;
} }
if (has_passive_modifier && conflicting_passive_modifier) { if (has_passive_modifier && conflicting_passive_modifier) {
error( e.invalid_event_modifier_combination(attribute, 'passive', conflicting_passive_modifier);
attribute,
'invalid-event-modifier-combination',
'passive',
conflicting_passive_modifier
);
} }
} }
} }
@ -222,16 +213,15 @@ function validate_element(node, context) {
/** /**
* @param {import('#compiler').Attribute} attribute * @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 ( if (
attribute.name.includes(':') && attribute.name.includes(':') &&
!attribute.name.startsWith('xmlns:') && !attribute.name.startsWith('xmlns:') &&
!attribute.name.startsWith('xlink:') && !attribute.name.startsWith('xlink:') &&
!attribute.name.startsWith('xml:') !attribute.name.startsWith('xml:')
) { ) {
warn(context.state.analysis.warnings, attribute, context.path, 'illegal-attribute-character'); w.illegal_attribute_character(attribute);
} }
} }
@ -259,7 +249,7 @@ function validate_slot_attribute(context, attribute) {
if (owner) { if (owner) {
if (!is_text_attribute(attribute)) { if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-attribute'); e.invalid_slot_attribute(attribute);
} }
if ( if (
@ -268,13 +258,13 @@ function validate_slot_attribute(context, attribute) {
owner.type === 'SvelteSelf' owner.type === 'SvelteSelf'
) { ) {
if (owner !== context.path.at(-2)) { if (owner !== context.path.at(-2)) {
error(attribute, 'invalid-slot-placement'); e.invalid_slot_placement(attribute);
} }
const name = attribute.value[0].data; const name = attribute.value[0].data;
if (context.state.component_slots.has(name)) { if (context.state.component_slots.has(name)) {
error(attribute, 'duplicate-slot-name', name, owner.name); e.duplicate_slot_name(attribute, name, owner.name);
} }
context.state.component_slots.add(name); context.state.component_slots.add(name);
@ -291,12 +281,12 @@ function validate_slot_attribute(context, attribute) {
} }
} }
error(node, 'invalid-default-slot-content'); e.invalid_default_slot_content(node);
} }
} }
} }
} else { } else {
error(attribute, 'invalid-slot-placement'); e.invalid_slot_placement(attribute);
} }
} }
@ -309,7 +299,7 @@ function validate_block_not_empty(node, context) {
// Assumption: If the block has zero elements, someone's in the middle of typing it out, // 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. // 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()) { 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.empty_block(node.nodes[0]);
} }
} }
@ -327,7 +317,7 @@ const validation = {
const left = object(assignee); const left = object(assignee);
if (left === null) { if (left === null) {
error(node, 'invalid-binding-expression'); e.invalid_binding_expression(node);
} }
const binding = context.state.scope.get(left.name); const binding = context.state.scope.get(left.name);
@ -347,36 +337,30 @@ const validation = {
binding.kind !== 'store_sub' && binding.kind !== 'store_sub' &&
!binding.mutated) !binding.mutated)
) { ) {
error(node.expression, 'invalid-binding-value'); e.invalid_binding_value(node.expression);
} }
if (binding.kind === 'derived') { if (binding.kind === 'derived') {
error(node.expression, 'invalid-derived-binding'); e.invalid_binding(node.expression, 'derived state');
} }
if (context.state.analysis.runes && binding.kind === 'each') { if (context.state.analysis.runes && binding.kind === 'each') {
error(node, 'invalid-each-assignment'); e.invalid_each_assignment(node);
} }
if (binding.kind === 'snippet') { if (binding.kind === 'snippet') {
error(node, 'invalid-snippet-assignment'); e.invalid_snippet_assignment(node);
} }
} }
if (node.name === 'group') { if (node.name === 'group') {
if (!binding) { 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) { if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
warn( w.invalid_rest_eachblock_binding(binding.node, binding.node.name);
context.state.analysis.warnings,
binding.node,
context.path,
'invalid-rest-eachblock-binding',
binding.node.name
);
} }
const parent = context.path.at(-1); const parent = context.path.at(-1);
@ -389,21 +373,14 @@ const validation = {
parent?.type === 'SvelteBody' parent?.type === 'SvelteBody'
) { ) {
if (context.state.options.namespace === 'foreign' && node.name !== 'this') { if (context.state.options.namespace === 'foreign' && node.name !== 'this') {
error( e.bind_invalid_detailed(node, node.name, 'Foreign elements only support `bind:this`');
node,
'invalid-binding',
node.name,
undefined,
'. Foreign elements only support bind:this'
);
} }
if (node.name in binding_properties) { if (node.name in binding_properties) {
const property = binding_properties[node.name]; const property = binding_properties[node.name];
if (property.valid_elements && !property.valid_elements.includes(parent.name)) { if (property.valid_elements && !property.valid_elements.includes(parent.name)) {
error( e.bind_invalid_target(
node, node,
'invalid-binding',
node.name, node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ') property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
); );
@ -415,17 +392,17 @@ const validation = {
); );
if (type && !is_text_attribute(type)) { if (type && !is_text_attribute(type)) {
if (node.name !== 'value' || type.value === true) { if (node.name !== 'value' || type.value === true) {
error(type, 'invalid-type-attribute'); e.invalid_type_attribute(type);
} }
return; // bind:value can handle dynamic `type` attributes return; // bind:value can handle dynamic `type` attributes
} }
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') { 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') { 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 +415,13 @@ const validation = {
a.value !== true a.value !== true
); );
if (multiple) { if (multiple) {
error(multiple, 'invalid-multiple-attribute'); e.invalid_multiple_attribute(multiple);
} }
} }
if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) { if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) {
error( e.bind_invalid_target(
node, node,
'invalid-binding',
node.name, node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead` `non-<svg> elements. Use 'clientWidth' for <svg> instead`
); );
@ -456,9 +432,9 @@ const validation = {
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable') parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable')
); );
if (!contenteditable) { if (!contenteditable) {
error(node, 'missing-contenteditable-attribute'); e.missing_contenteditable_attribute(node);
} else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) { } else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) {
error(contenteditable, 'dynamic-contenteditable-attribute'); e.dynamic_contenteditable_attribute(contenteditable);
} }
} }
} else { } else {
@ -466,15 +442,15 @@ const validation = {
if (match) { if (match) {
const property = binding_properties[match]; const property = binding_properties[match];
if (!property.valid_elements || property.valid_elements.includes(parent.name)) { if (!property.valid_elements || property.valid_elements.includes(parent.name)) {
error(node, 'invalid-binding', node.name, undefined, ` (did you mean '${match}'?)`); e.bind_invalid_detailed(node, node.name, `Did you mean '${match}'?`);
} }
} }
error(node, 'invalid-binding', node.name); e.bind_invalid(node, node.name);
} }
} }
}, },
ExportDefaultDeclaration(node) { ExportDefaultDeclaration(node) {
error(node, 'default-export'); e.default_export(node);
}, },
ConstTag(node, context) { ConstTag(node, context) {
const parent = context.path.at(-1); const parent = context.path.at(-1);
@ -491,7 +467,7 @@ const validation = {
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') || ((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot'))) !grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) { ) {
error(node, 'invalid-const-placement'); e.invalid_const_placement(node);
} }
}, },
ImportDeclaration(node, context) { ImportDeclaration(node, context) {
@ -502,7 +478,7 @@ const validation = {
specifier.imported.name === 'beforeUpdate' || specifier.imported.name === 'beforeUpdate' ||
specifier.imported.name === 'afterUpdate' specifier.imported.name === 'afterUpdate'
) { ) {
error(specifier, 'invalid-runes-mode-import', specifier.imported.name); e.invalid_runes_mode_import(specifier, specifier.imported.name);
} }
} }
} }
@ -520,14 +496,14 @@ const validation = {
parent.type !== 'SvelteSelf' && parent.type !== 'SvelteSelf' &&
parent.type !== 'SvelteFragment') parent.type !== 'SvelteFragment')
) { ) {
error(node, 'invalid-let-directive-placement'); e.invalid_let_directive_placement(node);
} }
}, },
RegularElement(node, context) { RegularElement(node, context) {
if (node.name === 'textarea' && node.fragment.nodes.length > 0) { if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') { if (attribute.type === 'Attribute' && attribute.name === 'value') {
error(node, 'invalid-textarea-content'); e.invalid_textarea_content(node);
} }
} }
} }
@ -538,20 +514,14 @@ const validation = {
binding.declaration_kind === 'import' && binding.declaration_kind === 'import' &&
binding.references.length === 0 binding.references.length === 0
) { ) {
warn( w.component_name_lowercase(node, node.name);
context.state.analysis.warnings,
node,
context.path,
'component-name-lowercase',
node.name
);
} }
validate_element(node, context); validate_element(node, context);
if (context.state.parent_element) { if (context.state.parent_element) {
if (!is_tag_valid_with_parent(node.name, 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.invalid_node_placement(node, `<${node.name}>`, context.state.parent_element);
} }
} }
@ -563,7 +533,7 @@ const validation = {
parent.name === node.name && parent.name === node.name &&
interactive_elements.has(parent.name) interactive_elements.has(parent.name)
) { ) {
error(node, 'invalid-node-placement', `<${node.name}>`, parent.name); e.invalid_node_placement(node, `<${node.name}>`, parent.name);
} }
} }
} }
@ -572,7 +542,7 @@ const validation = {
const path = context.path; const path = context.path;
for (let parent of path) { for (let parent of path) {
if (parent.type === 'RegularElement' && parent.name === 'p') { if (parent.type === 'RegularElement' && parent.name === 'p') {
error(node, 'invalid-node-placement', `<${node.name}>`, parent.name); e.invalid_node_placement(node, `<${node.name}>`, parent.name);
} }
} }
} }
@ -583,13 +553,7 @@ const validation = {
!VoidElements.includes(node.name) && !VoidElements.includes(node.name) &&
!SVGElements.includes(node.name) !SVGElements.includes(node.name)
) { ) {
warn( w.invalid_self_closing_tag(node, node.name);
context.state.analysis.warnings,
node,
context.path,
'invalid-self-closing-tag',
node.name
);
} }
context.next({ context.next({
@ -603,7 +567,7 @@ const validation = {
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) { for (const arg of raw_args) {
if (arg.type === 'SpreadElement') { if (arg.type === 'SpreadElement') {
error(arg, 'invalid-render-spread-argument'); e.invalid_render_spread_argument(arg);
} }
} }
@ -613,7 +577,7 @@ const validation = {
callee.property.type === 'Identifier' && callee.property.type === 'Identifier' &&
['bind', 'apply', 'call'].includes(callee.property.name) ['bind', 'apply', 'call'].includes(callee.property.name)
) { ) {
error(node, 'invalid-render-call'); e.invalid_render_call(node);
} }
const is_inside_textarea = context.path.find((n) => { const is_inside_textarea = context.path.find((n) => {
@ -625,9 +589,8 @@ const validation = {
); );
}); });
if (is_inside_textarea) { if (is_inside_textarea) {
error( e.invalid_tag_placement(
node, node,
'invalid-tag-placement',
'inside <textarea> or <svelte:element this="textarea">', 'inside <textarea> or <svelte:element this="textarea">',
'render' 'render'
); );
@ -667,19 +630,19 @@ const validation = {
(node) => node.type !== 'SnippetBlock' && (node.type !== 'Text' || node.data.trim()) (node) => node.type !== 'SnippetBlock' && (node.type !== 'Text' || node.data.trim())
) )
) { ) {
error(node, 'conflicting-children-snippet'); e.conflicting_children_snippet(node);
} }
} }
}, },
StyleDirective(node) { StyleDirective(node) {
if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) { if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) {
error(node, 'invalid-style-directive-modifier'); e.invalid_style_directive_modifier(node);
} }
}, },
SvelteHead(node) { SvelteHead(node) {
const attribute = node.attributes[0]; const attribute = node.attributes[0];
if (attribute) { if (attribute) {
error(attribute, 'illegal-svelte-head-attribute'); e.illegal_svelte_head_attribute(attribute);
} }
}, },
SvelteElement(node, context) { SvelteElement(node, context) {
@ -692,7 +655,7 @@ const validation = {
SvelteFragment(node, context) { SvelteFragment(node, context) {
const parent = context.path.at(-2); const parent = context.path.at(-2);
if (parent?.type !== 'Component' && parent?.type !== 'SvelteComponent') { if (parent?.type !== 'Component' && parent?.type !== 'SvelteComponent') {
error(node, 'invalid-svelte-fragment-placement'); e.invalid_svelte_fragment_placement(node);
} }
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
@ -701,7 +664,7 @@ const validation = {
validate_slot_attribute(context, attribute); validate_slot_attribute(context, attribute);
} }
} else if (attribute.type !== 'LetDirective') { } else if (attribute.type !== 'LetDirective') {
error(attribute, 'invalid-svelte-fragment-attribute'); e.invalid_svelte_fragment_attribute(attribute);
} }
} }
}, },
@ -710,15 +673,15 @@ const validation = {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
if (attribute.name === 'name') { if (attribute.name === 'name') {
if (!is_text_attribute(attribute)) { if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-name', false); e.invalid_slot_name(attribute);
} }
const slot_name = attribute.value[0].data; const slot_name = attribute.value[0].data;
if (slot_name === 'default') { if (slot_name === 'default') {
error(attribute, 'invalid-slot-name', true); e.invalid_slot_name_default(attribute);
} }
} }
} else if (attribute.type !== 'SpreadAttribute' && attribute.type !== 'LetDirective') { } else if (attribute.type !== 'SpreadAttribute' && attribute.type !== 'LetDirective') {
error(attribute, 'invalid-slot-element-attribute'); e.invalid_slot_element_attribute(attribute);
} }
} }
}, },
@ -729,19 +692,19 @@ const validation = {
if (!node.parent) return; if (!node.parent) return;
if (context.state.parent_element && regex_not_whitespace.test(node.data)) { if (context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) { if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', 'Text node', context.state.parent_element); e.invalid_node_placement(node, 'Text node', context.state.parent_element);
} }
} }
}, },
TitleElement(node) { TitleElement(node) {
const attribute = node.attributes[0]; const attribute = node.attributes[0];
if (attribute) { if (attribute) {
error(attribute, 'illegal-title-attribute'); e.illegal_title_attribute(attribute);
} }
const child = node.fragment.nodes.find((n) => n.type !== 'Text' && n.type !== 'ExpressionTag'); const child = node.fragment.nodes.find((n) => n.type !== 'Text' && n.type !== 'ExpressionTag');
if (child) { if (child) {
error(child, 'invalid-title-content'); e.invalid_title_content(child);
} }
}, },
UpdateExpression(node, context) { UpdateExpression(node, context) {
@ -751,7 +714,7 @@ const validation = {
if (!node.parent) return; if (!node.parent) return;
if (context.state.parent_element) { if (context.state.parent_element) {
if (!is_tag_valid_with_parent('#text', 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.invalid_node_placement(node, '{expression}', context.state.parent_element);
} }
} }
} }
@ -772,7 +735,7 @@ export const validation_legacy = merge(validation, a11y_validators, {
} }
if (state.scope.get(callee.name)?.kind !== 'store_sub') { if (state.scope.get(callee.name)?.kind !== 'store_sub') {
error(node.init, 'invalid-rune-usage', callee.name); e.invalid_rune_usage(node.init, callee.name);
} }
}, },
AssignmentExpression(node, { state, path }) { AssignmentExpression(node, { state, path }) {
@ -786,7 +749,7 @@ export const validation_legacy = merge(validation, a11y_validators, {
(state.ast_type !== 'instance' || (state.ast_type !== 'instance' ||
/** @type {import('#compiler').SvelteNode} */ (path.at(-1)).type !== 'Program') /** @type {import('#compiler').SvelteNode} */ (path.at(-1)).type !== 'Program')
) { ) {
warn(state.analysis.warnings, node, path, 'no-reactive-declaration'); w.no_reactive_declaration(node);
} }
}, },
UpdateExpression(node, { state }) { UpdateExpression(node, { state }) {
@ -805,11 +768,11 @@ function validate_export(node, scope, name) {
if (!binding) return; if (!binding) return;
if (binding.kind === 'derived') { if (binding.kind === 'derived') {
error(node, 'invalid-derived-export'); e.invalid_derived_export(node);
} }
if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) { if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
error(node, 'invalid-state-export'); e.invalid_state_export(node);
} }
} }
@ -827,7 +790,7 @@ function validate_call_expression(node, scope, path) {
if (rune === '$props') { if (rune === '$props') {
if (parent.type === 'VariableDeclarator') return; if (parent.type === 'VariableDeclarator') return;
error(node, 'invalid-props-location'); e.invalid_props_location(node);
} }
if (rune === '$bindable') { if (rune === '$bindable') {
@ -840,7 +803,7 @@ function validate_call_expression(node, scope, path) {
return; return;
} }
} }
error(node, 'invalid-bindable-location'); e.invalid_bindable_location(node);
} }
if ( if (
@ -851,46 +814,46 @@ function validate_call_expression(node, scope, path) {
) { ) {
if (parent.type === 'VariableDeclarator') return; if (parent.type === 'VariableDeclarator') return;
if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return;
error(node, 'invalid-state-location', rune); e.invalid_state_location(node, rune);
} }
if (rune === '$effect' || rune === '$effect.pre') { if (rune === '$effect' || rune === '$effect.pre') {
if (parent.type !== 'ExpressionStatement') { if (parent.type !== 'ExpressionStatement') {
error(node, 'invalid-effect-location'); e.invalid_effect_location(node);
} }
if (node.arguments.length !== 1) { if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]); e.invalid_rune_args_length(node, rune, 'exactly one argument');
} }
} }
if (rune === '$effect.active') { if (rune === '$effect.active') {
if (node.arguments.length !== 0) { if (node.arguments.length !== 0) {
error(node, 'invalid-rune-args-length', rune, [0]); e.invalid_rune_args(node, rune);
} }
} }
if (rune === '$effect.root') { if (rune === '$effect.root') {
if (node.arguments.length !== 1) { if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]); e.invalid_rune_args_length(node, rune, 'exactly one argument');
} }
} }
if (rune === '$inspect') { if (rune === '$inspect') {
if (node.arguments.length < 1) { if (node.arguments.length < 1) {
error(node, 'invalid-rune-args-length', rune, [1, 'more']); e.invalid_rune_args_length(node, rune, 'one or more arguments');
} }
} }
if (rune === '$inspect().with') { if (rune === '$inspect().with') {
if (node.arguments.length !== 1) { if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]); e.invalid_rune_args_length(node, rune, 'exactly one argument');
} }
} }
if (rune === '$state.snapshot') { if (rune === '$state.snapshot') {
if (node.arguments.length !== 1) { if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]); e.invalid_rune_args_length(node, rune, 'exactly one argument');
} }
} }
} }
@ -906,7 +869,7 @@ function ensure_no_module_import_conflict(node, state) {
state.scope === state.analysis.instance.scope && state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import' state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) { ) {
error(node.id, 'illegal-variable-declaration'); e.illegal_variable_declaration(node.id);
} }
} }
} }
@ -932,7 +895,7 @@ export const validation_runes_js = {
}, },
CallExpression(node, { state, path }) { CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$host') { if (get_rune(node, state.scope) === '$host') {
error(node, 'invalid-host-location'); e.invalid_host_location(node);
} }
validate_call_expression(node, state.scope, path); validate_call_expression(node, state.scope, path);
}, },
@ -945,13 +908,13 @@ export const validation_runes_js = {
const args = /** @type {import('estree').CallExpression} */ (init).arguments; const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) { if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]); e.invalid_rune_args_length(node, rune, 'exactly one argument');
} else if (rune === '$state' && args.length > 1) { } else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]); e.invalid_rune_args_length(node, rune, 'zero or one arguments');
} else if (rune === '$props') { } else if (rune === '$props') {
error(node, 'invalid-props-location'); e.invalid_props_location(node);
} else if (rune === '$bindable') { } else if (rune === '$bindable') {
error(node, 'invalid-bindable-location'); e.invalid_bindable_location(node);
} }
}, },
AssignmentExpression(node, { state }) { AssignmentExpression(node, { state }) {
@ -989,12 +952,12 @@ export const validation_runes_js = {
const allowed_depth = context.state.ast_type === 'module' ? 0 : 1; const allowed_depth = context.state.ast_type === 'module' ? 0 : 1;
if (context.state.scope.function_depth > allowed_depth) { if (context.state.scope.function_depth > allowed_depth) {
warn(context.state.analysis.warnings, node, context.path, 'avoid-nested-class'); w.avoid_nested_class(node);
} }
}, },
NewExpression(node, context) { NewExpression(node, context) {
if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) { if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) {
warn(context.state.analysis.warnings, node, context.path, 'avoid-inline-class'); w.avoid_inline_class(node);
} }
} }
}; };
@ -1021,16 +984,24 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
} else if (argument.type === 'Identifier') { } else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name); const binding = scope.get(argument.name);
if (binding?.declaration_kind === 'const' && binding.kind !== 'each') { if (binding?.declaration_kind === 'const' && binding.kind !== 'each') {
error( // e.invalid_const_assignment(
node, // node,
'invalid-const-assignment', // is_binding,
is_binding, // // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// 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.
// 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 !== 'state' && // binding.kind !== 'frozen_state' &&
binding.kind !== 'frozen_state' && // (binding.kind !== 'normal' || !binding.initial)
(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.invalid_binding(node, thing);
} else {
e.invalid_assignment(node, thing);
}
} }
} }
} }
@ -1048,16 +1019,16 @@ function validate_assignment(node, argument, state) {
if (state.analysis.runes) { if (state.analysis.runes) {
if (binding?.kind === 'derived') { if (binding?.kind === 'derived') {
error(node, 'invalid-derived-assignment'); e.invalid_assignment(node, 'derived state');
} }
if (binding?.kind === 'each') { if (binding?.kind === 'each') {
error(node, 'invalid-each-assignment'); e.invalid_each_assignment(node);
} }
} }
if (binding?.kind === 'snippet') { if (binding?.kind === 'snippet') {
error(node, 'invalid-snippet-assignment'); e.invalid_snippet_assignment(node);
} }
} }
@ -1073,7 +1044,7 @@ function validate_assignment(node, argument, state) {
if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') {
if (state.private_derived_state.includes(property.name)) { if (state.private_derived_state.includes(property.name)) {
error(node, 'invalid-derived-assignment'); e.invalid_assignment(node, 'derived state');
} }
} }
} }
@ -1081,7 +1052,7 @@ function validate_assignment(node, argument, state) {
export const validation_runes = merge(validation, a11y_validators, { export const validation_runes = merge(validation, a11y_validators, {
LabeledStatement(node, { path }) { LabeledStatement(node, { path }) {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return; if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
error(node, 'invalid-legacy-reactive-statement'); e.invalid_legacy_reactive_statement(node);
}, },
ExportNamedDeclaration(node, { state, next }) { ExportNamedDeclaration(node, { state, next }) {
if (state.ast_type === 'module') { if (state.ast_type === 'module') {
@ -1099,7 +1070,7 @@ export const validation_runes = merge(validation, a11y_validators, {
if (node.declaration?.type !== 'VariableDeclaration') return; if (node.declaration?.type !== 'VariableDeclaration') return;
if (node.declaration.kind !== 'let') return; if (node.declaration.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return; if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export'); e.invalid_legacy_export(node);
} }
}, },
ExportSpecifier(node, { state }) { ExportSpecifier(node, { state }) {
@ -1110,12 +1081,12 @@ export const validation_runes = merge(validation, a11y_validators, {
CallExpression(node, { state, path }) { CallExpression(node, { state, path }) {
const rune = get_rune(node, state.scope); const rune = get_rune(node, state.scope);
if (rune === '$bindable' && node.arguments.length > 1) { if (rune === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]); e.invalid_rune_args_length(node, '$bindable', 'zero or one arguments');
} else if (rune === '$host') { } else if (rune === '$host') {
if (node.arguments.length > 0) { if (node.arguments.length > 0) {
error(node, 'invalid-rune-args-length', '$host', [0]); e.invalid_rune_args(node, '$host');
} else if (state.ast_type === 'module' || !state.analysis.custom_element) { } else if (state.ast_type === 'module' || !state.analysis.custom_element) {
error(node, 'invalid-host-location'); e.invalid_host_location(node);
} }
} }
@ -1127,11 +1098,11 @@ export const validation_runes = merge(validation, a11y_validators, {
context.type === 'Identifier' && context.type === 'Identifier' &&
(context.name === '$state' || context.name === '$derived') (context.name === '$state' || context.name === '$derived')
) { ) {
error(node, 'invalid-state-location', context.name); e.invalid_state_location(node, context.name);
} }
next({ ...state }); next({ ...state });
}, },
VariableDeclarator(node, { state, path }) { VariableDeclarator(node, { state }) {
ensure_no_module_import_conflict(node, state); ensure_no_module_import_conflict(node, state);
const init = node.init; const init = node.init;
@ -1139,7 +1110,7 @@ export const validation_runes = merge(validation, a11y_validators, {
if (rune === null) { if (rune === null) {
if (init?.type === 'Identifier' && init.name === '$props' && !state.scope.get('props')) { if (init?.type === 'Identifier' && init.name === '$props' && !state.scope.get('props')) {
warn(state.analysis.warnings, node, path, 'invalid-props-declaration'); w.invalid_props_declaration(node);
} }
return; return;
} }
@ -1148,39 +1119,39 @@ export const validation_runes = merge(validation, a11y_validators, {
// TODO some of this is duplicated with above, seems off // TODO some of this is duplicated with above, seems off
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) { if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]); e.invalid_rune_args_length(node, rune, 'exactly one argument');
} else if (rune === '$state' && args.length > 1) { } else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]); e.invalid_rune_args_length(node, rune, 'zero or one arguments');
} else if (rune === '$props') { } else if (rune === '$props') {
if (state.has_props_rune) { if (state.has_props_rune) {
error(node, 'duplicate-props-rune'); e.duplicate_props_rune(node);
} }
state.has_props_rune = true; state.has_props_rune = true;
if (args.length > 0) { if (args.length > 0) {
error(node, 'invalid-rune-args-length', rune, [0]); e.invalid_rune_args(node, rune);
} }
if (node.id.type !== 'ObjectPattern') { if (node.id.type !== 'ObjectPattern') {
error(node, 'invalid-props-id'); e.invalid_props_id(node);
} }
if (state.scope !== state.analysis.instance.scope) { if (state.scope !== state.analysis.instance.scope) {
error(node, 'invalid-props-location'); e.invalid_props_location(node);
} }
for (const property of node.id.properties) { for (const property of node.id.properties) {
if (property.type === 'Property') { if (property.type === 'Property') {
if (property.computed) { if (property.computed) {
error(property, 'invalid-props-pattern'); e.invalid_props_pattern(property);
} }
const value = const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value; property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') { if (value.type !== 'Identifier') {
error(property, 'invalid-props-pattern'); e.invalid_props_pattern(property);
} }
} }
} }
@ -1192,29 +1163,29 @@ export const validation_runes = merge(validation, a11y_validators, {
arg.type === 'CallExpression' && arg.type === 'CallExpression' &&
(arg.callee.type === 'ArrowFunctionExpression' || arg.callee.type === 'FunctionExpression') (arg.callee.type === 'ArrowFunctionExpression' || arg.callee.type === 'FunctionExpression')
) { ) {
warn(state.analysis.warnings, node, path, 'derived-iife'); w.derived_iife(node);
} }
} }
}, },
AssignmentPattern(node, { state, path }) { AssignmentPattern(node, { state }) {
if ( if (
node.right.type === 'Identifier' && node.right.type === 'Identifier' &&
node.right.name === '$bindable' && node.right.name === '$bindable' &&
!state.scope.get('bindable') !state.scope.get('bindable')
) { ) {
warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration'); w.invalid_bindable_declaration(node);
} }
}, },
SlotElement(node, { state, path }) { SlotElement(node, { state }) {
if (!state.analysis.custom_element) { if (!state.analysis.custom_element) {
warn(state.analysis.warnings, node, path, 'deprecated-slot-element'); w.deprecated_slot_element(node);
} }
}, },
OnDirective(node, { state, path }) { OnDirective(node, { path }) {
const parent_type = path.at(-1)?.type; 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 // 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') { if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
warn(state.analysis.warnings, node, path, 'deprecated-event-handler', node.name); w.deprecated_event_handler(node, node.name);
} }
}, },
// TODO this is a code smell. need to refactor this stuff // TODO this is a code smell. need to refactor this stuff

@ -1,5 +1,4 @@
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { error } from '../../../errors.js';
import * as b from '../../../utils/builders.js'; import * as b from '../../../utils/builders.js';
import { set_scope } from '../../scope.js'; import { set_scope } from '../../scope.js';
import { template_visitors } from './visitors/template.js'; import { template_visitors } from './visitors/template.js';
@ -52,35 +51,41 @@ export function client_component(source, analysis, options) {
get before_init() { get before_init() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => a.push = () => {
error(null, 'INTERNAL', 'before_init.push should not be called outside create_block'); throw new Error('before_init.push should not be called outside create_block');
};
return a; return a;
}, },
get init() { get init() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; 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; return a;
}, },
get update() { get update() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => a.push = () => {
error(null, 'INTERNAL', 'update.push should not be called outside create_block'); throw new Error('update.push should not be called outside create_block');
};
return a; return a;
}, },
get after_update() { get after_update() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => a.push = () => {
error(null, 'INTERNAL', 'after_update.push should not be called outside create_block'); throw new Error('after_update.push should not be called outside create_block');
};
return a; return a;
}, },
get template() { get template() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => a.push = () => {
error(null, 'INTERNAL', 'template.push should not be called outside create_block'); throw new Error('template.push should not be called outside create_block');
};
return a; return a;
}, },
legacy_reactive_statements: new Map(), legacy_reactive_statements: new Map(),
@ -209,7 +214,7 @@ export function client_component(source, analysis, options) {
for (const [node] of analysis.reactive_statements) { for (const [node] of analysis.reactive_statements) {
const statement = [...state.legacy_reactive_statements].find(([n]) => n === node); const statement = [...state.legacy_reactive_statements].find(([n]) => n === node);
if (statement === undefined) { if (statement === undefined) {
error(node, 'INTERNAL', 'Could not find reactive statement'); throw new Error('Could not find reactive statement');
} }
instance.body.push(statement[1]); instance.body.push(statement[1]);
} }
@ -255,14 +260,24 @@ export function client_component(source, analysis, options) {
); );
if (analysis.runes && options.dev) { 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) { for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') { if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name)); bindable.push(b.literal(binding.prop_alias ?? name));
} }
} }
instance.body.unshift( 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}`)
)
)
); );
} }

@ -5,7 +5,6 @@ import {
is_simple_expression, is_simple_expression,
object object
} from '../../../utils/ast.js'; } from '../../../utils/ast.js';
import { error } from '../../../errors.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
@ -185,7 +184,7 @@ export function serialize_set_binding(node, context, fallback, options) {
} }
if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') { 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 // Handle class private/public state assignment cases

@ -222,7 +222,8 @@ export const javascript_visitors_runes = {
if (rune === '$props') { if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern'); 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) { for (const property of declarator.id.properties) {
if (property.type === 'Property') { if (property.type === 'Property') {

@ -16,7 +16,6 @@ import {
import { DOMProperties, PassiveEvents, VoidElements } from '../../../constants.js'; import { DOMProperties, PassiveEvents, VoidElements } from '../../../constants.js';
import { is_custom_element_node, is_element_node } from '../../../nodes.js'; import { is_custom_element_node, is_element_node } from '../../../nodes.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { error } from '../../../../errors.js';
import { import {
with_loc, with_loc,
function_visitor, function_visitor,
@ -1776,10 +1775,10 @@ export const template_visitors = {
); );
}, },
ClassDirective(node, { state, next }) { 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 }) { 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 }) { TransitionDirective(node, { state, visit }) {
let flags = node.modifiers.includes('global') ? TRANSITION_GLOBAL : 0; let flags = node.modifiers.includes('global') ? TRANSITION_GLOBAL : 0;
@ -2727,7 +2726,9 @@ export const template_visitors = {
case 'checked': case 'checked':
call_expr = b.call(`$.bind_checked`, state.node, getter, setter); call_expr = b.call(`$.bind_checked`, state.node, getter, setter);
break; break;
case 'focused':
call_expr = b.call(`$.bind_focused`, state.node, setter);
break;
case 'group': { case 'group': {
/** @type {import('estree').CallExpression[]} */ /** @type {import('estree').CallExpression[]} */
const indexes = []; const indexes = [];
@ -2773,7 +2774,7 @@ export const template_visitors = {
} }
default: default:
error(node, 'INTERNAL', 'unknown binding ' + node.name); throw new Error('unknown binding ' + node.name);
} }
} }

@ -116,7 +116,7 @@ const visitors = {
} }
} }
}, },
Rule(node, { state, next }) { Rule(node, { state, next, visit }) {
// keep empty rules in dev, because it's convenient to // keep empty rules in dev, because it's convenient to
// see them in devtools // see them in devtools
if (!state.dev && is_empty(node)) { if (!state.dev && is_empty(node)) {
@ -134,6 +134,26 @@ const visitors = {
return; return;
} }
if (node.metadata.is_global_block) {
const selector = node.prelude.children[0];
if (selector.children.length === 1) {
// `:global {...}`
state.code.prependRight(node.start, '/* ');
state.code.appendLeft(node.block.start + 1, '*/');
state.code.prependRight(node.block.end - 1, '/*');
state.code.appendLeft(node.block.end, '*/');
// don't recurse into selector or body
return;
}
// don't recurse into body
visit(node.prelude);
return;
}
next(); next();
}, },
SelectorList(node, { state, next, path }) { SelectorList(node, { state, next, path }) {
@ -275,6 +295,10 @@ const visitors = {
/** @param {import('#compiler').Css.Rule} rule */ /** @param {import('#compiler').Css.Rule} rule */
function is_empty(rule) { function is_empty(rule) {
if (rule.metadata.is_global_block) {
return rule.block.children.length === 0;
}
for (const child of rule.block.children) { for (const child of rule.block.children) {
if (child.type === 'Declaration') { if (child.type === 'Declaration') {
return false; return false;

@ -17,7 +17,7 @@ export function transform_component(analysis, source, options) {
return { return {
js: /** @type {any} */ (null), js: /** @type {any} */ (null),
css: null, css: null,
warnings: transform_warnings(source, options.filename, analysis.warnings), warnings: /** @type {any} */ (null), // set afterwards
metadata: { metadata: {
runes: analysis.runes runes: analysis.runes
}, },
@ -60,7 +60,7 @@ export function transform_component(analysis, source, options) {
return { return {
js, js,
css, css,
warnings: transform_warnings(source, options.filename, analysis.warnings), // TODO apply preprocessor sourcemap warnings: /** @type {any} */ (null), // set afterwards. TODO apply preprocessor sourcemap
metadata: { metadata: {
runes: analysis.runes runes: analysis.runes
}, },
@ -79,7 +79,7 @@ export function transform_module(analysis, source, options) {
return { return {
js: /** @type {any} */ (null), js: /** @type {any} */ (null),
css: null, css: null,
warnings: transform_warnings(source, analysis.name, analysis.warnings), warnings: /** @type {any} */ (null), // set afterwards
metadata: { metadata: {
runes: true runes: true
}, },
@ -105,45 +105,10 @@ export function transform_module(analysis, source, options) {
return { return {
js: print(program, {}), js: print(program, {}),
css: null, css: null,
warnings: transform_warnings(source, analysis.name, analysis.warnings),
metadata: { metadata: {
runes: true runes: true
}, },
warnings: /** @type {any} */ (null), // set afterwards
ast: /** @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 transform_inspect_rune
} from '../utils.js'; } from '../utils.js';
import { create_attribute, is_custom_element_node, is_element_node } from '../../nodes.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 { binding_properties } from '../../bindings.js';
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js'; import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
import { import {
@ -419,7 +418,7 @@ function serialize_set_binding(node, context, fallback) {
} }
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { 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; let left = node.left;
@ -692,8 +691,7 @@ const javascript_visitors_runes = {
} }
if (rune === '$props') { if (rune === '$props') {
// remove $bindable() from props declaration and handle rest props // remove $bindable() from props declaration
let uses_rest_props = false;
const id = walk(declarator.id, null, { const id = walk(declarator.id, null, {
AssignmentPattern(node) { AssignmentPattern(node) {
if ( if (
@ -705,26 +703,9 @@ const javascript_visitors_runes = {
: b.id('undefined'); : b.id('undefined');
return b.assignment_pattern(node.left, right); return b.assignment_pattern(node.left, right);
} }
},
RestElement(node, { path }) {
if (path.at(-1) === declarator.id) {
uses_rest_props = true;
}
} }
}); });
declarations.push(b.declarator(id, b.id('$$props')));
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')
)
);
continue; continue;
} }
@ -1329,10 +1310,10 @@ const template_visitors = {
state.template.push(block_close); state.template.push(block_close);
}, },
ClassDirective(node) { ClassDirective(node) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere'); throw new Error('Node should have been handled elsewhere');
}, },
StyleDirective(node) { StyleDirective(node) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere'); throw new Error('Node should have been handled elsewhere');
}, },
RegularElement(node, context) { RegularElement(node, context) {
const metadata = { const metadata = {
@ -2124,14 +2105,17 @@ export function server_component(analysis, options) {
get init() { get init() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; 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; return a;
}, },
get template() { get template() {
/** @type {any[]} */ /** @type {any[]} */
const a = []; const a = [];
a.push = () => a.push = () => {
error(null, 'INTERNAL', 'template.push should not be called outside create_block'); throw new Error('template.push should not be called outside create_block');
};
return a; return a;
}, },
metadata: { metadata: {
@ -2199,7 +2183,7 @@ export function server_component(analysis, options) {
for (const [node] of analysis.reactive_statements) { for (const [node] of analysis.reactive_statements) {
const statement = [...state.legacy_reactive_statements].find(([n]) => n === node); const statement = [...state.legacy_reactive_statements].find(([n]) => n === node);
if (statement === undefined) { if (statement === undefined) {
error(node, 'INTERNAL', 'Could not find reactive statement'); throw new Error('Could not find reactive statement');
} }
if ( if (

@ -21,6 +21,7 @@ export const binding_properties = {
event: 'durationchange', event: 'durationchange',
omit_in_ssr: true omit_in_ssr: true
}, },
focused: {},
paused: { paused: {
valid_elements: ['audio', 'video'], valid_elements: ['audio', 'video'],
omit_in_ssr: true omit_in_ssr: true

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

@ -6,7 +6,8 @@ import type {
SlotElement, SlotElement,
SvelteElement, SvelteElement,
SvelteNode, SvelteNode,
SvelteOptions SvelteOptions,
Warning
} from '#compiler'; } from '#compiler';
import type { Identifier, LabeledStatement, Program } from 'estree'; import type { Identifier, LabeledStatement, Program } from 'estree';
import type { Scope, ScopeRoot } from './scope.js'; import type { Scope, ScopeRoot } from './scope.js';
@ -28,19 +29,12 @@ export interface ReactiveStatement {
dependencies: Binding[]; dependencies: Binding[];
} }
export interface RawWarning {
code: string;
message: string;
position: [number, number] | undefined;
}
/** /**
* Analysis common to modules and components * Analysis common to modules and components
*/ */
export interface Analysis { export interface Analysis {
module: Js; module: Js;
name: string; // TODO should this be filename? it's used in `compileModule` as well as `compile` name: string; // TODO should this be filename? it's used in `compileModule` as well as `compile`
warnings: RawWarning[];
runes: boolean; runes: boolean;
immutable: boolean; immutable: boolean;

@ -33,6 +33,7 @@ export namespace Css {
metadata: { metadata: {
parent_rule: null | Rule; parent_rule: null | Rule;
has_local_selectors: boolean; has_local_selectors: boolean;
is_global_block: boolean;
}; };
} }

@ -198,6 +198,14 @@ export interface LegacyWindow extends BaseElement {
type: 'Window'; 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 = type LegacyDirective =
| LegacyAnimation | LegacyAnimation
| LegacyBinding | LegacyBinding
@ -213,6 +221,7 @@ export type LegacyAttributeLike = LegacyAttribute | LegacySpread | LegacyDirecti
export type LegacyElementLike = export type LegacyElementLike =
| LegacyBody | LegacyBody
| LegacyCatchBlock | LegacyCatchBlock
| LegacyComment
| LegacyDocument | LegacyDocument
| LegacyElement | LegacyElement
| LegacyHead | LegacyHead

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

@ -1,12 +1,10 @@
import { error } from '../errors.js';
/** /**
* @template T * @template T
* @param {T} value * @param {T} value
* @returns {asserts value is NonNullable<T>} * @returns {asserts value is NonNullable<T>}
*/ */
export function ok(value) { 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} * @returns {asserts actual is T}
*/ */
export function equal(actual, expected) { 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) .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,4 @@
import { error } from './errors.js'; import * as e from './errors.js';
/** /**
* @template [Input=any] * @template [Input=any]
@ -144,7 +144,7 @@ export const validate_component_options =
function removed(msg) { function removed(msg) {
return (input) => { return (input) => {
if (input !== undefined) { if (input !== undefined) {
error(null, 'removed-compiler-option', msg); e.removed_compiler_option(null, msg);
} }
return /** @type {any} */ (undefined); return /** @type {any} */ (undefined);
}; };
@ -203,9 +203,8 @@ function object(children, allow_unknown = false) {
if (allow_unknown) { if (allow_unknown) {
output[key] = input[key]; output[key] = input[key];
} else { } else {
error( e.invalid_compiler_option(
null, null,
'invalid-compiler-option',
`Unexpected option ${keypath ? `${keypath}.${key}` : key}` `Unexpected option ${keypath ? `${keypath}.${key}` : key}`
); );
} }
@ -310,5 +309,5 @@ function fun(fallback) {
/** @param {string} msg */ /** @param {string} msg */
function throw_error(msg) { function throw_error(msg) {
error(null, 'invalid-compiler-option', msg); e.invalid_compiler_option(null, msg);
} }

@ -1,340 +1,41 @@
import { /* This file is generated by scripts/process-messages/index.js. Do not edit! */
extract_ignores_above_position,
extract_svelte_ignore_from_comments
} from './utils/extract_svelte_ignore.js';
/** @typedef {Record<string, (...args: any[]) => string>} Warnings */ import { getLocator } from 'locate-character';
/** @satisfies {Warnings} */ /** @typedef {{ start?: number, end?: number }} NodeLike */
const css = { /** @type {import('#compiler').Warning[]} */
/** @param {string} name */ let warnings = [];
'css-unused-selector': (name) => `Unused CSS selector "${name}"` /** @type {string | undefined} */
}; let filename;
let locator = getLocator('', { offsetLine: 1 });
/** @satisfies {Warnings} */ /**
const attributes = { * @param {{
'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided', * source: string;
/** @param {string} name */ * filename: string | undefined;
'global-event-reference': (name) => * }} options
`You are referencing globalThis.${name}. Did you forget to declare a variable with that name?`, * @returns {import('#compiler').Warning[]}
'illegal-attribute-character': () => */
"Attributes should not contain ':' characters to prevent ambiguity with Svelte directives", export function reset_warnings(options) {
/** filename = options.filename;
* @param {string} wrong locator = getLocator(options.source, { offsetLine: 1 });
* @param {string} right return warnings = [];
*/ }
'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.`,
'deprecated-accessors': () =>
`The accessors option has been deprecated. It will have no effect in runes mode.`,
'deprecated-immutable': () =>
`The immutable option has been deprecated. It will have no effect in runes mode.`
};
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 {null | NodeLike} node
* @param {import('./phases/types').RawWarning[]} array the array to push the warning to, if not ignored * @param {string} code
* @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 {string} message
* @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) { function w(node, code, message) {
const fn = warnings[code]; // @ts-expect-error
if (node.ignores?.has(code)) return;
// 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({ warnings.push({
code, code,
// @ts-expect-error message,
message: fn(...args), filename,
position: start !== undefined && end !== undefined ? [start, end] : undefined start: node?.start !== undefined ? locator(node.start) : undefined,
end: node?.end !== undefined ? locator(node.end) : undefined
}); });
} }

@ -21,13 +21,13 @@ export function onMount(fn) {
throw new Error('onMount can only be used during component initialisation.'); throw new Error('onMount can only be used during component initialisation.');
} }
if (current_component_context.r) { if (current_component_context.l !== null) {
init_update_callbacks(current_component_context).m.push(fn);
} else {
user_effect(() => { user_effect(() => {
const cleanup = untrack(fn); const cleanup = untrack(fn);
if (typeof cleanup === 'function') return /** @type {() => void} */ (cleanup); if (typeof cleanup === 'function') return /** @type {() => void} */ (cleanup);
}); });
} else {
init_update_callbacks(current_component_context).m.push(fn);
} }
} }
@ -129,7 +129,7 @@ export function beforeUpdate(fn) {
throw new Error('beforeUpdate can only be used during component initialisation'); throw new Error('beforeUpdate can only be used during component initialisation');
} }
if (current_component_context.r) { if (current_component_context.l === null) {
throw new Error('beforeUpdate cannot be used in runes mode'); throw new Error('beforeUpdate cannot be used in runes mode');
} }
@ -153,7 +153,7 @@ export function afterUpdate(fn) {
throw new Error('afterUpdate can only be used during component initialisation.'); throw new Error('afterUpdate can only be used during component initialisation.');
} }
if (current_component_context.r) { if (current_component_context.l === null) {
throw new Error('afterUpdate cannot be used in runes mode'); throw new Error('afterUpdate cannot be used in runes mode');
} }
@ -162,10 +162,11 @@ export function afterUpdate(fn) {
/** /**
* Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate * 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) { 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,8 @@
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are). // 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. * @deprecated Svelte components were classes in Svelte 4. In Svelte 5, thy are not anymore.
* Use `mount` or `createRoot` instead to instantiate components. * Use `mount` or `createRoot` instead to instantiate components.
@ -18,12 +21,36 @@ export interface ComponentConstructorOptions<
$$inline?: boolean; $$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 /** Tooling for types uses this for properties are being used with `bind:` */
type PropsWithChildren<Props, Slots> = Props & export type Binding<T> = { 'bind:': T };
(Props extends { children?: any } /**
? {} * Tooling for types uses this for properties that may be bound to.
: Slots extends { default: any } * Only use this if you author Svelte component type definition files by hand (we recommend using `@sveltejs/package` instead).
? { children?: Snippet } * 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 }
: {}); : {});
/** /**
@ -55,7 +82,7 @@ type PropsWithChildren<Props, Slots> = Props &
* for more info. * for more info.
*/ */
export class SvelteComponent< export class SvelteComponent<
Props extends Record<string, any> = any, Props extends Record<string, any> = Record<string, any>,
Events extends Record<string, any> = any, Events extends Record<string, any> = any,
Slots extends Record<string, any> = any Slots extends Record<string, any> = any
> { > {
@ -74,7 +101,7 @@ export class SvelteComponent<
* Does not exist at runtime. * Does not exist at runtime.
* ### DO NOT USE! * ### DO NOT USE!
* */ * */
$$prop_def: PropsWithChildren<Props, Slots>; $$prop_def: RemoveBindable<Props>; // Without PropsWithChildren: unnecessary, causes type bugs
/** /**
* For type checking capabilities only. * For type checking capabilities only.
* Does not exist at runtime. * Does not exist at runtime.
@ -119,7 +146,7 @@ export class SvelteComponent<
* @deprecated Use `SvelteComponent` instead. See TODO for more information. * @deprecated Use `SvelteComponent` instead. See TODO for more information.
*/ */
export class SvelteComponentTyped< export class SvelteComponentTyped<
Props extends Record<string, any> = any, Props extends Record<string, any> = Record<string, any>,
Events extends Record<string, any> = any, Events extends Record<string, any> = any,
Slots extends Record<string, any> = any Slots extends Record<string, any> = any
> extends SvelteComponent<Props, Events, Slots> {} > extends SvelteComponent<Props, Events, Slots> {}
@ -154,7 +181,7 @@ export type ComponentEvents<Comp extends SvelteComponent> =
* ``` * ```
*/ */
export type ComponentProps<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 * 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'; export * from './index-client.js';
import './ambient.js';

@ -20,15 +20,17 @@ export function if_block(
elseif = false elseif = false
) { ) {
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
let consequent_effect = null; var consequent_effect = null;
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
let alternate_effect = null; var alternate_effect = null;
/** @type {boolean | null} */ /** @type {boolean | null} */
let condition = null; var condition = null;
const effect = block(() => { var flags = elseif ? EFFECT_TRANSPARENT : 0;
block(() => {
if (condition === (condition = !!get_condition())) return; if (condition === (condition = !!get_condition())) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@ -76,9 +78,5 @@ export function if_block(
// continue in hydration mode // continue in hydration mode
set_hydrating(true); set_hydrating(true);
} }
}); }, flags);
if (elseif) {
effect.f |= EFFECT_TRANSPARENT;
}
} }

@ -1,5 +1,5 @@
import { EFFECT_TRANSPARENT } from '../../constants.js'; import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, render_effect } from '../../reactivity/effects.js'; import { branch, block, destroy_effect } from '../../reactivity/effects.js';
/** /**
* @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn * @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn
@ -12,13 +12,19 @@ export function snippet(get_snippet, node, ...args) {
/** @type {SnippetFn | null | undefined} */ /** @type {SnippetFn | null | undefined} */
var snippet; var snippet;
var effect = render_effect(() => { /** @type {import('#client').Effect | null} */
var snippet_effect;
block(() => {
if (snippet === (snippet = get_snippet())) return; if (snippet === (snippet = get_snippet())) return;
if (snippet) { if (snippet_effect) {
branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args)); destroy_effect(snippet_effect);
snippet_effect = null;
} }
});
effect.f |= EFFECT_TRANSPARENT; if (snippet) {
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args));
}
}, EFFECT_TRANSPARENT);
} }

@ -40,7 +40,7 @@ function swap_block_dom(effect, from, to) {
* @param {Comment} anchor * @param {Comment} anchor
* @param {() => string} get_tag * @param {() => string} get_tag
* @param {boolean} is_svg * @param {boolean} is_svg
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn, * @param {undefined | ((element: Element, anchor: Node | null) => void)} render_fn,
* @param {undefined | (() => string)} get_namespace * @param {undefined | (() => string)} get_namespace
* @returns {void} * @returns {void}
*/ */
@ -115,14 +115,12 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace) {
? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild)) ? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild))
: element.appendChild(empty()); : element.appendChild(empty());
if (child_anchor) { // `child_anchor` is undefined if this is a void element, but we still
// `child_anchor` can be undefined if this is a void element with children, // need to call `render_fn` in order to run actions etc. If the element
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is // contains children, it's a user error (which is warned on elsewhere)
// user error, but we warn on it elsewhere (in dev) so here we just // and the DOM will be silently discarded
// silently ignore it
render_fn(element, child_anchor); render_fn(element, child_anchor);
} }
}
anchor.before(element); anchor.before(element);

@ -111,7 +111,8 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
var events = []; var events = [];
for (key in next) { 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; if (value === prev?.[key]) continue;
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2) var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
@ -119,8 +120,8 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
if (prefix === 'on') { if (prefix === 'on') {
/** @type {{ capture?: true }} */ /** @type {{ capture?: true }} */
var opts = {}; const opts = {};
var event_name = key.slice(2); let event_name = key.slice(2);
var delegated = DelegatedEvents.includes(event_name); var delegated = DelegatedEvents.includes(event_name);
if ( if (

@ -1,4 +1,5 @@
import { render_effect } from '../../../reactivity/effects.js'; import { render_effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/** /**
* @param {'innerHTML' | 'textContent' | 'innerText'} property * @param {'innerHTML' | 'textContent' | 'innerText'} property
@ -67,3 +68,14 @@ export function bind_property(property, event_name, type, element, get_value, up
} }
}); });
} }
/**
* @param {HTMLElement} element
* @param {(value: unknown) => void} update
* @returns {void}
*/
export function bind_focused(element, update) {
listen(element, ['focus', 'blur'], () => {
update(element === document.activeElement);
});
}

@ -122,13 +122,15 @@ export function handle_event_propagation(handler_element, event) {
} }
}); });
while (current_target !== null) { /** @param {Element} current_target */
function next(current_target) {
/** @type {null | Element} */ /** @type {null | Element} */
var parent_element = var parent_element =
current_target.parentNode || /** @type {any} */ (current_target).host || null; current_target.parentNode || /** @type {any} */ (current_target).host || null;
var internal_prop_name = '__' + event_name;
// @ts-ignore try {
var delegated = current_target[internal_prop_name]; // @ts-expect-error
var delegated = current_target['__' + event_name];
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) { if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
if (is_array(delegated)) { if (is_array(delegated)) {
@ -138,20 +140,24 @@ export function handle_event_propagation(handler_element, event) {
delegated.call(current_target, event); delegated.call(current_target, event);
} }
} }
} finally {
if ( if (
event.cancelBubble || !event.cancelBubble &&
parent_element === handler_element || parent_element !== handler_element &&
current_target === handler_element parent_element !== null &&
current_target !== handler_element
) { ) {
break; next(parent_element);
}
} }
current_target = parent_element;
} }
try {
next(current_target);
} finally {
// @ts-expect-error is used above // @ts-expect-error is used above
event.__root = handler_element; event.__root = handler_element;
// @ts-expect-error is used above // @ts-expect-error is used above
current_target = handler_element; current_target = handler_element;
}
} }

@ -7,7 +7,7 @@ import { should_intro } from '../../render.js';
import { is_function } from '../../utils.js'; import { is_function } from '../../utils.js';
import { current_each_item } from '../blocks/each.js'; import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN } from '../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
/** /**
* @template T * @template T
@ -241,18 +241,26 @@ export function transition(flags, element, get_fn, get_params) {
(e.transitions ??= []).push(transition); (e.transitions ??= []).push(transition);
// if this is a local transition, we only want to run it if the parent (block) effect's // if this is a local transition, we only want to run it if the parent (branch) effect's
// parent (branch) effect is where the state change happened. we can determine that by // parent (block) effect is where the state change happened. we can determine that by
// looking at whether the branch effect is currently initializing // looking at whether the block effect is currently initializing
if (is_intro && should_intro) { if (is_intro && should_intro) {
var parent = /** @type {import('#client').Effect} */ (e.parent); let run = is_global;
// e.g snippets are implemented as render effects — keep going until we find the parent block if (!run) {
while ((parent.f & BLOCK_EFFECT) === 0 && parent.parent) { var block = /** @type {import('#client').Effect | null} */ (e.parent);
parent = parent.parent;
// skip over transparent blocks (e.g. snippets, else-if blocks)
while (block && (block.f & EFFECT_TRANSPARENT) !== 0) {
while ((block = block.parent)) {
if ((block.f & BLOCK_EFFECT) !== 0) break;
}
}
run = !block || (block.f & EFFECT_RAN) !== 0;
} }
if (is_global || (parent.f & EFFECT_RAN) !== 0) { if (run) {
effect(() => { effect(() => {
untrack(() => transition.in()); untrack(() => transition.in());
}); });

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

@ -44,7 +44,11 @@ export { bind_prop } from './dom/elements/bindings/props.js';
export { bind_select_value, init_select, select_option } from './dom/elements/bindings/select.js'; export { bind_select_value, init_select, select_option } from './dom/elements/bindings/select.js';
export { bind_element_size, bind_resize_observer } from './dom/elements/bindings/size.js'; export { bind_element_size, bind_resize_observer } from './dom/elements/bindings/size.js';
export { bind_this } from './dom/elements/bindings/this.js'; export { bind_this } from './dom/elements/bindings/this.js';
export { bind_content_editable, bind_property } from './dom/elements/bindings/universal.js'; export {
bind_content_editable,
bind_property,
bind_focused
} from './dom/elements/bindings/universal.js';
export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js'; export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js';
export { export {
once, once,

@ -182,11 +182,11 @@ export function effect(fn) {
* @param {() => void | (() => void)} fn * @param {() => void | (() => void)} fn
*/ */
export function legacy_pre_effect(deps, 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 }} */ /** @type {{ effect: null | import('#client').Effect, ran: boolean }} */
var token = { effect: null, ran: false }; var token = { effect: null, ran: false };
context.l1.push(token); context.l.r1.push(token);
token.effect = render_effect(() => { token.effect = render_effect(() => {
deps(); deps();
@ -196,19 +196,19 @@ export function legacy_pre_effect(deps, fn) {
if (token.ran) return; if (token.ran) return;
token.ran = true; token.ran = true;
set(context.l2, true); set(context.l.r2, true);
untrack(fn); untrack(fn);
}); });
} }
export function legacy_pre_effect_reset() { 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(() => { render_effect(() => {
if (!get(context.l2)) return; if (!get(context.l.r2)) return;
// Run dirty `$:` statements // Run dirty `$:` statements
for (var token of context.l1) { for (var token of context.l.r1) {
var effect = token.effect; var effect = token.effect;
if (check_dirtiness(effect)) { if (check_dirtiness(effect)) {
@ -218,7 +218,7 @@ export function legacy_pre_effect_reset() {
token.ran = false; 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
}); });
} }
@ -230,9 +230,12 @@ export function render_effect(fn) {
return create_effect(RENDER_EFFECT, fn, true); return create_effect(RENDER_EFFECT, fn, true);
} }
/** @param {(() => void)} fn */ /**
export function block(fn) { * @param {(() => void)} fn
return create_effect(RENDER_EFFECT | BLOCK_EFFECT, fn, true); * @param {number} flags
*/
export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
} }
/** @param {(() => void)} fn */ /** @param {(() => void)} fn */

@ -55,8 +55,8 @@ export function mutable_source(initial_value) {
// bind the signal to the component context, in case we need to // bind the signal to the component context, in case we need to
// track updates to trigger beforeUpdate/afterUpdate callbacks // track updates to trigger beforeUpdate/afterUpdate callbacks
if (current_component_context) { if (current_component_context !== null && current_component_context.l !== null) {
(current_component_context.d ??= []).push(s); (current_component_context.l.s ??= []).push(s);
} }
return s; return s;

@ -92,7 +92,7 @@ export function stringify(value) {
* @param {{ * @param {{
* target: Document | Element | ShadowRoot; * target: Document | Element | ShadowRoot;
* anchor?: Node; * anchor?: Node;
* props?: Props; * props?: import('../types.js').RemoveBindable<Props>;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any }; * events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>; * context?: Map<any, any>;
* intro?: boolean; * intro?: boolean;
@ -114,7 +114,7 @@ export function mount(component, options) {
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component * @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
* @param {{ * @param {{
* target: Document | Element | ShadowRoot; * target: Document | Element | ShadowRoot;
* props?: Props; * props?: import('../types.js').RemoveBindable<Props>;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any }; * events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>; * context?: Map<any, any>;
* intro?: boolean; * intro?: boolean;
@ -181,24 +181,19 @@ export function hydrate(component, options) {
} }
/** /**
* @template {Record<string, any>} Props
* @template {Record<string, any>} Exports * @template {Record<string, any>} Exports
* @template {Record<string, any>} Events * @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>>} Component
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} Component
* @param {{ * @param {{
* target: Document | Element | ShadowRoot; * target: Document | Element | ShadowRoot;
* anchor: Node; * anchor: Node;
* props?: Props; * props?: any;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any }; * events?: any;
* context?: Map<any, any>; * context?: Map<any, any>;
* intro?: boolean; * intro?: boolean;
* }} options * }} options
* @returns {Exports} * @returns {Exports}
*/ */
function _mount( function _mount(Component, { target, anchor, props = {}, events, context, intro = false }) {
Component,
{ target, anchor, props = /** @type {Props} */ ({}), events, context, intro = false }
) {
init_operations(); init_operations();
const registered_events = new Set(); const registered_events = new Set();

@ -115,7 +115,7 @@ export function set_current_component_context(context) {
/** @returns {boolean} */ /** @returns {boolean} */
export function is_runes() { export function is_runes() {
return current_component_context !== null && current_component_context.r; return current_component_context !== null && current_component_context.l === null;
} }
/** /**
@ -1043,29 +1043,24 @@ export async function value_or_fallback_async(value, fallback) {
*/ */
export function push(props, runes = false, fn) { export function push(props, runes = false, fn) {
current_component_context = { current_component_context = {
// exports (and props, if `accessors: true`) p: current_component_context,
x: null,
// context
c: null, c: null,
// effects
e: null, e: null,
// mounted
m: false, m: false,
// parent
p: current_component_context,
// signals
d: null,
// props
s: props, s: props,
// runes x: null,
r: runes, l: null
// legacy $:
l1: [],
l2: source(false),
// update_callbacks
u: null
}; };
if (!runes) {
current_component_context.l = {
s: null,
u: null,
r1: [],
r2: source(false)
};
}
if (DEV) { if (DEV) {
// component function // component function
// @ts-expect-error // @ts-expect-error

@ -1,3 +1,4 @@
import type { Bindable, Binding } from '../../index.js';
import type { Store } from '#shared'; import type { Store } from '#shared';
import { STATE_SYMBOL } from './constants.js'; import { STATE_SYMBOL } from './constants.js';
import type { Effect, Source, Value } from './reactivity/types.js'; import type { Effect, Source, Value } from './reactivity/types.js';
@ -10,26 +11,31 @@ export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>;
// when the JS VM JITs the code. // when the JS VM JITs the code.
export type ComponentContext = { 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 */ /** parent */
p: null | ComponentContext; p: null | ComponentContext;
/** context */ /** context */
c: null | Map<unknown, unknown>; c: null | Map<unknown, unknown>;
/** runes */ /** deferred effects */
r: boolean; e: null | Array<() => void | (() => void)>;
/** legacy mode: if `$:` statements are allowed to run (ensures they only run once per render) */ /** mounted */
l1: any[]; m: boolean;
/** legacy mode: if `$:` statements are allowed to run (ensures they only run once per render) */ /**
l2: Source<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 */ /** update_callbacks */
u: null | { u: null | {
/** afterUpdate callbacks */ /** afterUpdate callbacks */
@ -39,6 +45,15 @@ export type ComponentContext = {
/** onMount callbacks */ /** onMount callbacks */
m: Array<() => any>; 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 Equals = (this: Value, value: unknown) => boolean;

@ -85,16 +85,26 @@ export function loop_guard(timeout) {
/** /**
* @param {Record<string, any>} $$props * @param {Record<string, any>} $$props
* @param {string[]} bindable * @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) { 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 (setter) {
if (exports.includes(key)) {
throw new Error(
`Component ${component.filename} 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}).`
);
}
if (!bindable.includes(key)) {
throw new Error( throw new Error(
`Cannot use bind:${key} on this component because the property was not declared as bindable. ` + `A component is binding to property ${key} of ${name}.svelte (i.e. <${name} bind:${key} />). This is disallowed because the property was not declared as bindable inside ${component.filename}. ` +
`To mark a property as bindable, use the $bindable() rune like this: \`let { ${key} = $bindable() } = $props()\`` `To mark a property as bindable, use the $bindable() rune in ${name}.svelte like this: \`let { ${key} = $bindable() } = $props()\``
); );
} }
} }

@ -1,2 +1,8 @@
import type { Bindable } from '../index.js';
/** Anything except a function */ /** Anything except a function */
export type NotFunction<T> = T extends Function ? never : T; 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];
};

@ -7,6 +7,7 @@ import { map } from './utils.js';
/** /**
* @template K * @template K
* @template V * @template V
* @extends {Map<K, V>}
*/ */
export class ReactiveMap extends Map { export class ReactiveMap extends Map {
/** @type {Map<K, import('#client').Source<V>>} */ /** @type {Map<K, import('#client').Source<V>>} */

@ -10,6 +10,7 @@ var inited = false;
/** /**
* @template T * @template T
* @extends {Set<T>}
*/ */
export class ReactiveSet extends Set { export class ReactiveSet extends Set {
/** @type {Map<T, import('#client').Source<boolean>>} */ /** @type {Map<T, import('#client').Source<boolean>>} */

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.0.0-next.110'; export const VERSION = '5.0.0-next.113';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'missing-attribute-value', code: 'missing_attribute_value',
message: 'Expected attribute value', message: 'Expected attribute value',
position: [12, 12] position: [12, 12]
} }

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

Loading…
Cancel
Save