Merge branch 'main' into props-binding-fix

pull/14210/head
Simon Holthausen 2 years ago
commit 6b0af693bd

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: consider variables with synthetic store sub as state

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ensure explicit nesting selector is always applied

@ -169,7 +169,7 @@ Any content inside the component tags that is _not_ a snippet declaration implic
```svelte
<!--- file: App.svelte --->
<Button>click me<Button>
<Button>click me</Button>
```
```svelte

@ -197,7 +197,7 @@ When writing component tests that involve two-way bindings, context or snippet p
E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/).
To get start with Playwright, either let you guide by [their VS Code extension](https://playwright.dev/docs/getting-started-vscode), or install it from the command line using `npm init playwright`. It is also part of the setup CLI when you run `npx sv create`.
To get started with Playwright, either install it via [the VS Code extension](https://playwright.dev/docs/getting-started-vscode), or install it from the command line using `npm init playwright`. It is also part of the setup CLI when you run `npx sv create`.
After you've done that, you should have a `tests` folder and a Playwright config. You may need to adjust that config to tell Playwright what to do before running the tests - mainly starting your application at a certain port:

@ -1,5 +1,69 @@
# svelte
## 5.2.0
### Minor Changes
- feat: better inlining of static attributes ([#14269](https://github.com/sveltejs/svelte/pull/14269))
## 5.1.17
### Patch Changes
- fix: account for `:has(...)` as part of `:root` ([#14229](https://github.com/sveltejs/svelte/pull/14229))
- fix: prevent nested pseudo class from being marked as unused ([#14229](https://github.com/sveltejs/svelte/pull/14229))
- fix: use strict equality for key block comparisons in runes mode ([#14285](https://github.com/sveltejs/svelte/pull/14285))
- fix: bump `is-reference` dependency to fix `import.meta` bug ([#14286](https://github.com/sveltejs/svelte/pull/14286))
## 5.1.16
### Patch Changes
- fix: don't wrap pseudo classes inside `:global(...)` with another `:global(...)` during migration ([#14267](https://github.com/sveltejs/svelte/pull/14267))
- fix: bail on named slots with that have reserved keywords during migration ([#14278](https://github.com/sveltejs/svelte/pull/14278))
## 5.1.15
### Patch Changes
- fix: consider static attributes that are inlined in the template ([#14249](https://github.com/sveltejs/svelte/pull/14249))
## 5.1.14
### Patch Changes
- fix: migration script messing with attributes ([#14260](https://github.com/sveltejs/svelte/pull/14260))
- fix: do not treat reassigned synthetic binds as state in runes mode ([#14236](https://github.com/sveltejs/svelte/pull/14236))
- fix: account for mutations in script module in ownership check ([#14253](https://github.com/sveltejs/svelte/pull/14253))
- fix: consider img with loading attribute not static ([#14237](https://github.com/sveltejs/svelte/pull/14237))
## 5.1.13
### Patch Changes
- fix: add migration task when there's a variable named that would conflict with a rune ([#14216](https://github.com/sveltejs/svelte/pull/14216))
- fix: consider `valueOf` in the reactive methods of `SvelteDate` ([#14227](https://github.com/sveltejs/svelte/pull/14227))
- fix: handle sibling combinators within `:has` ([#14213](https://github.com/sveltejs/svelte/pull/14213))
- fix: consider variables with synthetic store sub as state ([#14195](https://github.com/sveltejs/svelte/pull/14195))
- fix: read index as a source in legacy keyed each block ([#14208](https://github.com/sveltejs/svelte/pull/14208))
- fix: account for shadowing children slot during migration ([#14224](https://github.com/sveltejs/svelte/pull/14224))
- fix: ensure explicit nesting selector is always applied ([#14193](https://github.com/sveltejs/svelte/pull/14193))
- fix: add `lang="ts"` attribute during migration if needed ([#14222](https://github.com/sveltejs/svelte/pull/14222))
## 5.1.12
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.1.12",
"version": "5.2.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -144,7 +144,7 @@
"axobject-query": "^4.1.0",
"esm-env": "^1.0.0",
"esrap": "^1.2.2",
"is-reference": "^3.0.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"

@ -17,7 +17,7 @@ import {
} from '../utils/ast.js';
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
import { validate_component_options } from '../validate-options.js';
import { is_svg, is_void } from '../../utils.js';
import { is_reserved, is_svg, is_void } from '../../utils.js';
import { regex_is_valid_identifier } from '../phases/patterns.js';
const regex_style_tags = /(<style[^>]+>)([\S\s]*?)(<\/style>)/g;
@ -40,9 +40,10 @@ class MigrationError extends Error {
*/
function migrate_css(state) {
if (!state.analysis.css.ast?.start) return;
let code = state.str
const css_contents = state.str
.snip(state.analysis.css.ast.start, /** @type {number} */ (state.analysis.css.ast?.end))
.toString();
let code = css_contents;
let starting = 0;
// since we already blank css we can't work directly on `state.str` so we will create a copy that we can update
@ -56,23 +57,28 @@ function migrate_css(state) {
) {
let start = code.indexOf('(') + 1;
let is_global = false;
const global_str = ':global';
const next_global = code.indexOf(global_str);
const str_between = code.substring(start, next_global);
if (!str_between.trim()) {
is_global = true;
start += global_str.length;
} else {
const prev_global = css_contents.lastIndexOf(global_str, starting);
if (prev_global > -1) {
const end =
find_closing_parenthesis(css_contents.indexOf('(', prev_global) + 1, css_contents) -
starting;
if (end > start) {
starting += end;
code = code.substring(end);
continue;
}
let parenthesis = 1;
let end = start;
let char = code[end];
// find the closing parenthesis
while (parenthesis !== 0 && char) {
if (char === '(') parenthesis++;
if (char === ')') parenthesis--;
end++;
char = code[end];
}
}
const end = find_closing_parenthesis(start, code);
if (start && end) {
if (!is_global && !code.startsWith(':not')) {
str.prependLeft(starting + start, ':global(');
@ -89,6 +95,24 @@ function migrate_css(state) {
state.str.update(state.analysis.css.ast?.start, state.analysis.css.ast?.end, str.toString());
}
/**
* @param {number} start
* @param {string} code
*/
function find_closing_parenthesis(start, code) {
let parenthesis = 1;
let end = start;
let char = code[end];
// find the closing parenthesis
while (parenthesis !== 0 && char) {
if (char === '(') parenthesis++;
if (char === ')') parenthesis--;
end++;
char = code[end];
}
return end;
}
/**
* Does a best-effort migration of Svelte code towards using runes, event attributes and render tags.
* May throw an error if the code is too complex to migrate automatically.
@ -207,8 +231,12 @@ export function migrate(source, { filename, use_ts } = {}) {
analysis.uses_props ||
state.has_svelte_self;
const need_ts_tag =
state.uses_ts &&
(!parsed.instance || !parsed.instance.attributes.some((attr) => attr.name === 'lang'));
if (!parsed.instance && need_script) {
str.appendRight(0, '<script>');
str.appendRight(0, need_ts_tag ? '<script lang="ts">' : '<script>');
}
if (state.has_svelte_self && filename) {
@ -239,6 +267,18 @@ export function migrate(source, { filename, use_ts } = {}) {
insertion_point = state.props_insertion_point;
/**
* @param {"derived"|"props"|"bindable"} rune
*/
function check_rune_binding(rune) {
const has_rune_binding = !!state.scope.get(rune);
if (has_rune_binding) {
throw new MigrationError(
`migrating this component would require adding a \`$${rune}\` rune but there's already a variable named ${rune}.\n Rename the variable and try again or migrate by hand.`
);
}
}
if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) {
const has_many_props = state.props.length > 3;
const newline_separator = `\n${indent}${indent}`;
@ -253,6 +293,7 @@ export function migrate(source, { filename, use_ts } = {}) {
let prop_str =
prop.local === prop.exported ? prop.local : `${prop.exported}: ${prop.local}`;
if (prop.bindable) {
check_rune_binding('bindable');
prop_str += ` = $bindable(${prop.init})`;
} else if (prop.init) {
prop_str += ` = ${prop.init}`;
@ -300,17 +341,23 @@ export function migrate(source, { filename, use_ts } = {}) {
if (type) {
props_declaration = `${type}\n\n${indent}${props_declaration}`;
}
check_rune_binding('props');
props_declaration = `${props_declaration}${type ? `: ${type_name}` : ''} = $props();`;
} else {
if (type) {
props_declaration = `${state.props.length > 0 ? `${type}\n\n${indent}` : ''}/** @type {${state.props.length > 0 ? type_name : ''}${analysis.uses_props || analysis.uses_rest_props ? `${state.props.length > 0 ? ' & ' : ''}{ [key: string]: any }` : ''}} */\n${indent}${props_declaration}`;
}
check_rune_binding('props');
props_declaration = `${props_declaration} = $props();`;
}
props_declaration = `\n${indent}${props_declaration}`;
str.appendRight(insertion_point, props_declaration);
}
if (parsed.instance && need_ts_tag) {
str.appendRight(parsed.instance.start + '<script'.length, ' lang="ts"');
}
}
/**
@ -361,6 +408,7 @@ export function migrate(source, { filename, use_ts } = {}) {
: insertion_point;
if (state.derived_components.size > 0) {
check_rune_binding('derived');
str.appendRight(
insertion_point,
`\n${indent}${[...state.derived_components.entries()].map(([init, name]) => `const ${name} = $derived(${init});`).join(`\n${indent}`)}\n`
@ -368,6 +416,7 @@ export function migrate(source, { filename, use_ts } = {}) {
}
if (state.derived_conflicting_slots.size > 0) {
check_rune_binding('derived');
str.appendRight(
insertion_point,
`\n${indent}${[...state.derived_conflicting_slots.entries()].map(([name, init]) => `const ${name} = $derived(${init});`).join(`\n${indent}`)}\n`
@ -458,6 +507,7 @@ const instance_script = {
for (let specifier of node.specifiers) {
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
['beforeUpdate', 'afterUpdate'].includes(specifier.imported.name)
) {
const references = state.scope.references.get(specifier.local.name);
@ -495,6 +545,8 @@ const instance_script = {
let count_removed = 0;
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier') continue;
const binding = state.scope.get(specifier.local.name);
if (binding?.kind === 'bindable_prop') {
state.str.remove(
@ -652,6 +704,18 @@ const instance_script = {
continue;
}
/**
* @param {"state"|"derived"} rune
*/
function check_rune_binding(rune) {
const has_rune_binding = !!state.scope.get(rune);
if (has_rune_binding) {
throw new MigrationError(
`can't migrate \`${state.str.original.substring(/** @type {number} */ (node.start), node.end)}\` to \`$${rune}\` because there's a variable named ${rune}.\n Rename the variable and try again or migrate by hand.`
);
}
}
// state
if (declarator.init) {
let { start, end } = /** @type {{ start: number, end: number }} */ (declarator.init);
@ -661,6 +725,8 @@ const instance_script = {
while (state.str.original[end - 1] !== ')') end += 1;
}
check_rune_binding('state');
state.str.prependLeft(start, '$state(');
state.str.appendRight(end, ')');
} else {
@ -755,6 +821,8 @@ const instance_script = {
}
}
check_rune_binding('derived');
// Someone wrote a `$: { ... }` statement which we can turn into a `$derived`
state.str.appendRight(
/** @type {number} */ (declarator.id.typeAnnotation?.end ?? declarator.id.end),
@ -795,6 +863,8 @@ const instance_script = {
}
}
} else {
check_rune_binding('state');
state.str.prependLeft(
/** @type {number} */ (declarator.id.typeAnnotation?.end ?? declarator.id.end),
' = $state('
@ -858,6 +928,18 @@ const instance_script = {
next();
/**
* @param {"state"|"derived"} rune
*/
function check_rune_binding(rune) {
const has_rune_binding = state.scope.get(rune);
if (has_rune_binding) {
throw new MigrationError(
`can't migrate \`$: ${state.str.original.substring(/** @type {number} */ (node.body.start), node.body.end)}\` to \`$${rune}\` because there's a variable named ${rune}.\n Rename the variable and try again or migrate by hand.`
);
}
}
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
@ -878,6 +960,8 @@ const instance_script = {
node.body.expression.right
);
check_rune_binding('derived');
// $derived
state.str.update(
/** @type {number} */ (node.start),
@ -902,6 +986,7 @@ const instance_script = {
} else {
for (const binding of reassigned_bindings) {
if (binding && (ids.includes(binding.node) || expression_ids.length === 0)) {
check_rune_binding('state');
const init =
binding.kind === 'state'
? ' = $state()'
@ -1242,6 +1327,21 @@ const template = {
existing_prop.needs_refine_type = false;
}
if (
slot_name === 'default' &&
path.some(
(parent) =>
(parent.type === 'SvelteComponent' ||
parent.type === 'Component' ||
parent.type === 'RegularElement' ||
parent.type === 'SvelteElement' ||
parent.type === 'SvelteFragment') &&
parent.attributes.some((attr) => attr.type === 'LetDirective')
)
) {
aliased_slot_name = `${name}_render`;
state.derived_conflicting_slots.set(aliased_slot_name, name);
}
name = aliased_slot_name ?? name;
if (node.fragment.nodes.length > 0) {
@ -1343,7 +1443,7 @@ function migrate_slot_usage(node, path, state) {
if (snippet_name === 'default') {
snippet_name = 'children';
}
if (!regex_is_valid_identifier.test(snippet_name)) {
if (!regex_is_valid_identifier.test(snippet_name) || is_reserved(snippet_name)) {
has_migration_task = true;
state.str.appendLeft(
node.start,

@ -4,6 +4,7 @@
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js';
/**
* @typedef {Visitors<
@ -15,27 +16,6 @@ import { is_keyframes_node } from '../../css.js';
* >} CssVisitors
*/
/**
* True if is `:global(...)` or `:global`
* @param {Css.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
*/
function is_global(relative_selector) {
const first = relative_selector.selectors[0];
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
(first.args === null ||
// Only these two selector types keep the whole selector global, because e.g.
// :global(button).x means that the selector is still scoped because of the .x
relative_selector.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
))
);
}
/**
* True if is `:global`
* @param {Css.SimpleSelector} simple_selector
@ -119,11 +99,14 @@ const css_visitors = {
node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector'
) {
const first = node.children[0]?.selectors[1];
const no_nesting_scope =
first?.type !== 'PseudoClassSelector' || is_unscoped_pseudo_class(first);
const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some(
(child) => child.children.length === 1 && child.children[0].metadata.is_global
);
// mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used
if (parent_is_global) {
if (no_nesting_scope && parent_is_global) {
node.metadata.used = true;
}
}
@ -156,9 +139,23 @@ const css_visitors = {
].includes(first.name));
}
node.metadata.is_global_like ||= !!node.selectors.find(
node.metadata.is_global_like ||=
node.selectors.some(
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
);
) &&
// :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped
!node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has');
if (node.metadata.is_global_like || node.metadata.is_global) {
// So that nested selectors like `:root:not(.x)` are not marked as unused
for (const child of node.selectors) {
walk(/** @type {Css.Node} */ (child), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
});
}
}
context.next();
},

@ -1,7 +1,7 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js';
import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
@ -172,7 +172,7 @@ function get_relative_selectors(node) {
}
/**
* Discard trailing `:global(...)` selectors without a `:has/is/where/not(...)` modifier, these are unused for scoping purposes
* Discard trailing `:global(...)` selectors, these are unused for scoping purposes
* @param {Compiler.Css.ComplexSelector} node
*/
function truncate(node) {
@ -182,21 +182,22 @@ function truncate(node) {
// not after a :global selector
!metadata.is_global_like &&
!(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) &&
// not a :global(...) without a :has/is/where/not(...) modifier
(!metadata.is_global ||
selectors.some(
(selector) =>
selector.type === 'PseudoClassSelector' &&
selector.args !== null &&
(selector.name === 'has' ||
selector.name === 'is' ||
selector.name === 'where' ||
selector.name === 'not')
))
// not a :global(...) without a :has/is/where(...) modifier that is scoped
!metadata.is_global
);
});
return node.children.slice(0, i + 1);
return node.children.slice(0, i + 1).map((child) => {
// In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global).
// To properly accomplish that, we gotta filter out all selector types except `:has`.
const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root');
if (!root || child.metadata.is_global_like) return child;
return {
...child,
selectors: child.selectors.filter((s) => s.type === 'PseudoClassSelector' && s.name === 'has')
};
});
}
/**
@ -334,7 +335,9 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*/
function mark(relative_selector, element) {
if (!is_outer_global(relative_selector)) {
relative_selector.metadata.scoped = true;
}
element.metadata.scoped = true;
}
@ -412,6 +415,23 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
const child_elements = [];
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const descendant_elements = [];
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component (e.g. :root).
const rules = [rule, ...get_parent_rules(rule)];
const include_self =
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
rules[rules.length - 1].prelude.children.some((c) =>
c.children.some((r) =>
r.selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root')
)
);
if (include_self) {
child_elements.push(element);
descendant_elements.push(element);
}
walk(
/** @type {Compiler.SvelteNode} */ (element.fragment),
@ -457,7 +477,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
}
const descendants =
left_most_combinator.name === '>' ? child_elements : descendant_elements;
left_most_combinator.name === '+' || left_most_combinator.name === '~'
? (sibling_elements ??= get_following_sibling_elements(element, include_self))
: left_most_combinator.name === '>'
? child_elements
: descendant_elements;
let selector_matched = false;
@ -475,20 +499,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
}
if (!matched) {
if (relative_selector.metadata.is_global && !relative_selector.metadata.is_global_like) {
// Edge case: `:global(.x):has(.y)` where `.x` is global but `.y` doesn't match.
// Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and
// we have no way of knowing if it's safe to set it back to `false`, we'll mark
// the inner selector as used and scoped to prevent it from being pruned, which could
// result in a invalid CSS output (e.g. `.x:has(/* unused .y */)`). The result
// can't match a real element, so the only drawback is the missing prune.
// TODO clean this up some day
complex_selectors[0].metadata.used = true;
complex_selectors[0].children.forEach((selector) => {
selector.metadata.scoped = true;
});
}
return false;
}
}
@ -501,9 +511,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
switch (selector.type) {
case 'PseudoClassSelector': {
if (name === 'host' || name === 'root') {
return false;
}
if (name === 'host' || name === 'root') return false;
if (
name === 'global' &&
@ -572,23 +580,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
}
if (!matched) {
if (
relative_selector.metadata.is_global &&
!relative_selector.metadata.is_global_like
) {
// Edge case: `:global(.x):is(.y)` where `.x` is global but `.y` doesn't match.
// Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and
// we have no way of knowing if it's safe to set it back to `false`, we'll mark
// the inner selector as used and scoped to prevent it from being pruned, which could
// result in a invalid CSS output (e.g. `.x:is(/* unused .y */)`). The result
// can't match a real element, so the only drawback is the missing prune.
// TODO clean this up some day
selector.args.children[0].metadata.used = true;
selector.args.children[0].children.forEach((selector) => {
selector.metadata.scoped = true;
});
}
return false;
}
}
@ -656,7 +647,10 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule);
for (const complex_selector of parent.prelude.children) {
if (apply_selector(get_relative_selectors(complex_selector), parent, element, state)) {
if (
apply_selector(get_relative_selectors(complex_selector), parent, element, state) ||
complex_selector.children.every((s) => is_global(s, parent))
) {
complex_selector.metadata.used = true;
matched = true;
}
@ -675,6 +669,58 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
return true;
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {boolean} include_self
*/
function get_following_sibling_elements(element, include_self) {
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.Root | null} */
let parent = get_element_parent(element);
if (!parent) {
parent = element;
while (parent?.type !== 'Root') {
parent = /** @type {any} */ (parent).parent;
}
}
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const sibling_elements = [];
let found_parent = false;
for (const el of parent.fragment.nodes) {
if (found_parent) {
walk(
el,
{},
{
RegularElement(node) {
sibling_elements.push(node);
},
SvelteElement(node) {
sibling_elements.push(node);
}
}
);
} else {
/** @type {any} */
let child = element;
while (child !== el && child !== parent) {
child = child.parent;
}
if (child === el) {
found_parent = true;
}
}
}
if (include_self) {
sibling_elements.push(element);
}
return sibling_elements;
}
/**
* @param {any} operator
* @param {any} expected_value

@ -24,7 +24,12 @@ const visitors = {
}
},
ComplexSelector(node, context) {
if (!node.metadata.used) {
if (
!node.metadata.used &&
// prevent double-marking of `.unused:is(.unused)`
(context.path.at(-2)?.type !== 'PseudoClassSelector' ||
/** @type {Css.ComplexSelector} */ (context.path.at(-4))?.metadata.used)
) {
const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start);
w.css_unused_selector(node, text);

