Merge branch 'main' into deprecations

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

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

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

@ -0,0 +1,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 */
/** @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]`)'
// },
};
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
/** @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;
@ -516,24 +44,14 @@ export class CompileError extends Error {
}
/**
* @template {Exclude<keyof typeof errors, 'TODO'>} T
* @param {NodeLike | number | null} node
* @param {T} code
* @param {Parameters<typeof errors[T]>} args
* @param {null | number | NodeLike} node
* @param {string} code
* @param {string} message
* @returns {never}
*/
export function error(node, code, ...args) {
const fn = errors[code];
// @ts-expect-error
const message = fn(...args);
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
);
}
throw new CompileError(code, message, start !== undefined && end !== undefined ? [start, end] : undefined);
}

@ -1,5 +1,5 @@
import { getLocator } from 'locate-character';
import { walk } from 'zimmerframe';
import { walk as zimmerframe_walk } from 'zimmerframe';
import { CompileError } from './errors.js';
import { convert } from './legacy.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 { transform_component, transform_module } from './phases/3-transform/index.js';
import { validate_component_options, validate_module_options } from './validate-options.js';
import { reset_warnings } from './warnings.js';
export { default as preprocess } from './preprocess/index.js';
/**
@ -20,6 +21,7 @@ export { default as preprocess } from './preprocess/index.js';
*/
export function compile(source, options) {
try {
const warnings = reset_warnings({ source, filename: options.filename });
const validated = validate_component_options(options, '');
let parsed = _parse(source);
@ -44,6 +46,7 @@ export function compile(source, options) {
const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options);
result.warnings = warnings;
result.ast = to_public_ast(source, parsed, options.modernAst);
return result;
} catch (e) {
@ -65,9 +68,12 @@ export function compile(source, options) {
*/
export function compileModule(source, options) {
try {
const warnings = reset_warnings({ source, filename: options.filename });
const validated = validate_module_options(options, '');
const analysis = analyze_module(parse_acorn(source, false), validated);
return transform_module(analysis, source, validated);
const result = transform_module(analysis, source, validated);
result.warnings = warnings;
return result;
} catch (e) {
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
@ -98,6 +104,32 @@ function handle_compile_error(error, filename, source) {
throw error;
}
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* https://svelte.dev/docs/svelte-compiler#svelte-parse
* @overload
* @param {string} source
* @param {{ filename?: string; modern: true }} options
* @returns {import('#compiler').Root}
*/
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
* The `modern` option (`false` by default in Svelte 5) makes the parser return a modern AST instead of the legacy AST.
* `modern` will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* https://svelte.dev/docs/svelte-compiler#svelte-parse
* @overload
* @param {string} source
* @param {{ filename?: string; modern?: false }} [options]
* @returns {import('./types/legacy-nodes.js').LegacyRoot}
*/
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
@ -133,7 +165,7 @@ export function parse(source, options = {}) {
function to_public_ast(source, ast, modern) {
if (modern) {
// remove things that we don't want to treat as public API
return walk(ast, null, {
return zimmerframe_walk(ast, null, {
_(node, { next }) {
// @ts-ignore
delete node.parent;
@ -151,14 +183,12 @@ function to_public_ast(source, ast, modern) {
* @deprecated Replace this with `import { walk } from 'estree-walker'`
* @returns {never}
*/
function _walk() {
export function walk() {
throw new Error(
`'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 { VERSION } from '../version.js';

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

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

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

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

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

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

@ -1,9 +1,8 @@
import { error } from '../../../errors.js';
import * as e from '../../../errors.js';
const REGEX_MATCHER = /^[~^$*|]?=/;
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_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
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,
metadata: {
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('(')) {
args = read_selector_list(parser, true);
parser.eat(')', true);
} else if (name === 'global') {
error(parser.index, 'invalid-css-global-selector');
}
relative_selector.selectors.push({
@ -354,7 +352,7 @@ function read_selector(parser, inside_pseudo_class = false) {
if (combinator) {
if (relative_selector.selectors.length === 0) {
if (!inside_pseudo_class) {
error(start, 'invalid-css-selector');
e.invalid_css_selector(start);
}
} else {
relative_selector.end = index;
@ -367,12 +365,12 @@ function read_selector(parser, inside_pseudo_class = false) {
parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
error(parser.index, 'invalid-css-selector');
e.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);
if (!value && !property.startsWith('--')) {
error(parser.index, 'invalid-css-declaration');
e.invalid_css_declaration(parser.index);
}
const end = parser.index;
@ -533,7 +531,7 @@ function read_value(parser) {
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++;
}
error(parser.template.length, 'unexpected-eof');
e.unexpected_eof(parser.template.length);
}
/**
@ -579,7 +577,7 @@ function read_identifier(parser) {
let identifier = '';
if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) {
error(start, 'invalid-css-identifier');
e.invalid_css_identifier(start);
}
let escaped = false;
@ -604,7 +602,7 @@ function read_identifier(parser) {
}
if (identifier === '') {
error(start, 'invalid-css-identifier');
e.invalid_css_identifier(start);
}
return identifier;

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

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

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

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

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

@ -1,16 +1,15 @@
import { walk } from 'zimmerframe';
import { warn } from '../../../warnings.js';
import * as w from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js';
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').RawWarning[]} warnings
*/
export function warn_unused(stylesheet, warnings) {
walk(stylesheet, { warnings, stylesheet }, visitors);
export function warn_unused(stylesheet) {
walk(stylesheet, { stylesheet }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { warnings: import('../../types.js').RawWarning[], stylesheet: import('#compiler').Css.StyleSheet }>} */
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { stylesheet: import('#compiler').Css.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {
@ -26,9 +25,16 @@ const visitors = {
if (!node.metadata.used) {
const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start);
warn(context.state.warnings, node, context.path, 'css-unused-selector', text);
w.css_unused_selector(node, text);
}
context.next();
},
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 { walk } from 'zimmerframe';
import { error } from '../../errors.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import {
extract_identifiers,
extract_all_identifiers_from_expression,
@ -14,7 +15,6 @@ import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import { merge } from '../visitors.js';
import { validation_legacy, validation_runes, validation_runes_js } from './validation.js';
import { warn } from '../../warnings.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { regex_starts_with_newline } from '../patterns.js';
import { create_attribute, is_element_node } from '../nodes.js';
@ -24,6 +24,7 @@ import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
/**
* @param {import('#compiler').Script | null} script
@ -229,20 +230,13 @@ export function analyze_module(ast, options) {
for (const [name, references] of scope.references) {
if (name[0] !== '$' || ReservedKeywords.includes(name)) continue;
if (name === '$' || name[1] === '$') {
error(references[0].node, 'illegal-global', name);
e.illegal_global(references[0].node, name);
}
}
/** @type {import('../types').RawWarning[]} */
const warnings = [];
const analysis = {
warnings
};
walk(
/** @type {import('estree').Node} */ (ast),
{ scope, analysis },
{ scope },
// @ts-expect-error TODO clean this mess up
merge(set_scope(scopes), validation_runes_js, runes_scope_js_tweaker)
);
@ -250,7 +244,6 @@ export function analyze_module(ast, options) {
return {
module: { ast, scope, scopes },
name: options.filename || 'module',
warnings,
accessors: false,
runes: true,
immutable: true
@ -274,14 +267,11 @@ export function analyze_component(root, source, options) {
/** @type {import('../types.js').Template} */
const template = { ast: root.fragment, scope, scopes };
/** @type {import('../types').RawWarning[]} */
const warnings = [];
// create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || ReservedKeywords.includes(name)) continue;
if (name === '$' || name[1] === '$') {
error(references[0].node, 'illegal-global', name);
e.illegal_global(references[0].node, name);
}
const store_name = name.slice(1);
@ -321,16 +311,16 @@ export function analyze_component(root, source, options) {
}
if (is_nested_store_subscription_node) {
error(is_nested_store_subscription_node, 'illegal-store-subscription');
e.illegal_store_subscription(is_nested_store_subscription_node);
}
if (options.runes !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) {
error(references[0].node, 'illegal-global', name);
e.illegal_global(references[0].node, name);
} else if (declaration !== null && Runes.includes(/** @type {any} */ (name))) {
for (const { node, path } of references) {
if (path.at(-1)?.type === 'CallExpression') {
warn(warnings, node, [], 'store-with-rune-name', store_name);
w.store_with_rune_name(node, store_name);
}
}
}
@ -345,7 +335,7 @@ export function analyze_component(root, source, options) {
// const state = $state(0) is valid
get_rune(/** @type {import('estree').Node} */ (path.at(-1)), module.scope) === null
) {
error(node, 'illegal-subscription');
e.illegal_subscription(node);
}
}
}
@ -390,7 +380,6 @@ export function analyze_component(root, source, options) {
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Map(),
warnings,
css: {
ast: root.css,
hash: root.css
@ -409,15 +398,15 @@ export function analyze_component(root, source, options) {
if (root.options) {
for (const attribute of root.options.attributes) {
if (attribute.name === 'accessors') {
warn(analysis.warnings, attribute, [], 'deprecated-accessors');
w.deprecated_accessors(attribute);
}
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') {
warn(analysis.warnings, attribute, [], 'deprecated-immutable');
w.deprecated_immutable(attribute);
}
}
}
@ -425,12 +414,12 @@ export function analyze_component(root, source, options) {
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
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');
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]) {
@ -445,7 +434,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
private_derived_state: [],
function_depth: scope.function_depth
function_depth: scope.function_depth,
ignores: new Set()
};
walk(
@ -463,7 +453,7 @@ export function analyze_component(root, source, options) {
({ 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(),
expression: null,
private_derived_state: [],
function_depth: scope.function_depth
function_depth: scope.function_depth,
ignores: new Set()
};
walk(
@ -507,7 +498,7 @@ export function analyze_component(root, source, options) {
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
);
if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
warn(warnings, binding.node, [], 'unused-export-let', name);
w.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)) {
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
@ -547,7 +538,7 @@ export function analyze_component(root, source, options) {
type === 'AwaitBlock' ||
type === 'KeyBlock'
) {
warn(warnings, binding.node, [], 'non-state-reference', name);
w.non_state_reference(binding.node, name);
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;
}
}
@ -569,7 +560,13 @@ export function analyze_component(root, source, options) {
for (const element of analysis.elements) {
prune(analysis.css.ast, element);
}
warn_unused(analysis.css.ast, analysis.warnings);
if (
!analysis.css.ast.content.comment ||
!extract_svelte_ignore(analysis.css.ast.content.comment.data).includes('css_unused_selector')
) {
warn_unused(analysis.css.ast);
}
outer: for (const element of analysis.elements) {
if (element.metadata.scoped) {
@ -691,7 +688,7 @@ const legacy_scope_tweaker = {
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
)
) {
warn(state.analysis.warnings, node, path, 'module-script-reactive-declaration');
w.module_script_reactive_declaration(node);
}
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 = {
VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return;
@ -1057,6 +1054,65 @@ const function_visitor = (node, context) => {
/** @type {import('./types').Visitors} */
const common_visitors = {
_(node, context) {
// @ts-expect-error
const comments = /** @type {import('estree').Comment[]} */ (node.leadingComments);
if (comments) {
/** @type {string[]} */
const ignores = [];
for (const comment of comments) {
ignores.push(...extract_svelte_ignore(comment.value));
}
if (ignores.length > 0) {
// @ts-expect-error see below
node.ignores = new Set([...context.state.ignores, ...ignores]);
}
}
// @ts-expect-error
if (node.ignores) {
context.next({
...context.state,
// @ts-expect-error see below
ignores: node.ignores
});
} else if (context.state.ignores.size > 0) {
// @ts-expect-error
node.ignores = context.state.ignores;
}
},
Fragment(node, context) {
/** @type {string[]} */
let ignores = [];
for (const child of node.nodes) {
if (child.type === 'Text' && child.data.trim() === '') {
continue;
}
if (child.type === 'Comment') {
ignores.push(...extract_svelte_ignore(child.data));
} else {
const combined_ignores = new Set(context.state.ignores);
for (const ignore of ignores) combined_ignores.add(ignore);
if (combined_ignores.size > 0) {
// TODO this is a grotesque hack that's made necessary by the fact that
// we can't call `context.visit(...)` here, because we do the convoluted
// visitor merging thing. I'm increasingly of the view that we should
// rearchitect this stuff and have a single visitor per node. It'd be
// more efficient and much simpler.
// @ts-expect-error
child.ignores = combined_ignores;
}
ignores = [];
}
}
},
Attribute(node, context) {
if (node.value === true) return;
@ -1154,7 +1210,7 @@ const common_visitors = {
binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth
) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');
w.static_state_reference(node);
}
}
},
@ -1437,7 +1493,7 @@ function order_reactive_statements(unsorted_reactive_declarations) {
const cycle = check_graph_for_cycles(edges);
if (cycle?.length) {
const declaration = /** @type {Tuple[]} */ (lookup.get(cycle[0]))[0];
error(declaration[0], 'cyclical-reactive-declaration', cycle);
e.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

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

@ -3,7 +3,7 @@ import {
interactive_elements,
is_tag_valid_with_parent
} from '../../../constants.js';
import { error } from '../../errors.js';
import * as e from '../../errors.js';
import {
extract_identifiers,
get_parent,
@ -12,7 +12,7 @@ import {
object,
unwrap_optional
} from '../../utils/ast.js';
import { warn } from '../../warnings.js';
import * as w from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { binding_properties } from '../bindings.js';
import {
@ -44,14 +44,14 @@ function validate_component(node, context) {
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective'
) {
error(attribute, 'invalid-component-directive');
e.invalid_component_directive(attribute);
}
if (
attribute.type === 'OnDirective' &&
(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') {
@ -62,12 +62,12 @@ function validate_component(node, context) {
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression');
if (char === '{') e.invalid_sequence_expression(expression);
}
}
}
validate_attribute_name(attribute, context);
validate_attribute_name(attribute);
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
@ -93,8 +93,12 @@ const react_attributes = new Map([
*/
function validate_element(node, context) {
let has_animate_directive = false;
let has_in_transition = false;
let has_out_transition = false;
/** @type {import('#compiler').TransitionDirective | null} */
let in_transition = null;
/** @type {import('#compiler').TransitionDirective | null} */
let out_transition = null;
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
@ -107,18 +111,18 @@ function validate_element(node, context) {
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression');
if (char === '{') e.invalid_sequence_expression(expression);
}
}
}
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 (!is_expression) {
error(attribute, 'invalid-event-attribute-value');
e.invalid_event_attribute_value(attribute);
}
const value = attribute.value[0].expression;
@ -127,13 +131,7 @@ function validate_element(node, context) {
value.name === attribute.name &&
!context.state.scope.get(value.name)
) {
warn(
context.state.analysis.warnings,
attribute,
context.path,
'global-event-reference',
attribute.name
);
w.global_event_reference(attribute, attribute.name);
}
}
@ -143,28 +141,21 @@ function validate_element(node, context) {
}
if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') {
warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is');
w.avoid_is(attribute);
}
const correct_name = react_attributes.get(attribute.name);
if (correct_name) {
warn(
context.state.analysis.warnings,
attribute,
context.path,
'invalid-html-attribute',
attribute.name,
correct_name
);
w.invalid_html_attribute(attribute, attribute.name, correct_name);
}
validate_attribute_name(attribute, context);
validate_attribute_name(attribute);
} else if (attribute.type === 'AnimateDirective') {
const parent = context.path.at(-2);
if (parent?.type !== 'EachBlock') {
error(attribute, 'invalid-animation', 'no-each');
e.animation_invalid_placement(attribute);
} else if (!parent.key) {
error(attribute, 'invalid-animation', 'each-key');
e.animation_missing_key(attribute);
} else if (
parent.body.nodes.filter(
(n) =>
@ -173,34 +164,39 @@ function validate_element(node, context) {
(n.type !== 'Text' || n.data.trim() !== '')
).length > 1
) {
error(attribute, 'invalid-animation', 'child');
e.animation_invalid_placement(attribute);
}
if (has_animate_directive) {
error(attribute, 'duplicate-animation');
e.animation_duplicate(attribute);
} else {
has_animate_directive = true;
}
} else if (attribute.type === 'TransitionDirective') {
if ((attribute.outro && has_out_transition) || (attribute.intro && has_in_transition)) {
/** @param {boolean} _in @param {boolean} _out */
const type = (_in, _out) => (_in && _out ? 'transition' : _in ? 'in' : 'out');
error(
attribute,
'duplicate-transition',
type(has_in_transition, has_out_transition),
type(attribute.intro, attribute.outro)
);
const existing = /** @type {import('#compiler').TransitionDirective | null} */ (
(attribute.intro && in_transition) || (attribute.outro && out_transition)
);
if (existing) {
const a = existing.intro ? (existing.outro ? 'transition' : 'in') : 'out';
const b = attribute.intro ? (attribute.outro ? 'transition' : 'in') : 'out';
if (a === b) {
e.transition_duplicate(attribute, a);
} else {
e.transition_conflict(attribute, a, b);
}
}
has_in_transition = has_in_transition || attribute.intro;
has_out_transition = has_out_transition || attribute.outro;
if (attribute.intro) in_transition = attribute;
if (attribute.outro) out_transition = attribute;
} else if (attribute.type === 'OnDirective') {
let has_passive_modifier = false;
let conflicting_passive_modifier = '';
for (const modifier of attribute.modifiers) {
if (!EventModifiers.includes(modifier)) {
error(attribute, 'invalid-event-modifier', EventModifiers);
const list = `${EventModifiers.slice(0, -1).join(', ')} or ${EventModifiers.at(-1)}`;
e.invalid_event_modifier(attribute, list);
}
if (modifier === 'passive') {
has_passive_modifier = true;
@ -208,12 +204,7 @@ function validate_element(node, context) {
conflicting_passive_modifier = modifier;
}
if (has_passive_modifier && conflicting_passive_modifier) {
error(
attribute,
'invalid-event-modifier-combination',
'passive',
conflicting_passive_modifier
);
e.invalid_event_modifier_combination(attribute, 'passive', conflicting_passive_modifier);
}
}
}
@ -222,16 +213,15 @@ function validate_element(node, context) {
/**
* @param {import('#compiler').Attribute} attribute
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
*/
function validate_attribute_name(attribute, context) {
function validate_attribute_name(attribute) {
if (
attribute.name.includes(':') &&
!attribute.name.startsWith('xmlns:') &&
!attribute.name.startsWith('xlink:') &&
!attribute.name.startsWith('xml:')
) {
warn(context.state.analysis.warnings, attribute, context.path, 'illegal-attribute-character');
w.illegal_attribute_character(attribute);
}
}
@ -259,7 +249,7 @@ function validate_slot_attribute(context, attribute) {
if (owner) {
if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-attribute');
e.invalid_slot_attribute(attribute);
}
if (
@ -268,13 +258,13 @@ function validate_slot_attribute(context, attribute) {
owner.type === 'SvelteSelf'
) {
if (owner !== context.path.at(-2)) {
error(attribute, 'invalid-slot-placement');
e.invalid_slot_placement(attribute);
}
const name = attribute.value[0].data;
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);
@ -291,12 +281,12 @@ function validate_slot_attribute(context, attribute) {
}
}
error(node, 'invalid-default-slot-content');
e.invalid_default_slot_content(node);
}
}
}
} 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,
// so don't warn in that case because it would be distracting.
if (node.nodes.length === 1 && node.nodes[0].type === 'Text' && !node.nodes[0].raw.trim()) {
warn(context.state.analysis.warnings, node.nodes[0], context.path, 'empty-block');
w.empty_block(node.nodes[0]);
}
}
@ -327,7 +317,7 @@ const validation = {
const left = object(assignee);
if (left === null) {
error(node, 'invalid-binding-expression');
e.invalid_binding_expression(node);
}
const binding = context.state.scope.get(left.name);
@ -347,36 +337,30 @@ const validation = {
binding.kind !== 'store_sub' &&
!binding.mutated)
) {
error(node.expression, 'invalid-binding-value');
e.invalid_binding_value(node.expression);
}
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') {
error(node, 'invalid-each-assignment');
e.invalid_each_assignment(node);
}
if (binding.kind === 'snippet') {
error(node, 'invalid-snippet-assignment');
e.invalid_snippet_assignment(node);
}
}
if (node.name === 'group') {
if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
throw new Error('Cannot find declaration for bind:group');
}
}
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
warn(
context.state.analysis.warnings,
binding.node,
context.path,
'invalid-rest-eachblock-binding',
binding.node.name
);
w.invalid_rest_eachblock_binding(binding.node, binding.node.name);
}
const parent = context.path.at(-1);
@ -389,21 +373,14 @@ const validation = {
parent?.type === 'SvelteBody'
) {
if (context.state.options.namespace === 'foreign' && node.name !== 'this') {
error(
node,
'invalid-binding',
node.name,
undefined,
'. Foreign elements only support bind:this'
);
e.bind_invalid_detailed(node, node.name, 'Foreign elements only support `bind:this`');
}
if (node.name in binding_properties) {
const property = binding_properties[node.name];
if (property.valid_elements && !property.valid_elements.includes(parent.name)) {
error(
e.bind_invalid_target(
node,
'invalid-binding',
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
);
@ -415,17 +392,17 @@ const validation = {
);
if (type && !is_text_attribute(type)) {
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
}
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
error(node, 'invalid-binding', node.name, '<input type="checkbox">');
e.bind_invalid_target(node, node.name, '<input type="checkbox">');
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
error(node, 'invalid-binding', node.name, '<input type="file">');
e.bind_invalid_target(node, node.name, '<input type="file">');
}
}
@ -438,14 +415,13 @@ const validation = {
a.value !== true
);
if (multiple) {
error(multiple, 'invalid-multiple-attribute');
e.invalid_multiple_attribute(multiple);
}
}
if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) {
error(
e.bind_invalid_target(
node,
'invalid-binding',
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
);
@ -456,9 +432,9 @@ const validation = {
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable')
);
if (!contenteditable) {
error(node, 'missing-contenteditable-attribute');
e.missing_contenteditable_attribute(node);
} else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) {
error(contenteditable, 'dynamic-contenteditable-attribute');
e.dynamic_contenteditable_attribute(contenteditable);
}
}
} else {
@ -466,15 +442,15 @@ const validation = {
if (match) {
const property = binding_properties[match];
if (!property.valid_elements || property.valid_elements.includes(parent.name)) {
error(node, 'invalid-binding', node.name, undefined, ` (did you mean '${match}'?)`);
e.bind_invalid_detailed(node, node.name, `Did you mean '${match}'?`);
}
}
error(node, 'invalid-binding', node.name);
e.bind_invalid(node, node.name);
}
}
},
ExportDefaultDeclaration(node) {
error(node, 'default-export');
e.default_export(node);
},
ConstTag(node, context) {
const parent = context.path.at(-1);
@ -491,7 +467,7 @@ const validation = {
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {
error(node, 'invalid-const-placement');
e.invalid_const_placement(node);
}
},
ImportDeclaration(node, context) {
@ -502,7 +478,7 @@ const validation = {
specifier.imported.name === 'beforeUpdate' ||
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 !== 'SvelteFragment')
) {
error(node, 'invalid-let-directive-placement');
e.invalid_let_directive_placement(node);
}
},
RegularElement(node, context) {
if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') {
error(node, 'invalid-textarea-content');
e.invalid_textarea_content(node);
}
}
}
@ -538,20 +514,14 @@ const validation = {
binding.declaration_kind === 'import' &&
binding.references.length === 0
) {
warn(
context.state.analysis.warnings,
node,
context.path,
'component-name-lowercase',
node.name
);
w.component_name_lowercase(node, node.name);
}
validate_element(node, context);
if (context.state.parent_element) {
if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) {
error(node, 'invalid-node-placement', `<${node.name}>`, context.state.parent_element);
e.invalid_node_placement(node, `<${node.name}>`, context.state.parent_element);
}
}
@ -563,7 +533,7 @@ const validation = {
parent.name === node.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;
for (let parent of path) {
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) &&
!SVGElements.includes(node.name)
) {
warn(
context.state.analysis.warnings,
node,
context.path,
'invalid-self-closing-tag',
node.name
);
w.invalid_self_closing_tag(node, node.name);
}
context.next({
@ -603,7 +567,7 @@ const validation = {
const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) {
if (arg.type === 'SpreadElement') {
error(arg, 'invalid-render-spread-argument');
e.invalid_render_spread_argument(arg);
}
}
@ -613,7 +577,7 @@ const validation = {
callee.property.type === 'Identifier' &&
['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) => {
@ -625,9 +589,8 @@ const validation = {
);
});
if (is_inside_textarea) {
error(
e.invalid_tag_placement(
node,
'invalid-tag-placement',
'inside <textarea> or <svelte:element this="textarea">',
'render'
);
@ -667,19 +630,19 @@ const validation = {
(node) => node.type !== 'SnippetBlock' && (node.type !== 'Text' || node.data.trim())
)
) {
error(node, 'conflicting-children-snippet');
e.conflicting_children_snippet(node);
}
}
},
StyleDirective(node) {
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) {
const attribute = node.attributes[0];
if (attribute) {
error(attribute, 'illegal-svelte-head-attribute');
e.illegal_svelte_head_attribute(attribute);
}
},
SvelteElement(node, context) {
@ -692,7 +655,7 @@ const validation = {
SvelteFragment(node, context) {
const parent = context.path.at(-2);
if (parent?.type !== 'Component' && parent?.type !== 'SvelteComponent') {
error(node, 'invalid-svelte-fragment-placement');
e.invalid_svelte_fragment_placement(node);
}
for (const attribute of node.attributes) {
@ -701,7 +664,7 @@ const validation = {
validate_slot_attribute(context, attribute);
}
} 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.name === 'name') {
if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-name', false);
e.invalid_slot_name(attribute);
}
const slot_name = attribute.value[0].data;
if (slot_name === 'default') {
error(attribute, 'invalid-slot-name', true);
e.invalid_slot_name_default(attribute);
}
}
} 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 (context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', 'Text node', context.state.parent_element);
e.invalid_node_placement(node, 'Text node', context.state.parent_element);
}
}
},
TitleElement(node) {
const attribute = node.attributes[0];
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');
if (child) {
error(child, 'invalid-title-content');
e.invalid_title_content(child);
}
},
UpdateExpression(node, context) {
@ -751,7 +714,7 @@ const validation = {
if (!node.parent) return;
if (context.state.parent_element) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', '{expression}', context.state.parent_element);
e.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') {
error(node.init, 'invalid-rune-usage', callee.name);
e.invalid_rune_usage(node.init, callee.name);
}
},
AssignmentExpression(node, { state, path }) {
@ -786,7 +749,7 @@ export const validation_legacy = merge(validation, a11y_validators, {
(state.ast_type !== 'instance' ||
/** @type {import('#compiler').SvelteNode} */ (path.at(-1)).type !== 'Program')
) {
warn(state.analysis.warnings, node, path, 'no-reactive-declaration');
w.no_reactive_declaration(node);
}
},
UpdateExpression(node, { state }) {
@ -805,11 +768,11 @@ function validate_export(node, scope, name) {
if (!binding) return;
if (binding.kind === 'derived') {
error(node, 'invalid-derived-export');
e.invalid_derived_export(node);
}
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 (parent.type === 'VariableDeclarator') return;
error(node, 'invalid-props-location');
e.invalid_props_location(node);
}
if (rune === '$bindable') {
@ -840,7 +803,7 @@ function validate_call_expression(node, scope, path) {
return;
}
}
error(node, 'invalid-bindable-location');
e.invalid_bindable_location(node);
}
if (
@ -851,46 +814,46 @@ function validate_call_expression(node, scope, path) {
) {
if (parent.type === 'VariableDeclarator') return;
if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return;
error(node, 'invalid-state-location', rune);
e.invalid_state_location(node, rune);
}
if (rune === '$effect' || rune === '$effect.pre') {
if (parent.type !== 'ExpressionStatement') {
error(node, 'invalid-effect-location');
e.invalid_effect_location(node);
}
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 (node.arguments.length !== 0) {
error(node, 'invalid-rune-args-length', rune, [0]);
e.invalid_rune_args(node, rune);
}
}
if (rune === '$effect.root') {
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 (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 (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 (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.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 }) {
if (get_rune(node, state.scope) === '$host') {
error(node, 'invalid-host-location');
e.invalid_host_location(node);
}
validate_call_expression(node, state.scope, path);
},
@ -945,13 +908,13 @@ export const validation_runes_js = {
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.invalid_rune_args_length(node, rune, 'exactly one argument');
} 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') {
error(node, 'invalid-props-location');
e.invalid_props_location(node);
} else if (rune === '$bindable') {
error(node, 'invalid-bindable-location');
e.invalid_bindable_location(node);
}
},
AssignmentExpression(node, { state }) {
@ -989,12 +952,12 @@ export const validation_runes_js = {
const allowed_depth = context.state.ast_type === 'module' ? 0 : 1;
if (context.state.scope.function_depth > allowed_depth) {
warn(context.state.analysis.warnings, node, context.path, 'avoid-nested-class');
w.avoid_nested_class(node);
}
},
NewExpression(node, context) {
if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) {
warn(context.state.analysis.warnings, node, context.path, 'avoid-inline-class');
w.avoid_inline_class(node);
}
}
};
@ -1021,16 +984,24 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (binding?.declaration_kind === 'const' && binding.kind !== 'each') {
error(
node,
'invalid-const-assignment',
is_binding,
// This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
(binding.kind !== 'normal' || !binding.initial)
);
// e.invalid_const_assignment(
// node,
// is_binding,
// // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
// binding.kind !== 'state' &&
// binding.kind !== 'frozen_state' &&
// (binding.kind !== 'normal' || !binding.initial)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
const thing = 'constant';
if (is_binding) {
e.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 (binding?.kind === 'derived') {
error(node, 'invalid-derived-assignment');
e.invalid_assignment(node, 'derived state');
}
if (binding?.kind === 'each') {
error(node, 'invalid-each-assignment');
e.invalid_each_assignment(node);
}
}
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 (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, {
LabeledStatement(node, { path }) {
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 }) {
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.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export');
e.invalid_legacy_export(node);
}
},
ExportSpecifier(node, { state }) {
@ -1110,12 +1081,12 @@ export const validation_runes = merge(validation, a11y_validators, {
CallExpression(node, { state, path }) {
const rune = get_rune(node, state.scope);
if (rune === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
e.invalid_rune_args_length(node, '$bindable', 'zero or one arguments');
} else if (rune === '$host') {
if (node.arguments.length > 0) {
error(node, 'invalid-rune-args-length', '$host', [0]);
e.invalid_rune_args(node, '$host');
} 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.name === '$state' || context.name === '$derived')
) {
error(node, 'invalid-state-location', context.name);
e.invalid_state_location(node, context.name);
}
next({ ...state });
},
VariableDeclarator(node, { state, path }) {
VariableDeclarator(node, { state }) {
ensure_no_module_import_conflict(node, state);
const init = node.init;
@ -1139,7 +1110,7 @@ export const validation_runes = merge(validation, a11y_validators, {
if (rune === null) {
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;
}
@ -1148,39 +1119,39 @@ export const validation_runes = merge(validation, a11y_validators, {
// TODO some of this is duplicated with above, seems off
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
e.invalid_rune_args_length(node, rune, 'exactly one argument');
} 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') {
if (state.has_props_rune) {
error(node, 'duplicate-props-rune');
e.duplicate_props_rune(node);
}
state.has_props_rune = true;
if (args.length > 0) {
error(node, 'invalid-rune-args-length', rune, [0]);
e.invalid_rune_args(node, rune);
}
if (node.id.type !== 'ObjectPattern') {
error(node, 'invalid-props-id');
e.invalid_props_id(node);
}
if (state.scope !== state.analysis.instance.scope) {
error(node, 'invalid-props-location');
e.invalid_props_location(node);
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
error(property, 'invalid-props-pattern');
e.invalid_props_pattern(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
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.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 (
node.right.type === 'Identifier' &&
node.right.name === '$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) {
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;
// Don't warn on component events; these might not be under the author's control so the warning would be unactionable
if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
warn(state.analysis.warnings, node, path, 'deprecated-event-handler', node.name);
w.deprecated_event_handler(node, node.name);
}
},
// TODO this is a code smell. need to refactor this stuff

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

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

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

@ -16,7 +16,6 @@ import {
import { DOMProperties, PassiveEvents, VoidElements } from '../../../constants.js';
import { is_custom_element_node, is_element_node } from '../../../nodes.js';
import * as b from '../../../../utils/builders.js';
import { error } from '../../../../errors.js';
import {
with_loc,
function_visitor,
@ -1776,10 +1775,10 @@ export const template_visitors = {
);
},
ClassDirective(node, { state, next }) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere');
throw new Error('Node should have been handled elsewhere');
},
StyleDirective(node, { state, next }) {
error(node, 'INTERNAL', 'Node should have been handled elsewhere');
throw new Error('Node should have been handled elsewhere');
},
TransitionDirective(node, { state, visit }) {
let flags = node.modifiers.includes('global') ? TRANSITION_GLOBAL : 0;
@ -2727,7 +2726,9 @@ export const template_visitors = {
case 'checked':
call_expr = b.call(`$.bind_checked`, state.node, getter, setter);
break;
case 'focused':
call_expr = b.call(`$.bind_focused`, state.node, setter);
break;
case 'group': {
/** @type {import('estree').CallExpression[]} */
const indexes = [];
@ -2773,7 +2774,7 @@ export const template_visitors = {
}
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
// see them in devtools
if (!state.dev && is_empty(node)) {
@ -134,6 +134,26 @@ const visitors = {
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();
},
SelectorList(node, { state, next, path }) {
@ -275,6 +295,10 @@ const visitors = {
/** @param {import('#compiler').Css.Rule} rule */
function is_empty(rule) {
if (rule.metadata.is_global_block) {
return rule.block.children.length === 0;
}
for (const child of rule.block.children) {
if (child.type === 'Declaration') {
return false;

@ -17,7 +17,7 @@ export function transform_component(analysis, source, options) {
return {
js: /** @type {any} */ (null),
css: null,
warnings: transform_warnings(source, options.filename, analysis.warnings),
warnings: /** @type {any} */ (null), // set afterwards
metadata: {
runes: analysis.runes
},
@ -60,7 +60,7 @@ export function transform_component(analysis, source, options) {
return {
js,
css,
warnings: transform_warnings(source, options.filename, analysis.warnings), // TODO apply preprocessor sourcemap
warnings: /** @type {any} */ (null), // set afterwards. TODO apply preprocessor sourcemap
metadata: {
runes: analysis.runes
},
@ -79,7 +79,7 @@ export function transform_module(analysis, source, options) {
return {
js: /** @type {any} */ (null),
css: null,
warnings: transform_warnings(source, analysis.name, analysis.warnings),
warnings: /** @type {any} */ (null), // set afterwards
metadata: {
runes: true
},
@ -105,45 +105,10 @@ export function transform_module(analysis, source, options) {
return {
js: print(program, {}),
css: null,
warnings: transform_warnings(source, analysis.name, analysis.warnings),
metadata: {
runes: true
},
warnings: /** @type {any} */ (null), // set afterwards
ast: /** @type {any} */ (null) // set afterwards
};
}
/**
* @param {string} source
* @param {string | undefined} name
* @param {import('../types').RawWarning[]} warnings
* @returns {import('#compiler').Warning[]}
*/
function transform_warnings(source, name, warnings) {
if (warnings.length === 0) return [];
const locate = getLocator(source, { offsetLine: 1 });
/** @type {import('#compiler').Warning[]} */
const result = [];
for (const warning of warnings) {
const start =
warning.position &&
/** @type {import('locate-character').Location} */ (locate(warning.position[0]));
const end =
warning.position &&
/** @type {import('locate-character').Location} */ (locate(warning.position[1]));
result.push({
start,
end,
filename: name,
message: warning.message,
code: warning.code
});
}
return result;
}

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

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

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

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

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

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

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

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

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

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

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

@ -1,340 +1,41 @@
import {
extract_ignores_above_position,
extract_svelte_ignore_from_comments
} from './utils/extract_svelte_ignore.js';
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
/** @typedef {Record<string, (...args: any[]) => string>} Warnings */
import { getLocator } from 'locate-character';
/** @satisfies {Warnings} */
const css = {
/** @param {string} name */
'css-unused-selector': (name) => `Unused CSS selector "${name}"`
};
/** @satisfies {Warnings} */
const attributes = {
'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided',
/** @param {string} name */
'global-event-reference': (name) =>
`You are referencing globalThis.${name}. Did you forget to declare a variable with that name?`,
'illegal-attribute-character': () =>
"Attributes should not contain ':' characters to prevent ambiguity with Svelte directives",
/**
* @param {string} wrong
* @param {string} right
*/
'invalid-html-attribute': (wrong, right) =>
`'${wrong}' is not a valid HTML attribute. Did you mean '${right}'?`
};
/** @satisfies {Warnings} */
const runes = {
/** @param {string} name */
'store-with-rune-name': (name) =>
`It looks like you're using the $${name} rune, but there is a local binding called ${name}. ` +
`Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`,
/** @param {string} name */
'non-state-reference': (name) =>
`${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.`,
'derived-iife': () =>
`Use \`$derived.by(() => {...})\` instead of \`$derived((() => {...})());\``,
'invalid-props-declaration': () =>
`Component properties are declared using $props() in runes mode. Did you forget to call the function?`,
'invalid-bindable-declaration': () =>
`Bindable component properties are declared using $bindable() in runes mode. Did you forget to call the function?`
};
/** @satisfies {Warnings} */
const a11y = {
/** @param {string} name */
'a11y-aria-attributes': (name) => `A11y: <${name}> should not have aria-* attributes`,
/**
* @param {string} attribute
* @param {string | null} [suggestion]
*/
'a11y-unknown-aria-attribute': (attribute, suggestion) =>
`A11y: Unknown aria attribute 'aria-${attribute}'` +
(suggestion ? ` (did you mean '${suggestion}'?)` : ''),
/** @param {string} name */
'a11y-hidden': (name) => `A11y: <${name}> element should not be hidden`,
/**
* @param {import('aria-query').ARIAPropertyDefinition} schema
* @param {string} attribute
*/
'a11y-incorrect-aria-attribute-type': (schema, attribute) => {
let message;
switch (schema.type) {
case 'boolean':
message = `The value of '${attribute}' must be exactly one of true or false`;
break;
case 'id':
message = `The value of '${attribute}' must be a string that represents a DOM element ID`;
break;
case 'idlist':
message = `The value of '${attribute}' must be a space-separated list of strings that represent DOM element IDs`;
break;
case 'tristate':
message = `The value of '${attribute}' must be exactly one of true, false, or mixed`;
break;
case 'token':
message = `The value of '${attribute}' must be exactly one of ${(schema.values || []).join(
', '
)}`;
break;
case 'tokenlist':
message = `The value of '${attribute}' must be a space-separated list of one or more of ${(
schema.values || []
).join(', ')}`;
break;
default:
message = `The value of '${attribute}' must be of type ${schema.type}`;
}
return `A11y: ${message}`;
},
'a11y-aria-activedescendant-has-tabindex': () =>
'A11y: Elements with attribute aria-activedescendant should have tabindex value',
/** @param {string} name */
'a11y-misplaced-role': (name) => `A11y: <${name}> should not have role attribute`,
/** @param {string | boolean} role */
'a11y-no-abstract-role': (role) => `A11y: Abstract role '${role}' is forbidden`,
/**
* @param {string | boolean} role
* @param {string | null} [suggestion]
*/
'a11y-unknown-role': (role, suggestion) =>
`A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : ''),
/** @param {string | boolean} role */
'a11y-no-redundant-roles': (role) => `A11y: Redundant role '${role}'`,
/**
* @param {string} role
* @param {string[]} props
*/
'a11y-role-has-required-aria-props': (role, props) =>
`A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props
.map((name) => `"${name}"`)
.join(', ')}`,
/** @param {string} role */
'a11y-interactive-supports-focus': (role) =>
`A11y: Elements with the '${role}' interactive role must have a tabindex value.`,
/**
* @param {string | boolean} role
* @param {string} element
*/
'a11y-no-interactive-element-to-noninteractive-role': (role, element) =>
`A11y: <${element}> cannot have role '${role}'`,
/**
* @param {string | boolean} role
* @param {string} element
*/
'a11y-no-noninteractive-element-to-interactive-role': (role, element) =>
`A11y: Non-interactive element <${element}> cannot have interactive role '${role}'`,
'a11y-accesskey': () => 'A11y: Avoid using accesskey',
'a11y-autofocus': () => 'A11y: Avoid using autofocus',
'a11y-misplaced-scope': () => 'A11y: The scope attribute should only be used with <th> elements',
'a11y-positive-tabindex': () => 'A11y: avoid tabindex values above zero',
'a11y-click-events-have-key-events': () =>
'A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type="button"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.',
'a11y-no-noninteractive-tabindex': () =>
'A11y: noninteractive element cannot have nonnegative tabIndex value',
/**
* @param {string} attribute
* @param {string} role
* @param {boolean} is_implicit
* @param {string} name
*/
'a11y-role-supports-aria-props': (attribute, role, is_implicit, name) => {
let message = `The attribute '${attribute}' is not supported by the role '${role}'.`;
if (is_implicit) {
message += ` This role is implicit on the element <${name}>.`;
}
return `A11y: ${message}`;
},
/** @param {string} element */
'a11y-no-noninteractive-element-interactions': (element) =>
`A11y: Non-interactive element <${element}> should not be assigned mouse or keyboard event listeners.`,
/**
* @param {string} element
* @param {string[]} handlers
*/
'a11y-no-static-element-interactions': (element, handlers) =>
`A11y: <${element}> with ${handlers.join(', ')} ${
handlers.length === 1 ? 'handler' : 'handlers'
} must have an ARIA role`,
/**
* @param {string} href_attribute
* @param {string} href_value
*/
'a11y-invalid-attribute': (href_attribute, href_value) =>
`A11y: '${href_value}' is not a valid ${href_attribute} attribute`,
/**
* @param {string} name
* @param {string} article
* @param {string} sequence
*/
'a11y-missing-attribute': (name, article, sequence) =>
`A11y: <${name}> element should have ${article} ${sequence} attribute`,
/**
* @param {null | true | string} type
* @param {null | true | string} value
*/
'a11y-autocomplete-valid': (type, value) =>
`A11y: The value '${value}' is not supported by the attribute 'autocomplete' on element <input type="${
type || '...'
}">`,
'a11y-img-redundant-alt': () =>
'A11y: Screenreaders already announce <img> elements as an image.',
'a11y-label-has-associated-control': () =>
'A11y: A form label must be associated with a control.',
'a11y-media-has-caption': () => 'A11y: <video> elements must have a <track kind="captions">',
/** @param {string} name */
'a11y-distracting-elements': (name) => `A11y: Avoid <${name}> elements`,
/** @param {boolean} immediate */
'a11y-structure': (immediate) =>
immediate
? 'A11y: <figcaption> must be an immediate child of <figure>'
: 'A11y: <figcaption> must be first or last child of <figure>',
/**
* @param {string} event
* @param {string} accompanied_by
*/
'a11y-mouse-events-have-key-events': (event, accompanied_by) =>
`A11y: '${event}' event must be accompanied by '${accompanied_by}' event`,
/** @param {string} name */
'a11y-missing-content': (name) => `A11y: <${name}> element should have child content`
};
/** @satisfies {Warnings} */
const state = {
'static-state-reference': () =>
`State referenced in its own scope will never update. Did you mean to reference it inside a closure?`,
/** @param {string} name */
'invalid-rest-eachblock-binding': (name) =>
`The rest operator (...) will create a new object and binding '${name}' with the original object will not work`
};
/** @satisfies {Warnings} */
const performance = {
'avoid-inline-class': () =>
`Avoid 'new class' — instead, declare the class at the top level scope`,
'avoid-nested-class': () => `Avoid declaring classes below the top level scope`
};
/** @satisfies {Warnings} */
const components = {
/** @param {string} name */
'component-name-lowercase': (name) =>
`<${name}> will be treated as an HTML element unless it begins with a capital letter`
};
const legacy = {
'no-reactive-declaration': () =>
`Reactive declarations only exist at the top level of the instance script`,
'module-script-reactive-declaration': () =>
'All dependencies of the reactive declaration are declared in a module script and will not be reactive',
/** @param {string} name */
'unused-export-let': (name) =>
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``,
'deprecated-slot-element': () =>
`Using <slot> to render parent content is deprecated. Use {@render ...} tags instead.`,
/** @param {string} name */
'deprecated-event-handler': (name) =>
`Using on:${name} to listen to the ${name} event is is deprecated. Use the event attribute on${name} instead.`,
'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 */
/** @typedef {{ start?: number, end?: number }} NodeLike */
/** @type {import('#compiler').Warning[]} */
let warnings = [];
/** @type {string | undefined} */
let filename;
let locator = getLocator('', { offsetLine: 1 });
/**
* @template {keyof AllWarnings} T
* @param {import('./phases/types').RawWarning[]} array the array to push the warning to, if not ignored
* @param {{ start?: number, end?: number, type?: string, parent?: import('#compiler').SvelteNode | null, leadingComments?: import('estree').Comment[] } | null} node the node related to the warning
* @param {import('#compiler').SvelteNode[]} path the path to the node, so that we can traverse upwards to find svelte-ignore comments
* @param {T} code the warning code
* @param {Parameters<AllWarnings[T]>} args the arguments to pass to the warning function
* @returns {void}
* @param {{
* source: string;
* filename: string | undefined;
* }} options
* @returns {import('#compiler').Warning[]}
*/
export function warn(array, node, path, code, ...args) {
const fn = warnings[code];
// Traverse the AST upwards to find any svelte-ignore comments.
// This assumes that people don't have their code littered with warnings,
// at which point this might become inefficient.
/** @type {string[]} */
const ignores = [];
if (node) {
// comments inside JavaScript (estree)
if ('leadingComments' in node) {
ignores.push(...extract_svelte_ignore_from_comments(node));
}
}
for (let i = path.length - 1; i >= 0; i--) {
const current = path[i];
// comments inside JavaScript (estree)
if ('leadingComments' in current) {
ignores.push(...extract_svelte_ignore_from_comments(current));
}
// Svelte nodes
if (current.type === 'Fragment') {
ignores.push(
...extract_ignores_above_position(
/** @type {import('#compiler').TemplateNode} */ (path[i + 1] ?? node),
current.nodes
)
);
}
// Style nodes
if (current.type === 'StyleSheet' && current.content.comment) {
ignores.push(...current.content.comment.ignores);
}
}
if (ignores.includes(code)) return;
export function reset_warnings(options) {
filename = options.filename;
locator = getLocator(options.source, { offsetLine: 1 });
return warnings = [];
}
const start = node?.start;
const end = node?.end;
/**
* @param {null | NodeLike} node
* @param {string} code
* @param {string} message
*/
function w(node, code, message) {
// @ts-expect-error
if (node.ignores?.has(code)) return;
array.push({
warnings.push({
code,
// @ts-expect-error
message: fn(...args),
position: start !== undefined && end !== undefined ? [start, end] : undefined
message,
filename,
start: node?.start !== undefined ? locator(node.start) : undefined,
end: node?.end !== undefined ? locator(node.end) : undefined
});
}
}

@ -21,13 +21,13 @@ export function onMount(fn) {
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(() => {
const cleanup = untrack(fn);
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');
}
if (current_component_context.r) {
if (current_component_context.l === null) {
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.');
}
if (current_component_context.r) {
if (current_component_context.l === null) {
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
* @param {import('./internal/client/types.js').ComponentContext} context
* @param {import('#client').ComponentContext} context
*/
function init_update_callbacks(context) {
return (context.u ??= { a: [], b: [], m: [] });
var l = /** @type {import('#client').ComponentContextLegacy} */ (context).l;
return (l.u ??= { a: [], b: [], m: [] });
}
/**

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

@ -20,15 +20,17 @@ export function if_block(
elseif = false
) {
/** @type {import('#client').Effect | null} */
let consequent_effect = null;
var consequent_effect = null;
/** @type {import('#client').Effect | null} */
let alternate_effect = null;
var alternate_effect = 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;
/** 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
set_hydrating(true);
}
});
if (elseif) {
effect.f |= EFFECT_TRANSPARENT;
}
}, flags);
}

@ -1,5 +1,5 @@
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
@ -12,13 +12,19 @@ export function snippet(get_snippet, node, ...args) {
/** @type {SnippetFn | null | undefined} */
var snippet;
var effect = render_effect(() => {
/** @type {import('#client').Effect | null} */
var snippet_effect;
block(() => {
if (snippet === (snippet = get_snippet())) return;
if (snippet) {
branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args));
if (snippet_effect) {
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 {() => string} get_tag
* @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
* @returns {void}
*/
@ -115,13 +115,11 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace) {
? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild))
: element.appendChild(empty());
if (child_anchor) {
// `child_anchor` can be undefined if this is a void element with children,
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is
// user error, but we warn on it elsewhere (in dev) so here we just
// silently ignore it
render_fn(element, child_anchor);
}
// `child_anchor` is undefined if this is a void element, but we still
// need to call `render_fn` in order to run actions etc. If the element
// contains children, it's a user error (which is warned on elsewhere)
// and the DOM will be silently discarded
render_fn(element, child_anchor);
}
anchor.before(element);

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

@ -1,4 +1,5 @@
import { render_effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
/**
* @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,36 +122,42 @@ export function handle_event_propagation(handler_element, event) {
}
});
while (current_target !== null) {
/** @param {Element} current_target */
function next(current_target) {
/** @type {null | Element} */
var parent_element =
current_target.parentNode || /** @type {any} */ (current_target).host || null;
var internal_prop_name = '__' + event_name;
// @ts-ignore
var delegated = current_target[internal_prop_name];
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]);
} else {
delegated.call(current_target, event);
}
}
if (
event.cancelBubble ||
parent_element === handler_element ||
current_target === handler_element
) {
break;
try {
// @ts-expect-error
var delegated = current_target['__' + event_name];
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]);
} else {
delegated.call(current_target, event);
}
}
} finally {
if (
!event.cancelBubble &&
parent_element !== handler_element &&
parent_element !== null &&
current_target !== handler_element
) {
next(parent_element);
}
}
current_target = parent_element;
}
// @ts-expect-error is used above
event.__root = handler_element;
// @ts-expect-error is used above
current_target = handler_element;
try {
next(current_target);
} finally {
// @ts-expect-error is used above
event.__root = handler_element;
// @ts-expect-error is used above
current_target = handler_element;
}
}

@ -7,7 +7,7 @@ import { should_intro } from '../../render.js';
import { is_function } from '../../utils.js';
import { current_each_item } from '../blocks/each.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
@ -241,18 +241,26 @@ export function transition(flags, element, get_fn, get_params) {
(e.transitions ??= []).push(transition);
// if this is a local transition, we only want to run it if the parent (block) effect's
// parent (branch) effect is where the state change happened. we can determine that by
// looking at whether the branch effect is currently initializing
// if this is a local transition, we only want to run it if the parent (branch) effect's
// parent (block) effect is where the state change happened. we can determine that by
// looking at whether the block effect is currently initializing
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
while ((parent.f & BLOCK_EFFECT) === 0 && parent.parent) {
parent = parent.parent;
if (!run) {
var block = /** @type {import('#client').Effect | null} */ (e.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(() => {
untrack(() => transition.in());
});

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

@ -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_element_size, bind_resize_observer } from './dom/elements/bindings/size.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 {
once,

@ -182,11 +182,11 @@ export function effect(fn) {
* @param {() => void | (() => void)} fn
*/
export function legacy_pre_effect(deps, fn) {
var context = /** @type {import('#client').ComponentContext} */ (current_component_context);
var context = /** @type {import('#client').ComponentContextLegacy} */ (current_component_context);
/** @type {{ effect: null | import('#client').Effect, ran: boolean }} */
var token = { effect: null, ran: false };
context.l1.push(token);
context.l.r1.push(token);
token.effect = render_effect(() => {
deps();
@ -196,19 +196,19 @@ export function legacy_pre_effect(deps, fn) {
if (token.ran) return;
token.ran = true;
set(context.l2, true);
set(context.l.r2, true);
untrack(fn);
});
}
export function legacy_pre_effect_reset() {
var context = /** @type {import('#client').ComponentContext} */ (current_component_context);
var context = /** @type {import('#client').ComponentContextLegacy} */ (current_component_context);
render_effect(() => {
if (!get(context.l2)) return;
if (!get(context.l.r2)) return;
// Run dirty `$:` statements
for (var token of context.l1) {
for (var token of context.l.r1) {
var effect = token.effect;
if (check_dirtiness(effect)) {
@ -218,7 +218,7 @@ export function legacy_pre_effect_reset() {
token.ran = false;
}
context.l2.v = false; // set directly to avoid rerunning this effect
context.l.r2.v = false; // set directly to avoid rerunning this effect
});
}
@ -230,9 +230,12 @@ export function render_effect(fn) {
return create_effect(RENDER_EFFECT, fn, true);
}
/** @param {(() => void)} fn */
export function block(fn) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT, fn, true);
/**
* @param {(() => void)} fn
* @param {number} flags
*/
export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
}
/** @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
// track updates to trigger beforeUpdate/afterUpdate callbacks
if (current_component_context) {
(current_component_context.d ??= []).push(s);
if (current_component_context !== null && current_component_context.l !== null) {
(current_component_context.l.s ??= []).push(s);
}
return s;

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

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

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

@ -85,16 +85,26 @@ export function loop_guard(timeout) {
/**
* @param {Record<string, any>} $$props
* @param {string[]} bindable
* @param {string[]} exports
* @param {Function & { filename: string }} component
*/
export function validate_prop_bindings($$props, bindable) {
export function validate_prop_bindings($$props, bindable, exports, component) {
for (const key in $$props) {
if (!bindable.includes(key)) {
var setter = get_descriptor($$props, key)?.set;
var setter = get_descriptor($$props, key)?.set;
var name = component.name;
if (setter) {
if (setter) {
if (exports.includes(key)) {
throw new Error(
`Cannot use bind:${key} on this component because the property was not declared as bindable. ` +
`To mark a property as bindable, use the $bindable() rune like this: \`let { ${key} = $bindable() } = $props()\``
`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(
`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 in ${name}.svelte like this: \`let { ${key} = $bindable() } = $props()\``
);
}
}

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

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

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

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

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

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

Loading…
Cancel
Save