@ -1,4 +1,4 @@
/** @import { AST } from '#compiler' */
/** @import { AST, Css } from '#compiler' */
/** @import { Node } from 'estree' */
const UNKNOWN = {};
@ -33,3 +33,85 @@ export function get_possible_values(chunk) {
if (values.has(UNKNOWN)) return null;
return values;
}
/**
* Returns all parent rules; root is last
* @param {Css.Rule | null} rule
*/
export function get_parent_rules(rule) {
const parents = [];
let parent = rule?.metadata.parent_rule;
while (parent) {
parents.push(parent);
parent = parent.metadata.parent_rule;
}
return parents;
}
/**
* True if is `:global(...)` or `:global` and no pseudo class that is scoped.
* @param {Css.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
*/
export function is_global(relative_selector) {
const first = relative_selector.selectors[0];
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
(first.args === null ||
// Only these two selector types keep the whole selector global, because e.g.
// :global(button).x means that the selector is still scoped because of the .x
relative_selector.selectors.every(
(selector) =>
is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector'
))
);
}
/**
* `true` if is a pseudo class that cannot be or is not scoped
* @param {Css.SimpleSelector} selector
*/
export function is_unscoped_pseudo_class(selector) {
return (
selector.type === 'PseudoClassSelector' &&
// These make the selector scoped
((selector.name !== 'has' &&
selector.name !== 'is' &&
selector.name !== 'where' &&
// Not is special because we want to scope as specific as possible, but because :not
// inverses the result, we want to leave the unscoped, too. The exception is more than
// one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped
(selector.name !== 'not' ||
selector.args === null ||
selector.args.children.every((c) => c.children.length === 1))) ||
// selectors with has/is/where/not can also be global if all their children are global
selector.args === null ||
selector.args.children.every((c) => c.children.every((r) => is_global(r))))
);
}
/**
* True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped.
* Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`.
* @param {Css.RelativeSelector} relative_selector
* @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array<Css.PseudoClassSelector | Css.PseudoElementSelector>] }}
*/
export function is_outer_global(relative_selector) {
const first = relative_selector.selectors[0];
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
(first.args === null ||
// Only these two selector types can keep the whole selector global, because e.g.
// :global(button).x means that the selector is still scoped because of the .x
relative_selector.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
))
);
}

@ -276,6 +276,8 @@ export function analyze_component(root, source, options) {
/** @type {Template} */
const template = { ast: root.fragment, scope, scopes };
let synthetic_stores_legacy_check = [];
// create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -351,8 +353,12 @@ export function analyze_component(root, source, options) {
}
}
// we push to the array because at this moment in time we can't be sure if we are in legacy
// mode yet because we are still changing the module scope
synthetic_stores_legacy_check.push(() => {
// if we are creating a synthetic binding for a let declaration we should also declare
// the declaration as state in case it's reassigned
// the declaration as state in case it's reassigned and we are not in runes mode (the function will
// not be called if we are not in runes mode, that's why there's no !runes check here)
if (
declaration !== null &&
declaration.kind === 'normal' &&
@ -361,6 +367,7 @@ export function analyze_component(root, source, options) {
) {
declaration.kind = 'state';
}
});
const binding = instance.scope.declare(b.id(name), 'store_sub', 'synthetic');
binding.references = references;
@ -373,6 +380,12 @@ export function analyze_component(root, source, options) {
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
if (!runes) {
for (let check of synthetic_stores_legacy_check) {
check();
}
}
if (runes && root.module) {
const context = root.module.attributes.find((attribute) => attribute.name === 'context');
if (context) {
@ -461,6 +474,10 @@ export function analyze_component(root, source, options) {
}
} else {
for (const specifier of node.specifiers) {
if (specifier.local.type !== 'Identifier' || specifier.exported.type !== 'Identifier') {
continue;
}
const binding = instance.scope.get(specifier.local.name);
if (

@ -1,7 +1,7 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import { is_capture_event, is_delegated } from '../../../../utils.js';
import { is_boolean_attribute, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
@ -16,14 +16,23 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function Attribute(node, context) {
context.next();
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
// special case
if (node.name === 'value') {
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
if (parent.type === 'RegularElement' && parent.name === 'option') {
mark_subtree_dynamic(context.path);
}
}
if (node.name.startsWith('on')) {
mark_subtree_dynamic(context.path);
}
if (parent.type === 'RegularElement' && is_boolean_attribute(node.name.toLowerCase())) {
node.metadata.expression.can_inline = false;
}
if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
@ -37,6 +46,7 @@ export function Attribute(node, context) {
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
node.metadata.expression.can_inline &&= chunk.metadata.expression.can_inline;
}
if (is_event_attribute(node)) {

@ -178,6 +178,7 @@ export function CallExpression(node, context) {
if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
context.state.expression.can_inline = false;
}
}
}

@ -60,6 +60,8 @@ export function ExportNamedDeclaration(node, context) {
if (!context.state.ast_type /* .svelte.js module */ || context.state.ast_type === 'module') {
for (const specified of node.specifiers) {
if (specified.local.type !== 'Identifier') continue;
const binding = context.state.scope.get(specified.local.name);
if (!binding) continue;

@ -9,18 +9,25 @@ import * as e from '../../../errors.js';
* @param {Context} context
*/
export function ExportSpecifier(node, context) {
const local_name =
node.local.type === 'Identifier' ? node.local.name : /** @type {string} */ (node.local.value);
const exported_name =
node.exported.type === 'Identifier'
? node.exported.name
: /** @type {string} */ (node.exported.value);
if (context.state.ast_type === 'instance') {
if (context.state.analysis.runes) {
context.state.analysis.exports.push({
name: node.local.name,
alias: node.exported.name
name: local_name,
alias: exported_name
});
const binding = context.state.scope.get(node.local.name);
const binding = context.state.scope.get(local_name);
if (binding) binding.reassigned = binding.updated = true;
}
} else {
validate_export(node, context.state.scope, node.local.name);
validate_export(node, context.state.scope, local_name);
}
}

@ -2,7 +2,6 @@
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.ExpressionTag} node
@ -15,9 +14,5 @@ export function ExpressionTag(node, context) {
}
}
// TODO ideally we wouldn't do this here, we'd just do it on encountering
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -1,5 +1,4 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
@ -20,8 +19,6 @@ export function Identifier(node, context) {
return;
}
mark_subtree_dynamic(context.path);
// If we are using arguments outside of a function, then throw an error
if (
node.name === 'arguments' &&
@ -87,6 +84,12 @@ export function Identifier(node, context) {
}
}
// no binding means global, and we can't inline e.g. `<span>{location}</span>`
// because it could change between component renders. if there _is_ a
// binding and it is outside module scope, the expression cannot
// be inlined (TODO allow inlining in more cases - e.g. primitive consts)
let can_inline = !!binding && !binding.scope.parent && binding.kind === 'normal';
if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
@ -122,4 +125,17 @@ export function Identifier(node, context) {
w.reactive_declaration_module_script_dependency(node);
}
}
if (!can_inline && context.state.expression) {
context.state.expression.can_inline = false;
}
/**
* if the identifier is part of an expression tag of an attribute we want to check if it's inlinable
* before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
* directly making the whole thing actually static.
*/
if (!can_inline || !context.path.find((node) => node.type === 'Attribute')) {
mark_subtree_dynamic(context.path);
}
}

@ -18,8 +18,9 @@ export function ImportDeclaration(node, context) {
for (const specifier of node.specifiers) {
if (specifier.type === 'ImportSpecifier') {
if (
specifier.imported.name === 'beforeUpdate' ||
specifier.imported.name === 'afterUpdate'
specifier.imported.type === 'Identifier' &&
(specifier.imported.name === 'beforeUpdate' ||
specifier.imported.name === 'afterUpdate')
) {
e.runes_mode_invalid_import(specifier, specifier.imported.name);
}

@ -19,6 +19,7 @@ export function MemberExpression(node, context) {
if (context.state.expression && !is_pure(node, context)) {
context.state.expression.has_state = true;
context.state.expression.can_inline = false;
}
if (!is_safe_identifier(node, context.state.scope)) {

@ -10,6 +10,7 @@ export function TaggedTemplateExpression(node, context) {
if (context.state.expression && !is_pure(node.tag, context)) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
context.state.expression.can_inline = false;
}
if (node.tag.type === 'Identifier') {

@ -3,16 +3,16 @@
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES,
PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
PROPS_IS_UPDATED
} from '../../../../constants.js';
import { dev } from '../../../state.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js';
import { get_value } from './visitors/shared/declarations.js';
/**
@ -311,18 +311,3 @@ export function create_derived_block_argument(node, context) {
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}
/**
* Whether a variable can be referenced directly from template string.
* @param {import('#compiler').Binding | undefined} binding
* @returns {boolean}
*/
export function can_inline_variable(binding) {
return (
!!binding &&
// in a `<script module>` block
!binding.scope.parent &&
// to prevent the need for escaping
binding.initial?.type === 'Literal'
);
}

@ -196,7 +196,7 @@ export function EachBlock(node, context) {
// forbidden in runes mode
return b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
index,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);
}
@ -208,7 +208,7 @@ export function EachBlock(node, context) {
const left = b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
index,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);

@ -1,4 +1,4 @@
/** @import { Expression, Identifier, Statement } from 'estree' */
/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@ -141,14 +141,14 @@ export function Fragment(node, context) {
const id = b.id(context.state.scope.generate('fragment'));
const use_space_template =
trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
if (use_space_template) {
// special case — we can use `$.text` instead of creating a unique template
const id = b.id(context.state.scope.generate('text'));
process_children(trimmed, () => id, false, {
process_children(trimmed, () => id, null, {
...context,
state
});
@ -158,12 +158,12 @@ export function Fragment(node, context) {
} else {
if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
process_children(trimmed, () => b.id('$$anchor'), null, { ...context, state });
} else {
/** @type {(is_text: boolean) => Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
process_children(trimmed, expression, false, { ...context, state });
process_children(trimmed, expression, null, { ...context, state });
let flags = TEMPLATE_FRAGMENT;
@ -212,12 +212,34 @@ function join_template(items) {
let quasi = b.quasi('');
const template = b.template([quasi], []);
/**
* @param {Expression} expression
*/
function push(expression) {
if (expression.type === 'TemplateLiteral') {
for (let i = 0; i < expression.expressions.length; i += 1) {
const q = expression.quasis[i];
const e = expression.expressions[i];
quasi.value.cooked += /** @type {string} */ (q.value.cooked);
push(e);
}
const last = /** @type {TemplateElement} */ (expression.quasis.at(-1));
quasi.value.cooked += /** @type {string} */ (last.value.cooked);
} else if (expression.type === 'Literal') {
/** @type {string} */ (quasi.value.cooked) += expression.value;
} else {
template.expressions.push(expression);
template.quasis.push((quasi = b.quasi('')));
}
}
for (const item of items) {
if (typeof item === 'string') {
quasi.value.cooked += item;
} else {
template.expressions.push(item);
template.quasis.push((quasi = b.quasi('')));
push(item);
}
}

@ -1,41 +1,36 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import { escape_html } from '../../../../../escaping.js';
import {
is_boolean_attribute,
is_dom_property,
is_load_error_element,
is_void
} from '../../../../../utils.js';
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import {
get_attribute_expression,
is_event_attribute,
is_text_attribute
} from '../../../../utils/ast.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter, can_inline_variable, create_derived } from '../utils.js';
import { build_getter, create_derived } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_set_attributes,
build_style_directives,
build_set_attributes
get_attribute_name
} from './shared/element.js';
import { visit_event_attribute } from './shared/events.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
build_template_literal,
build_template_chunk,
build_update,
build_update_assignment,
get_states_and_calls
build_update_assignment
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
* @param {AST.RegularElement} node
@ -357,28 +352,32 @@ export function RegularElement(node, context) {
// special case — if an element that only contains text, we don't need
// to descend into it if the text is non-reactive
const states_and_calls =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.some((node) => node.type === 'ExpressionTag') &&
get_states_and_calls(trimmed);
const is_text = trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
// in the rare case that we have static text that can't be inlined
// (e.g. `<span>{location}</span>`), set `textContent` programmatically
const use_text_content =
is_text &&
trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) &&
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
if (use_text_content) {
let { value } = build_template_chunk(trimmed, context.visit, child_state);
if (states_and_calls && states_and_calls.states === 0) {
child_state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, 'textContent'),
build_template_literal(trimmed, context.visit, child_state).value
)
)
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
);
} else {
/** @type {Expression} */
let arg = context.state.node;
// If `hydrate_node` is set inside the element, we need to reset it
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// after the element has been hydrated (we don't need to reset if it's been inlined)
let needs_reset = !trimmed.every(
(node) =>
node.type === 'Text' ||
(node.type === 'ExpressionTag' && node.metadata.expression.can_inline)
);
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
@ -388,7 +387,7 @@ export function RegularElement(node, context) {
arg = b.member(arg, 'content');
}
process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), true, {
process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), node, {
...context,
state: child_state
});
@ -581,13 +580,6 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
);
}
const inlinable_expression =
attribute.value === true
? false // not an expression
: is_inlinable_expression(
Array.isArray(attribute.value) ? attribute.value : [attribute.value],
context.state
);
if (attribute.metadata.expression.has_state) {
if (has_call) {
state.init.push(build_update(update));
@ -595,38 +587,44 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
state.update.push(update);
}
return true;
} else {
if (inlinable_expression) {
context.state.template.push(` ${name}="`, value, '"');
} else {
state.init.push(update);
}
return false;
// we need to special case textarea value because it's not an actual attribute
const can_inline =
(attribute.name !== 'value' || element.name !== 'textarea') &&
attribute.metadata.expression.can_inline;
if (can_inline) {
/** @type {Literal | undefined} */
let literal = undefined;
if (value.type === 'Literal') {
literal = value;
} else if (value.type === 'Identifier') {
const binding = context.state.scope.get(value.name);
if (binding && binding.initial?.type === 'Literal' && !binding.reassigned) {
literal = binding.initial;
}
}
}
/**
* @param {(AST.Text | AST.ExpressionTag)[]} nodes
* @param {import('../types.js').ComponentClientTransformState} state
*/
function is_inlinable_expression(nodes, state) {
let has_expression_tag = false;
for (let value of nodes) {
if (value.type === 'ExpressionTag') {
if (value.expression.type === 'Identifier') {
const binding = state.scope
.owner(value.expression.name)
?.declarations.get(value.expression.name);
if (!can_inline_variable(binding)) {
return false;
if (literal && escape_html(literal.value, true) === String(literal.value)) {
if (is_boolean_attribute(name)) {
if (literal.value) {
context.state.template.push(` ${name}`);
}
} else {
return false;
context.state.template.push(` ${name}="`, value, '"');
}
has_expression_tag = true;
} else {
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
} else {
state.init.push(update);
}
return has_expression_tag;
return false;
}
/**

@ -1,14 +1,14 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { build_template_literal } from './shared/utils.js';
import { build_template_chunk } from './shared/utils.js';
/**
* @param {AST.TitleElement} node
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
const { has_state, value } = build_template_literal(
const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes),
context.visit,
context.state

@ -6,7 +6,7 @@ import { is_ignored } from '../../../../../state.js';
import { get_attribute_expression, is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_getter, create_derived } from '../../utils.js';
import { build_template_literal, build_update } from './utils.js';
import { build_template_chunk, build_update } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -200,7 +200,7 @@ export function build_attribute_value(value, context) {
};
}
return build_template_literal(value, context.visit, context.state);
return build_template_chunk(value, context.visit, context.state);
}
/**

@ -1,9 +1,11 @@
/** @import { Expression } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { Scope } from '../../../../scope.js' */
/** @import { ComponentContext } from '../../types' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { escape_html } from '../../../../../../escaping.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_template_literal, build_update } from './utils.js';
import { build_template_chunk, build_update } from './utils.js';
/**
* Processes an array of template nodes, joining sibling text/expression nodes
@ -11,10 +13,10 @@ import { build_template_literal, build_update } from './utils.js';
* corresponding template node references these updates are applied to.
* @param {SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} initial
* @param {boolean} is_element
* @param {AST.RegularElement | null} element
* @param {ComponentContext} context
*/
export function process_children(nodes, initial, is_element, { visit, state }) {
export function process_children(nodes, initial, element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable;
let prev = initial;
let skipped = 0;
@ -60,16 +62,17 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
* @param {Sequence} sequence
*/
function flush_sequence(sequence) {
if (sequence.every((node) => node.type === 'Text')) {
const { has_state, has_call, value, can_inline } = build_template_chunk(sequence, visit, state);
if (can_inline) {
skipped += 1;
state.template.push(sequence.map((node) => node.raw).join(''));
const raw = element?.name === 'script' || element?.name === 'style';
state.template.push(raw ? value : escape_inline_expression(value, state.scope));
return;
}
state.template.push(' ');
const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
// if this is a standalone `{expression}`, make sure we handle the case where
// no text node was created because the expression was empty during SSR
const is_text = sequence.length === 1;
@ -99,7 +102,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
if (is_static_element(node)) {
skipped += 1;
} else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
} else if (node.type === 'EachBlock' && nodes.length === 1 && element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');
@ -128,6 +131,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function is_static_element(node) {
if (node.type !== 'RegularElement') return false;
if (node.fragment.metadata.dynamic) return false;
if (node.name.includes('-')) return false; // we're setting all attributes on custom elements through properties
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') {
@ -138,22 +142,62 @@ function is_static_element(node) {
return false;
}
if (attribute.value !== true && !is_text_attribute(attribute)) {
if (attribute.name === 'autofocus' || attribute.name === 'muted') {
return false;
}
if (attribute.name === 'autofocus' || attribute.name === 'muted') {
if (node.name === 'option' && attribute.name === 'value') {
return false;
}
if (node.name === 'option' && attribute.name === 'value') {
// We need to apply src and loading after appending the img to the DOM for lazy loading to work
if (node.name === 'img' && attribute.name === 'loading') {
return false;
}
if (node.name.includes('-')) {
return false; // we're setting all attributes on custom elements through properties
if (!attribute.metadata.expression.can_inline) {
return false;
}
}
return true;
}
/**
* @param {Expression} node
* @param {Scope} scope
* @returns {Expression}
*/
function escape_inline_expression(node, scope) {
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
return b.literal(escape_html(node.value));
}
return node;
}
if (node.type === 'TemplateLiteral') {
return b.template(
node.quasis.map((q) => b.quasi(escape_html(q.value.cooked))),
node.expressions.map((expression) => escape_inline_expression(expression, scope))
);
}
/**
* If we can't determine the range of possible values statically, wrap in
* `$.escape(...)`. TODO expand this to cover more cases
*/
let needs_escape = true;
if (node.type === 'Identifier') {
const binding = scope.get(node.name);
// TODO handle more cases
if (binding?.initial?.type === 'Literal' && !binding.reassigned) {
needs_escape = escape_html(binding.initial.value) !== String(binding.initial.value);
}
}
return needs_escape ? b.call('$.escape', node) : node;
}

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super, TemplateLiteral, Node } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
@ -10,87 +10,91 @@ import { create_derived } from '../../utils.js';
import is_reference from 'is-reference';
import { locator } from '../../../../../state.js';
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
*/
export function get_states_and_calls(values) {
let states = 0;
let calls = 0;
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'ExpressionTag') {
if (node.metadata.expression.has_call) {
calls++;
}
if (node.metadata.expression.has_state) {
states++;
}
}
}
return { states, calls };
}
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @returns {{ value: Expression, has_state: boolean, has_call: boolean }}
* @returns {{ value: Expression, has_state: boolean, has_call: boolean, can_inline: boolean }}
*/
export function build_template_literal(values, visit, state) {
export function build_template_chunk(values, visit, state) {
/** @type {Expression[]} */
const expressions = [];
let quasi = b.quasi('');
const quasis = [quasi];
const { states, calls } = get_states_and_calls(values);
let has_call = false;
let has_state = false;
let can_inline = true;
let contains_multiple_call_expression = false;
for (const node of values) {
if (node.type === 'ExpressionTag') {
if (node.metadata.expression.has_call) {
if (has_call) contains_multiple_call_expression = true;
has_call = true;
}
if (node.metadata.expression.has_state) {
has_state = true;
}
let has_call = calls > 0;
let has_state = states > 0;
let contains_multiple_call_expression = calls > 1;
if (!node.metadata.expression.can_inline) {
can_inline = false;
}
}
}
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'Text') {
quasi.value.cooked += node.data;
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += node.expression.value + '';
} else {
const expression = /** @type {Expression} */ (visit(node.expression, state));
if (expression.type === 'Literal') {
if (expression.value != null) {
quasi.value.cooked += expression.value + '';
}
} else {
let value = expression;
// if we don't know the value, we need to add `?? ''` to replace
// `null` and `undefined` with the empty string
let needs_fallback = true;
if (value.type === 'Identifier') {
const binding = state.scope.get(value.name);
if (binding && binding.initial?.type === 'Literal' && !binding.reassigned) {
needs_fallback = binding.initial.value === null;
}
}
if (needs_fallback) {
value = b.logical('??', expression, b.literal(''));
}
if (contains_multiple_call_expression) {
const id = b.id(state.scope.generate('stringified_text'));
state.init.push(
b.const(
id,
create_derived(
state,
b.thunk(
b.logical(
'??',
/** @type {Expression} */ (visit(node.expression, state)),
b.literal('')
)
)
)
)
);
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
expressions.push(b.call('$.get', id));
} else if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value: visit(node.expression, state), has_state, has_call };
return { value: visit(node.expression, state), has_state, has_call, can_inline };
} else {
expressions.push(b.logical('??', visit(node.expression, state), b.literal('')));
expressions.push(value);
}
quasi = b.quasi('', i + 1 === values.length);
quasis.push(quasi);
}
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
@ -98,7 +102,7 @@ export function build_template_literal(values, visit, state) {
const value = b.template(quasis, expressions);
return { value, has_state, has_call };
return { value, has_state, has_call, can_inline };
}
/**

@ -292,6 +292,13 @@ const visitors = {
context.state.code.prependRight(global.start, '&');
}
continue;
} else {
// for any :global() or :global at the middle of compound selector
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector, null, context.state);
}
}
}
if (relative_selector.metadata.scoped) {
@ -306,13 +313,6 @@ const visitors = {
}
}
// for any :global() or :global at the middle of compound selector
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector, null, context.state);
}
}
if (relative_selector.selectors.some((s) => s.type === 'NestingSelector')) {
continue;
}

@ -58,6 +58,7 @@ export function create_expression_metadata() {
return {
dependencies: new Set(),
has_state: false,
has_call: false
has_call: false,
can_inline: true
};
}

@ -87,8 +87,8 @@ export namespace Css {
/**
* `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`.
* Selectors like `:global(...).x` are not considered global, because they still need scoping.
* Selectors like `:global(...):is/where/not/has(...)` are considered global even if they aren't
* strictly speaking (we should consolidate the logic around this at some point).
* Selectors like `:global(...):is/where/not/has(...)` are only considered global if all their
* children are global.
*/
is_global: boolean;
/** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */

@ -317,6 +317,8 @@ export interface ExpressionMetadata {
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression can be inlined into a template */
can_inline: boolean;
}
export * from './template.js';

@ -433,7 +433,11 @@ export function is_simple_expression(node) {
}
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
return is_simple_expression(node.left) && is_simple_expression(node.right);
return (
node.left.type !== 'PrivateIdentifier' &&
is_simple_expression(node.left) &&
is_simple_expression(node.right)
);
}
return false;
@ -475,7 +479,10 @@ export function is_expression_async(expression) {
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression': {
return is_expression_async(expression.left) || is_expression_async(expression.right);
return (
(expression.left.type !== 'PrivateIdentifier' && is_expression_async(expression.left)) ||
is_expression_async(expression.right)
);
}
case 'CallExpression':
case 'NewExpression': {

@ -350,7 +350,7 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition}
*/
export function prop_def(key, value, computed = false, is_static = false) {
return { type: 'PropertyDefinition', key, value, computed, static: is_static, decorators: [] };
return { type: 'PropertyDefinition', key, value, computed, static: is_static };
}
/**
@ -551,8 +551,7 @@ export function method(kind, key, params, body, computed = false, is_static = fa
kind,
value: function_builder(null, params, block(body)),
computed,
static: is_static,
decorators: []
static: is_static
};
}

@ -59,6 +59,9 @@ export function get_component() {
}
for (const module of modules) {
if (module.end == null) {
return null;
}
if (module.start.line < entry.line && module.end.line > entry.line) {
return module.component;
}

@ -1,7 +1,8 @@
/** @import { Effect, TemplateNode } from '#client' */
import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { safe_not_equal } from '../../reactivity/equality.js';
import { not_equal, safe_not_equal } from '../../reactivity/equality.js';
import { is_runes } from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
/**
@ -24,8 +25,10 @@ export function key_block(node, get_key, render_fn) {
/** @type {Effect} */
var effect;
var changed = is_runes() ? not_equal : safe_not_equal;
block(() => {
if (safe_not_equal(key, (key = get_key()))) {
if (changed(key, (key = get_key()))) {
if (effect) {
pause_effect(effect);
}

@ -1,4 +1,5 @@
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { escape_html as escape } from '../../escaping.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
@ -155,6 +156,7 @@ export {
$window as window,
$document as document
} from './dom/operations.js';
export { attr } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js';
export { noop, fallback } from '../shared/utils.js';
export {

@ -15,6 +15,15 @@ export function safe_not_equal(a, b) {
: a !== b || (a !== null && typeof a === 'object') || typeof a === 'function';
}
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function not_equal(a, b) {
return a !== b;
}
/** @type {Equals} */
export function safe_equals(value) {
return !safe_not_equal(value, this.v);

@ -13,7 +13,7 @@ import { derived, derived_safe_equal } from './deriveds.js';
import {
active_effect,
get,
is_signals_recorded,
captured_signals,
set_active_effect,
untrack,
update
@ -394,7 +394,7 @@ export function prop(props, key, flags, fallback) {
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
// legacy nonsense — need to ensure the source is invalidated when necessary
// also needed for when handling inspect logic so we can inspect the correct source signal
if (is_signals_recorded) {
if (captured_signals !== null) {
// set this so that we don't reset to the parent value if `d`
// is invalidated because of `invalidate_inner_signals` (rather
// than because the parent or child value changed)

@ -128,8 +128,8 @@ let current_version = 0;
// to prevent memory leaks, we skip adding the reaction.
export let skip_reaction = false;
// Handle collecting all signals which are read during a specific time frame
export let is_signals_recorded = false;
let captured_signals = new Set();
/** @type {Set<Value> | null} */
export let captured_signals = null;
// Handling runtime component context
/** @type {ComponentContext | null} */
@ -732,7 +732,7 @@ export function get(signal) {
return value;
}
if (is_signals_recorded) {
if (captured_signals !== null) {
captured_signals.add(signal);
}
@ -800,21 +800,18 @@ export function safe_get(signal) {
* @param {() => any} fn
*/
export function invalidate_inner_signals(fn) {
var previous_is_signals_recorded = is_signals_recorded;
var previous_captured_signals = captured_signals;
is_signals_recorded = true;
captured_signals = new Set();
var captured = captured_signals;
var signal;
try {
untrack(fn);
} finally {
is_signals_recorded = previous_is_signals_recorded;
if (is_signals_recorded) {
if (previous_captured_signals !== null) {
for (signal of captured_signals) {
previous_captured_signals.add(signal);
}
}
} finally {
captured_signals = previous_captured_signals;
}
for (signal of captured) {

@ -2,6 +2,7 @@
/** @import { Component, Payload, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
import { attr } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
@ -153,33 +154,6 @@ export function head(payload, fn) {
head_payload.out += BLOCK_CLOSE;
}
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
* may be other odd cases that need to be added to this list in future
* @type {Record<string, Map<any, string>>}
*/
const replacements = {
translate: new Map([
[true, 'yes'],
[false, 'no']
])
};
/**
* @template V
* @param {string} name
* @param {V} value
* @param {boolean} [is_boolean]
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}
/**
* @param {Payload} payload
* @param {boolean} is_html
@ -549,6 +523,8 @@ export function once(get_value) {
};
}
export { attr };
export { html } from './blocks/html.js';
export { push, pop } from './context.js';

@ -0,0 +1,28 @@
import { escape_html } from '../../escaping.js';
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
* may be other odd cases that need to be added to this list in future
* @type {Record<string, Map<any, string>>}
*/
const replacements = {
translate: new Map([
[true, 'yes'],
[false, 'no']
])
};
/**
* @template V
* @param {string} name
* @param {V} value
* @param {boolean} [is_boolean]
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}

@ -30,7 +30,7 @@ export class SvelteDate extends Date {
);
for (const method of methods) {
if (method.startsWith('get') || method.startsWith('to')) {
if (method.startsWith('get') || method.startsWith('to') || method === 'valueOf') {
// @ts-ignore
proto[method] = function (...args) {
// don't memoize if there are arguments

@ -588,6 +588,30 @@ test('Date.toLocaleString', () => {
cleanup();
});
test('Date.valueOf', () => {
const date = new SvelteDate(initial_date);
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(date.valueOf());
});
});
flushSync();
assert.deepEqual(log, [initial_date.valueOf()]);
flushSync(() => {
date.setTime(date.getTime() + 10);
});
assert.deepEqual(log, [initial_date.valueOf(), new Date(initial_date.getTime() + 10).valueOf()]);
cleanup();
});
test('Date.instanceOf', () => {
assert.equal(new SvelteDate() instanceof Date, true);
});

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

@ -44,6 +44,20 @@ export default test({
character: 401
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
line: 40,
column: 1,
character: 422
},
end: {
line: 40,
column: 27,
character: 448
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"',
@ -127,6 +141,48 @@ export default test({
column: 11,
character: 1134
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(~ y)"',
start: {
line: 121,
column: 1,
character: 1326
},
end: {
line: 121,
column: 11,
character: 1336
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"',
start: {
line: 129,
column: 2,
character: 1409
},
end: {
line: 129,
column: 15,
character: 1422
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"',
start: {
line: 135,
column: 2,
character: 1480
},
end: {
line: 135,
column: 16,
character: 1494
}
}
]
});

@ -27,9 +27,9 @@
/* (unused) x:has(.unused) {
color: red;
}*/
.foo:has(.unused.svelte-xyz) {
/* (unused) :global(.foo):has(.unused) {
color: red;
}
}*/
x.svelte-xyz:has(y:where(.svelte-xyz) /* (unused) .unused*/) {
color: green;
@ -101,3 +101,28 @@
x.svelte-xyz:has(y:where(.svelte-xyz)) + c:where(.svelte-xyz) {
color: green;
}
x.svelte-xyz:has(+ c:where(.svelte-xyz)) {
color: green;
}
x.svelte-xyz:has(~ c:where(.svelte-xyz)) {
color: green;
}
/* (unused) x:has(~ y) {
color: red;
}*/
.foo {
.svelte-xyz:has(x:where(.svelte-xyz)) {
color: green;
}
/* (unused) :has(.unused) {
color: red;
}*/
&:has(x.svelte-xyz) {
color: green;
}
/* (unused) &:has(.unused) {
color: red;
}*/
}

@ -111,4 +111,29 @@
x:has(y) + c {
color: green;
}
x:has(+ c) {
color: green;
}
x:has(~ c) {
color: green;
}
x:has(~ y) {
color: red;
}
:global(.foo) {
:has(x) {
color: green;
}
:has(.unused) {
color: red;
}
&:has(x) {
color: green;
}
&:has(.unused) {
color: red;
}
}
</style>

@ -30,20 +30,6 @@ export default test({
character: 125
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
start: {
line: 14,
column: 7,
character: 117
},
end: {
line: 14,
column: 14,
character: 124
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo) :is(.unused)"',
@ -60,16 +46,30 @@ export default test({
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
message: 'Unused CSS selector ":global(.foo):is(.unused)"',
start: {
line: 28,
column: 19,
character: 292
line: 34,
column: 1,
character: 363
},
end: {
line: 28,
line: 34,
column: 26,
character: 299
character: 388
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":is(.unused)"',
start: {
line: 52,
column: 2,
character: 636
},
end: {
line: 52,
column: 14,
character: 648
}
}
]

@ -22,6 +22,12 @@
/* (unused) :global(.foo) :is(.unused) {
color: red;
}*/
.foo:is(x.svelte-xyz) {
color: green;
}
/* (unused) :global(.foo):is(.unused) {
color: red;
}*/
x.svelte-xyz :is(html *) {
color: green;
@ -32,3 +38,12 @@
y.svelte-xyz :is(x:where(.svelte-xyz) :where(.svelte-xyz)) {
color: green; /* matches z */
}
.foo {
:is(x.svelte-xyz) {
color: green;
}
/* (unused) :is(.unused) {
color: red;
}*/
}

@ -28,6 +28,12 @@
:global(.foo) :is(.unused) {
color: red;
}
:global(.foo):is(x) {
color: green;
}
:global(.foo):is(.unused) {
color: red;
}
x :is(:global(html *)) {
color: green;
@ -38,4 +44,13 @@
y :is(x *) {
color: green; /* matches z */
}
:global(.foo) {
:is(x) {
color: green;
}
:is(.unused) {
color: red;
}
}
</style>

@ -27,3 +27,12 @@
span:not(p span) {
color: green;
}
.x {
.svelte-xyz:not(.foo) {
color: green;
}
&:not(.foo) {
color: green;
}
}

@ -34,4 +34,13 @@
:global(span:not(p span)) {
color: green;
}
:global(.x) {
:not(.foo) {
color: green;
}
&:not(.foo) {
color: green;
}
}
</style>

@ -1,3 +1,76 @@
import { test } from '../../test';
export default test({});
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":root .unused"',
start: {
line: 18,
column: 2,
character: 190
},
end: {
line: 18,
column: 15,
character: 203
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":root:has(.unused)"',
start: {
line: 25,
column: 2,
character: 269
},
end: {
line: 25,
column: 20,
character: 287
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
start: {
line: 37,
column: 4,
character: 401
},
end: {
line: 37,
column: 11,
character: 408
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"',
start: {
line: 43,
column: 4,
character: 480
},
end: {
line: 43,
column: 17,
character: 493
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"',
start: {
line: 49,
column: 4,
character: 566
},
end: {
line: 49,
column: 18,
character: 580
}
}
]
});

@ -1,9 +1,52 @@
:root {
color: red;
color: green;
}
.foo:root {
color: blue;
color: green;
}
:root.foo {
color: green;
}
:root.unknown {
color: green;
}
:root h1.svelte-xyz {
color: green;
}
/* (unused) :root .unused {
color: red;
}*/
:root:has(h1:where(.svelte-xyz)) {
color: green;
}
/* (unused) :root:has(.unused) {
color: red;
}*/
:root:not(.x) {
color: green;
}
:root {
h1.svelte-xyz {
color: green;
}
/* (unused) .unused {
color: red;
}*/
.svelte-xyz:has(h1:where(.svelte-xyz)) {
color: green;
}
/* (unused) :has(.unused) {
color: red;
}*/
&:has(h1.svelte-xyz) {
color: green;
}
/* (unused) &:has(.unused) {
color: red;
}*/
}

@ -1 +1 @@
<h1>Hello!</h1>
<h1 class="svelte-xyz">Hello!</h1>

@ -1,13 +1,55 @@
<style>
:root {
color: red;
color: green;
}
.foo:root {
color: blue;
color: green;
}
:root.foo {
color: green;
}
:root.unknown {
color: green;
}
:root h1 {
color: green;
}
:root .unused {
color: red;
}
:root:has(h1) {
color: green;
}
:root:has(.unused) {
color: red;
}
:root:not(.x) {
color: green;
}
:root {
h1 {
color: green;
}
.unused {
color: red;
}
:has(h1) {
color: green;
}
:has(.unused) {
color: red;
}
&:has(h1) {
color: green;
}
&:has(.unused) {
color: red;
}
}
</style>
<h1>Hello!</h1>

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,6 @@
<script>
let bindable;
export let something;
</script>
<input bind:value={something} />

@ -0,0 +1,8 @@
<!-- @migration-task Error while migrating Svelte code: migrating this component would require adding a `$bindable` rune but there's already a variable named bindable.
Rename the variable and try again or migrate by hand. -->
<script>
let bindable;
export let something;
</script>
<input bind:value={something} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,9 @@
<script>
let name = 'world';
let derived;
$: other = name;
</script>
<input bind:value={name} />

@ -0,0 +1,11 @@
<!-- @migration-task Error while migrating Svelte code: can't migrate `$: other = name;` to `$derived` because there's a variable named derived.
Rename the variable and try again or migrate by hand. -->
<script>
let name = 'world';
let derived;
$: other = name;
</script>
<input bind:value={name} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,5 @@
<script>
let derived;
</script>
<svelte:component this={derived} />

@ -0,0 +1,7 @@
<!-- @migration-task Error while migrating Svelte code: migrating this component would require adding a `$derived` rune but there's already a variable named derived.
Rename the variable and try again or migrate by hand. -->
<script>
let derived;
</script>
<svelte:component this={derived} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,6 @@
<script>
let derived;
</script>
<Component>
<slot name="derived" slot="derived" />
</Component>

@ -0,0 +1,7 @@
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot making the component unusable -->
<script>
let derived;
</script>
<Component>
<slot name="derived" slot="derived" />
</Component>

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,10 @@
<script>
let name = 'world';
let derived;
let other;
$: other = name;
</script>
<input bind:value={name} />

@ -0,0 +1,12 @@
<!-- @migration-task Error while migrating Svelte code: can't migrate `let other;` to `$derived` because there's a variable named derived.
Rename the variable and try again or migrate by hand. -->
<script>
let name = 'world';
let derived;
let other;
$: other = name;
</script>
<input bind:value={name} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,6 @@
<!-- @migration-task Error while migrating Svelte code: migrating this component would require adding a `$props` rune but there's already a variable named props.
Rename the variable and try again or migrate by hand. -->
<script>
let props;
export let something;
</script>

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,7 @@
<script>
let state = 'world';
let other;
</script>
<input bind:value={other} />

@ -0,0 +1,9 @@
<!-- @migration-task Error while migrating Svelte code: can't migrate `let other;` to `$state` because there's a variable named state.
Rename the variable and try again or migrate by hand. -->
<script>
let state = 'world';
let other;
</script>
<input bind:value={other} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,7 @@
<script>
let state = 'world';
let other = 42;
</script>
<input bind:value={other} />

@ -0,0 +1,9 @@
<!-- @migration-task Error while migrating Svelte code: can't migrate `let other = 42;` to `$state` because there's a variable named state.
Rename the variable and try again or migrate by hand. -->
<script>
let state = 'world';
let other = 42;
</script>
<input bind:value={other} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
logs: [
'One or more `@migration-task` comments were added to `output.svelte`, please check them and complete the migration manually.'
],
errors: []
});

@ -0,0 +1,7 @@
<script>
let state = 'world';
$: other = 42;
</script>
<input bind:value={other} />

@ -0,0 +1,9 @@
<!-- @migration-task Error while migrating Svelte code: can't migrate `$: other = 42;` to `$state` because there's a variable named state.
Rename the variable and try again or migrate by hand. -->
<script>
let state = 'world';
$: other = 42;
</script>
<input bind:value={other} />

@ -47,6 +47,8 @@ what if i'm talking about `:has()` in my blog?
div :where(:global(.class:is(span:has(* > *)))){}
div :is(:global(.class:is(span:is(:hover)), .x)){}
:global(button:has(.is-active)){}
div{
p:has(&){

@ -47,6 +47,8 @@ what if i'm talking about `:has()` in my blog?
div :where(:global(.class:is(span:has(* > *)))){}
div :is(:global(.class:is(span:is(:hover)), .x)){}
:global(button:has(.is-active)){}
div{
p:has(:global(&)){

@ -13,3 +13,13 @@
</MyComponent>
</div>
</MyInput>
<MyInput let:args>
<slot/>
</MyInput>
<MyInput>
<div let:args>
<slot/>
</div>
</MyInput>

@ -2,11 +2,13 @@
/**
* @typedef {Object} Props
* @property {import('svelte').Snippet} [label]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { label } = $props();
let { label, children } = $props();
const label_render = $derived(label);
const children_render = $derived(children);
</script>
@ -31,3 +33,17 @@
</div>
{/snippet}
</MyInput>
<MyInput >
{#snippet children({ args })}
{@render children_render?.()}
{/snippet}
</MyInput>
<MyInput>
<div >
{#snippet children({ args })}
{@render children_render?.()}
{/snippet}
</div>
</MyInput>

@ -0,0 +1,5 @@
<MyComponent
variant="outlined"
>
<slot />
</MyComponent>

@ -0,0 +1,15 @@
<script>
/**
* @typedef {Object} Props
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { children } = $props();
</script>
<MyComponent
variant="outlined"
>
{@render children?.()}
</MyComponent>

@ -14,6 +14,12 @@
</div>
</Comp>
<Comp>
<div slot="new">
reserved keyword
</div>
</Comp>
<Comp>
<div slot="stuff">
cool

@ -16,6 +16,13 @@
</div>
</Comp>
<Comp>
<!-- @migration-task: migrate this slot by hand, `new` is an invalid identifier -->
<div slot="new">
reserved keyword
</div>
</Comp>
<Comp>
{#snippet stuff()}
<div >

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
use_ts: true
});

@ -0,0 +1,5 @@
<script>
// script tag but no lang="ts", because for example only imports present
</script>
<slot />

@ -0,0 +1,10 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
// script tag but no lang="ts", because for example only imports present
</script>
{@render children?.()}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
use_ts: true
});

@ -0,0 +1,6 @@
<script>
/** @type {ShouldNotUseTSBecauseImUsingJsDoc} */
export let data;
</script>
<slot />

@ -0,0 +1,13 @@
<script>
/**
* @typedef {Object} Props
* @property {ShouldNotUseTSBecauseImUsingJsDoc} data
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { data, children } = $props();
</script>
{@render children?.()}

@ -1,4 +1,4 @@
<script>
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}

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

Loading…
Cancel
Save