Merge branch 'main' into bindable-types

pull/11225/head
Simon Holthausen 2 years ago
commit b9b0235f50

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: introduce `$host` rune, deprecate `createEventDispatcher`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: execute sole static script tag

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: make static `element` property available for the SvelteComponent type

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve internal proxied state signal heuristic

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: robustify interop of exports and props in runes mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure deep mutation ownership widening

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve compiled output of multiple call expression in single text node

@ -132,6 +132,7 @@
"happy-suits-film", "happy-suits-film",
"healthy-planes-vanish", "healthy-planes-vanish",
"heavy-comics-move", "heavy-comics-move",
"heavy-ducks-leave",
"heavy-ears-rule", "heavy-ears-rule",
"hip-balloons-begin", "hip-balloons-begin",
"honest-buses-add", "honest-buses-add",
@ -250,6 +251,7 @@
"red-feet-worry", "red-feet-worry",
"red-poets-study", "red-poets-study",
"rich-cobras-exist", "rich-cobras-exist",
"rich-garlics-laugh",
"rich-olives-yell", "rich-olives-yell",
"rich-sheep-burn", "rich-sheep-burn",
"rich-tables-sing", "rich-tables-sing",
@ -366,6 +368,7 @@
"three-icons-trade", "three-icons-trade",
"three-lions-visit", "three-lions-visit",
"three-papayas-buy", "three-papayas-buy",
"three-rice-tie",
"three-suits-grin", "three-suits-grin",
"tidy-buses-whisper", "tidy-buses-whisper",
"tidy-starfishes-allow", "tidy-starfishes-allow",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve hydration of svelte head blocks

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve handled of unowned derived signals

@ -1,5 +1,15 @@
# svelte # svelte
## 5.0.0-next.95
### Patch Changes
- breaking: robustify interop of exports and props in runes mode ([#11064](https://github.com/sveltejs/svelte/pull/11064))
- fix: improve handled of unowned derived signals ([#11077](https://github.com/sveltejs/svelte/pull/11077))
- fix: bundle CSS types ([#11067](https://github.com/sveltejs/svelte/pull/11067))
## 5.0.0-next.94 ## 5.0.0-next.94
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.94", "version": "5.0.0-next.95",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -211,3 +211,24 @@ declare function $bindable<T>(t?: T): T;
declare function $inspect<T extends any[]>( declare function $inspect<T extends any[]>(
...values: T ...values: T
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void }; ): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
/**
* Retrieves the `this` reference of the custom element that contains this component. Example:
*
* ```svelte
* <svelte:options customElement="my-element" />
*
* <script>
* function greet(greeting) {
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
* }
* </script>
*
* <button onclick={() => greet('hello')}>say hello</button>
* ```
*
* Only available inside custom element components, and only on the client-side.
*
* https://svelte-5-preview.vercel.app/docs/runes#$host
*/
declare function $host<El extends HTMLElement = HTMLElement>(): El;

@ -187,6 +187,8 @@ const runes = {
'invalid-state-location': (rune) => 'invalid-state-location': (rune) =>
`${rune}(...) can only be used as a variable declaration initializer or a class field`, `${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-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} is_binding
* @param {boolean} show_details * @param {boolean} show_details
@ -212,7 +214,9 @@ const runes = {
'duplicate-props-rune': () => `Cannot use $props() more than once`, 'duplicate-props-rune': () => `Cannot use $props() more than once`,
'invalid-each-assignment': () => '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')`, `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-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)` '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} */ /** @satisfies {Errors} */

@ -36,7 +36,8 @@ export default function read_style(parser, start, attributes) {
content: { content: {
start: content_start, start: content_start,
end: content_end, end: content_end,
styles: parser.template.slice(content_start, content_end) styles: parser.template.slice(content_start, content_end),
comment: null
} }
}; };
} }

@ -283,25 +283,26 @@ export default function tag(parser) {
if (is_top_level_script_or_style) { if (is_top_level_script_or_style) {
parser.eat('>', true); parser.eat('>', true);
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
/** @type {import('#compiler').Comment | null} */ /** @type {import('#compiler').Comment | null} */
let prev_comment = null; let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) { for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i]; const node = current.fragment.nodes[i];
if (i === current.fragment.nodes.length - 1 && node.end !== start) { if (i === current.fragment.nodes.length - 1 && node.end !== start) {
break; break;
} }
if (node.type === 'Comment') { if (node.type === 'Comment') {
prev_comment = node; prev_comment = node;
break; break;
} else if (node.type !== 'Text' || node.data.trim()) { } else if (node.type !== 'Text' || node.data.trim()) {
break; break;
}
} }
}
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
if (prev_comment) { if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set, // We take advantage of the fact that the root will never have leadingComments set,
// and set the previous comment to it so that the warning mechanism can later // and set the previous comment to it so that the warning mechanism can later
@ -318,6 +319,7 @@ export default function tag(parser) {
} }
} else { } else {
const content = read_style(parser, start, element.attributes); const content = read_style(parser, start, element.attributes);
content.content.comment = prev_comment;
if (current.css) error(start, 'duplicate-style-element'); if (current.css) error(start, 'duplicate-style-element');
current.css = content; current.css = content;

@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
let sibling_matched = false; let sibling_matched = false;
for (const possible_sibling of siblings.keys()) { for (const possible_sibling of siblings.keys()) {
if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
mark(relative_selector, element);
sibling_matched = true;
}
} else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
mark(relative_selector, element); mark(relative_selector, element);
sibling_matched = true; sibling_matched = true;
} }
@ -564,38 +570,39 @@ function get_element_parent(node) {
function find_previous_sibling(node) { function find_previous_sibling(node) {
/** @type {import('#compiler').SvelteNode} */ /** @type {import('#compiler').SvelteNode} */
let current_node = node; let current_node = node;
do {
if (current_node.type === 'SlotElement') { while (
const slot_children = current_node.fragment.nodes; // @ts-expect-error TODO
if (slot_children.length > 0) { !current_node.prev &&
current_node = slot_children.slice(-1)[0]; // go to its last child first // @ts-expect-error TODO
continue; current_node.parent?.type === 'SlotElement'
} ) {
} // @ts-expect-error TODO
while ( current_node = current_node.parent;
// @ts-expect-error TODO }
!current_node.prev &&
// @ts-expect-error TODO // @ts-expect-error
current_node.parent && current_node = current_node.prev;
// @ts-expect-error TODO
current_node.parent.type === 'SlotElement' while (current_node?.type === 'SlotElement') {
) { const slot_children = current_node.fragment.nodes;
// @ts-expect-error TODO if (slot_children.length > 0) {
current_node = current_node.parent; current_node = slot_children.slice(-1)[0];
} else {
break;
} }
// @ts-expect-error }
current_node = current_node.prev;
} while (current_node && current_node.type === 'SlotElement');
return current_node; return current_node;
} }
/** /**
* @param {import('#compiler').SvelteNode} node * @param {import('#compiler').SvelteNode} node
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>} * @returns {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>}
*/ */
function get_possible_element_siblings(node, adjacent_only) { function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */ /** @type {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>} */
const result = new Map(); const result = new Map();
/** @type {import('#compiler').SvelteNode} */ /** @type {import('#compiler').SvelteNode} */
@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) {
if (adjacent_only && has_definite_elements(possible_last_child)) { if (adjacent_only && has_definite_elements(possible_last_child)) {
return result; return result;
} }
} else if (
prev.type === 'SlotElement' ||
prev.type === 'RenderTag' ||
prev.type === 'SvelteElement'
) {
result.set(prev, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
} }
} }
@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) {
} }
/** /**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result * @param {Map<unknown, NodeExistsValue>} result
* @returns {boolean} * @returns {boolean}
*/ */
function has_definite_elements(result) { function has_definite_elements(result) {
@ -734,8 +749,9 @@ function has_definite_elements(result) {
} }
/** /**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from * @template T
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to * @param {Map<T, NodeExistsValue>} from
* @param {Map<T, NodeExistsValue>} to
* @returns {void} * @returns {void}
*/ */
function add_to_map(from, to) { function add_to_map(from, to) {

@ -0,0 +1,34 @@
import { walk } from 'zimmerframe';
import { warn } 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);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { warnings: import('../../types.js').RawWarning[], stylesheet: import('#compiler').Css.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {
context.next();
}
},
PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where') {
context.next();
}
},
ComplexSelector(node, context) {
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);
}
context.next();
}
};

@ -23,6 +23,7 @@ import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
import { analyze_css } from './css/css-analyze.js'; import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js'; import { prune } from './css/css-prune.js';
import { hash } from './utils.js'; import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
/** /**
* @param {import('#compiler').Script | null} script * @param {import('#compiler').Script | null} script
@ -437,6 +438,20 @@ export function analyze_component(root, source, options) {
merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors) merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors)
); );
} }
if (analysis.exports.length > 0) {
for (const [_, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (
analysis.exports.some(
({ alias, name }) => (binding.prop_alias ?? binding.node.name) === (alias ?? name)
)
) {
error(binding.node, 'conflicting-property-name');
}
}
}
}
} else { } else {
instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic'); instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic');
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic'); instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
@ -534,6 +549,7 @@ export function analyze_component(root, source, options) {
for (const element of analysis.elements) { for (const element of analysis.elements) {
prune(analysis.css.ast, element); prune(analysis.css.ast, element);
} }
warn_unused(analysis.css.ast, analysis.warnings);
outer: for (const element of analysis.elements) { outer: for (const element of analysis.elements) {
if (element.metadata.scoped) { if (element.metadata.scoped) {

@ -896,6 +896,9 @@ export const validation_runes_js = {
} }
}, },
CallExpression(node, { state, path }) { CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$host') {
error(node, 'invalid-host-location');
}
validate_call_expression(node, state.scope, path); validate_call_expression(node, state.scope, path);
}, },
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
@ -1063,9 +1066,17 @@ export const validation_runes = merge(validation, a11y_validators, {
} }
}, },
CallExpression(node, { state, path }) { CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) { const rune = get_rune(node, state.scope);
if (rune === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]); error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
} else if (rune === '$host') {
if (node.arguments.length > 0) {
error(node, 'invalid-rune-args-length', '$host', [0]);
} else if (state.ast_type === 'module' || !state.analysis.custom_element) {
error(node, 'invalid-host-location');
}
} }
validate_call_expression(node, state.scope, path); validate_call_expression(node, state.scope, path);
}, },
EachBlock(node, { next, state }) { EachBlock(node, { next, state }) {

@ -255,8 +255,7 @@ export function client_component(source, analysis, options) {
); );
if (analysis.runes && options.dev) { if (analysis.runes && options.dev) {
/** @type {import('estree').Literal[]} */ const bindable = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name));
const bindable = [];
for (const [name, binding] of properties) { for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') { if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name)); bindable.push(b.literal(binding.prop_alias ?? name));
@ -382,7 +381,6 @@ export function client_component(source, analysis, options) {
); );
if (analysis.uses_rest_props) { if (analysis.uses_rest_props) {
/** @type {string[]} */
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name); if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
@ -401,15 +399,12 @@ export function client_component(source, analysis, options) {
} }
if (analysis.uses_props || analysis.uses_rest_props) { if (analysis.uses_props || analysis.uses_rest_props) {
const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')];
if (analysis.custom_element) {
to_remove.push(b.literal('$$host'));
}
component_block.body.unshift( component_block.body.unshift(
b.const( b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove)))
'$$sanitized_props',
b.call(
'$.rest_props',
b.id('$$props'),
b.array([b.literal('children'), b.literal('$$slots'), b.literal('$$events')])
)
)
); );
} }

@ -195,8 +195,7 @@ export const javascript_visitors_runes = {
if (rune === '$props') { if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern'); assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */ const seen = state.analysis.exports.map(({ name, alias }) => alias ?? name);
const seen = [];
for (const property of declarator.id.properties) { for (const property of declarator.id.properties) {
if (property.type === 'Property') { if (property.type === 'Property') {
@ -381,6 +380,10 @@ export const javascript_visitors_runes = {
CallExpression(node, context) { CallExpression(node, context) {
const rune = get_rune(node, context.state.scope); const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('$$props.$$host');
}
if (rune === '$effect.active') { if (rune === '$effect.active') {
return b.call('$.effect_active'); return b.call('$.effect_active');
} }

@ -1396,7 +1396,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
state.template.push(' '); state.template.push(' ');
const [contains_call_expression, value] = serialize_template_literal(sequence, visit); const [contains_call_expression, value] = serialize_template_literal(sequence, visit, state);
const update = b.stmt(b.call('$.set_text', text_id, value)); const update = b.stmt(b.call('$.set_text', text_id, value));
@ -1511,25 +1511,39 @@ function serialize_attribute_value(attribute_value, context) {
} }
} }
return serialize_template_literal(attribute_value, context.visit); return serialize_template_literal(attribute_value, context.visit, context.state);
} }
/** /**
* @param {Array<import('#compiler').Text | import('#compiler').ExpressionTag>} values * @param {Array<import('#compiler').Text | import('#compiler').ExpressionTag>} values
* @param {(node: import('#compiler').SvelteNode) => any} visit * @param {(node: import('#compiler').SvelteNode) => any} visit
* @param {import("../types.js").ComponentClientTransformState} state
* @returns {[boolean, import('estree').TemplateLiteral]} * @returns {[boolean, import('estree').TemplateLiteral]}
*/ */
function serialize_template_literal(values, visit) { function serialize_template_literal(values, visit, state) {
/** @type {import('estree').TemplateElement[]} */ /** @type {import('estree').TemplateElement[]} */
const quasis = []; const quasis = [];
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
const expressions = []; const expressions = [];
let contains_call_expression = false; let contains_call_expression = false;
let contains_multiple_call_expression = false;
quasis.push(b.quasi('')); quasis.push(b.quasi(''));
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
const node = values[i]; const node = values[i];
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
if (contains_call_expression) {
contains_multiple_call_expression = true;
}
contains_call_expression = true;
}
}
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'Text') { if (node.type === 'Text') {
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1)); const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += sanitize_template_string(node.data); last.value.raw += sanitize_template_string(node.data);
@ -1539,11 +1553,23 @@ function serialize_template_literal(values, visit) {
last.value.raw += sanitize_template_string(node.expression.value + ''); last.value.raw += sanitize_template_string(node.expression.value + '');
} }
} else { } else {
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) { if (contains_multiple_call_expression) {
contains_call_expression = true; const id = b.id(state.scope.generate('stringified_text'));
}
expressions.push(b.call('$.stringify', visit(node.expression))); state.init.push(
b.const(
id,
b.call(
// In runes mode, we want things to be fine-grained - but not in legacy mode
state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)))
)
)
);
expressions.push(b.call('$.get', id));
} else {
expressions.push(b.call('$.stringify', visit(node.expression)));
}
quasis.push(b.quasi('', i + 1 === values.length)); quasis.push(b.quasi('', i + 1 === values.length));
} }
} }
@ -1586,7 +1612,7 @@ export const template_visitors = {
declaration.id, declaration.id,
b.call( b.call(
// In runes mode, we want things to be fine-grained - but not in legacy mode // In runes mode, we want things to be fine-grained - but not in legacy mode
state.options.runes ? '$.derived' : '$.derived_safe_equal', state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init))) b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
) )
) )
@ -1623,7 +1649,7 @@ export const template_visitors = {
state.init.push( state.init.push(
// In runes mode, we want things to be fine-grained - but not in legacy mode // In runes mode, we want things to be fine-grained - but not in legacy mode
b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn)) b.const(tmp, b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', fn))
); );
// we need to eagerly evaluate the expression in order to hit any // we need to eagerly evaluate the expression in order to hit any
@ -2972,7 +2998,7 @@ export const template_visitors = {
b.assignment( b.assignment(
'=', '=',
b.member(b.id('$.document'), b.id('title')), b.member(b.id('$.document'), b.id('title')),
serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit)[1] serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit, state)[1]
) )
) )
); );

@ -691,7 +691,8 @@ const javascript_visitors_runes = {
} }
if (rune === '$props') { if (rune === '$props') {
// remove $bindable() from props declaration // remove $bindable() from props declaration and handle rest props
let uses_rest_props = false;
const id = walk(declarator.id, null, { const id = walk(declarator.id, null, {
AssignmentPattern(node) { AssignmentPattern(node) {
if ( if (
@ -703,9 +704,26 @@ const javascript_visitors_runes = {
: b.id('undefined'); : b.id('undefined');
return b.assignment_pattern(node.left, right); return b.assignment_pattern(node.left, right);
} }
},
RestElement(node, { path }) {
if (path.at(-1) === declarator.id) {
uses_rest_props = true;
}
} }
}); });
declarations.push(b.declarator(id, b.id('$$props')));
const exports = /** @type {import('../../types').ComponentAnalysis} */ (
state.analysis
).exports.map(({ name, alias }) => b.literal(alias ?? name));
declarations.push(
b.declarator(
id,
uses_rest_props && exports.length > 0
? b.call('$.rest_props', b.id('$$props'), b.array(exports))
: b.id('$$props')
)
);
continue; continue;
} }
@ -767,6 +785,10 @@ const javascript_visitors_runes = {
CallExpression(node, context) { CallExpression(node, context) {
const rune = get_rune(node, context.state.scope); const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('undefined');
}
if (rune === '$effect.active') { if (rune === '$effect.active') {
return b.literal(false); return b.literal(false);
} }

@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([
'$effect.active', '$effect.active',
'$effect.root', '$effect.root',
'$inspect', '$inspect',
'$inspect().with' '$inspect().with',
'$host'
]); ]);
/** /**

@ -1,3 +1,5 @@
import type { Comment } from '#compiler';
export namespace Css { export namespace Css {
export interface BaseNode { export interface BaseNode {
start: number; start: number;
@ -12,6 +14,8 @@ export namespace Css {
start: number; start: number;
end: number; end: number;
styles: string; styles: string;
/** Possible comment atop the style tag */
comment: Comment | null;
}; };
} }

@ -7,7 +7,8 @@ import {
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
const css = { const css = {
'unused-selector': () => 'Unused CSS selector' /** @param {string} name */
'css-unused-selector': (name) => `Unused CSS selector "${name}"`
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -300,6 +301,11 @@ export function warn(array, node, path, code, ...args) {
) )
); );
} }
// Style nodes
if (current.type === 'StyleSheet' && current.content.comment) {
ignores.push(...current.content.comment.ignores);
}
} }
if (ignores.includes(code)) return; if (ignores.includes(code)) return;

@ -80,6 +80,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* ``` * ```
* *
* https://svelte.dev/docs/svelte#createeventdispatcher * https://svelte.dev/docs/svelte#createeventdispatcher
* @deprecated Use callback props and/or the `$host()` rune instead see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher
* @template {Record<string, any>} [EventMap = any] * @template {Record<string, any>} [EventMap = any]
* @returns {import('./index.js').EventDispatcher<EventMap>} * @returns {import('./index.js').EventDispatcher<EventMap>}
*/ */

@ -74,6 +74,9 @@ export class SvelteComponent<
Events extends Record<string, any> = any, Events extends Record<string, any> = any,
Slots extends Record<string, any> = any Slots extends Record<string, any> = any
> { > {
/** The custom element version of the component. Only present if compiled with the `customElement` compiler option */
static element?: typeof HTMLElement;
[prop: string]: any; [prop: string]: any;
/** /**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which * @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which

@ -2,6 +2,7 @@
import { STATE_SYMBOL } from '../constants.js'; import { STATE_SYMBOL } from '../constants.js';
import { untrack } from '../runtime.js'; import { untrack } from '../runtime.js';
import { get_descriptors } from '../utils.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */ /** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {}; const boundaries = {};
@ -91,49 +92,107 @@ export function mark_module_end() {
} }
} }
let add_owner_visited = new Set();
/** /**
* *
* @param {any} object * @param {any} object
* @param {any} owner * @param {any} owner
*/ */
export function add_owner(object, owner) { export function add_owner(object, owner) {
untrack(() => { // Needed because ownership addition can invoke getters on a proxy,
add_owner_to_object(object, owner); // calling add_owner anew, so just keeping the set as part of
}); // add_owner_to_object would not be enough.
const prev = add_owner_visited;
try {
add_owner_visited = new Set(add_owner_visited);
untrack(() => {
add_owner_to_object(object, owner, add_owner_visited);
});
} finally {
add_owner_visited = prev;
}
} }
/** /**
* @param {any} object * @param {any} object
* @param {Function} owner * @param {Function} owner
* @param {Set<any>} visited
*/ */
function add_owner_to_object(object, owner) { function add_owner_to_object(object, owner, visited) {
if (visited.has(object)) return;
visited.add(object);
if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) { if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) {
object[STATE_SYMBOL].o.add(owner); object[STATE_SYMBOL].o.add(owner);
for (const key in object) {
add_owner_to_object(object[key], owner);
}
} }
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => add_owner_to_object(nested, owner, visited));
} }
let strip_owner_visited = new Set();
/** /**
* @param {any} object * @param {any} object
*/ */
export function strip_owner(object) { export function strip_owner(object) {
untrack(() => { // Needed because ownership stripping can invoke getters on a proxy,
strip_owner_from_object(object); // calling strip_owner anew, so just keeping the set as part of
}); // strip_owner_from_object would not be enough.
const prev = strip_owner_visited;
try {
untrack(() => {
strip_owner_from_object(object, strip_owner_visited);
});
} finally {
strip_owner_visited = prev;
}
} }
/** /**
* @param {any} object * @param {any} object
* @param {Set<any>} visited
*/ */
function strip_owner_from_object(object) { function strip_owner_from_object(object, visited) {
if (visited.has(object)) return;
visited.add(object);
if (object?.[STATE_SYMBOL]?.o) { if (object?.[STATE_SYMBOL]?.o) {
object[STATE_SYMBOL].o = null; object[STATE_SYMBOL].o = null;
}
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => strip_owner_from_object(nested, visited));
}
/**
* @param {any} object
* @param {(obj: any) => void} cb
*/
function traverse_for_owners(object, cb) {
if (typeof object === 'object' && object !== null && !(object instanceof EventTarget)) {
for (const key in object) { for (const key in object) {
strip_owner(object[key]); cb(object[key]);
}
// deal with state on classes
const proto = Object.getPrototypeOf(object);
if (
proto !== Object.prototype &&
proto !== Array.prototype &&
proto !== Map.prototype &&
proto !== Set.prototype &&
proto !== Date.prototype
) {
const descriptors = get_descriptors(proto);
for (let key in descriptors) {
const get = descriptors[key].get;
if (get) {
try {
cb(object[key]);
} catch (e) {
// continue
}
}
}
} }
} }
} }

@ -1,7 +1,16 @@
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js'; import { block } from '../../reactivity/effects.js';
import { HYDRATION_START } from '../../../../constants.js'; import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
/**
* @type {Node | undefined}
*/
let head_anchor;
export function reset_head_anchor() {
head_anchor = undefined;
}
/** /**
* @param {(anchor: Node) => import('#client').Dom | void} render_fn * @param {(anchor: Node) => import('#client').Dom | void} render_fn
@ -19,12 +28,20 @@ export function head(render_fn) {
if (hydrating) { if (hydrating) {
previous_hydrate_nodes = hydrate_nodes; previous_hydrate_nodes = hydrate_nodes;
let anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild); // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) { if (head_anchor === undefined) {
anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling); head_anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild);
}
while (
head_anchor.nodeType !== 8 ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START
) {
head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling);
} }
anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor)); head_anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(head_anchor));
head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling);
} else { } else {
anchor = document.head.appendChild(empty()); anchor = document.head.appendChild(empty());
} }

@ -138,7 +138,8 @@ if (typeof HTMLElement === 'function') {
target: this.shadowRoot || this, target: this.shadowRoot || this,
props: { props: {
...this.$$d, ...this.$$d,
$$slots $$slots,
$$host: this
} }
}); });

@ -3,6 +3,7 @@ import { clone_node, empty } from './operations.js';
import { create_fragment_from_html } from './reconciler.js'; import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js'; import { current_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { effect } from '../reactivity/effects.js';
/** /**
* @param {string} content * @param {string} content
@ -120,14 +121,29 @@ export function svg_template_with_script(content, flags) {
* @param {Element | DocumentFragment} node * @param {Element | DocumentFragment} node
*/ */
function run_scripts(node) { function run_scripts(node) {
for (const script of node.querySelectorAll('script')) { // scripts were SSR'd, in which case they will run
if (hydrating) return;
const scripts =
/** @type {HTMLElement} */ (node).tagName === 'SCRIPT'
? [/** @type {HTMLScriptElement} */ (node)]
: node.querySelectorAll('script');
for (const script of scripts) {
var clone = document.createElement('script'); var clone = document.createElement('script');
for (var attribute of script.attributes) { for (var attribute of script.attributes) {
clone.setAttribute(attribute.name, attribute.value); clone.setAttribute(attribute.name, attribute.value);
} }
clone.textContent = script.textContent; clone.textContent = script.textContent;
script.replaceWith(clone); // If node === script tag, replaceWith will do nothing because there's no parent yet,
// waiting until that's the case using an effect solves this.
// Don't do it in other circumstances or we could accidentally execute scripts
// in an adjacent @html tag that was instantiated in the meantime.
if (script === node) {
effect(() => script.replaceWith(clone));
} else {
script.replaceWith(clone);
}
} }
} }

@ -1,6 +1,11 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { get, batch_inspect, current_component_context, untrack } from './runtime.js'; import {
import { effect_active } from './reactivity/effects.js'; get,
batch_inspect,
current_component_context,
untrack,
current_effect
} from './runtime.js';
import { import {
array_prototype, array_prototype,
define_property, define_property,
@ -206,7 +211,7 @@ const state_proxy_handler = {
// but only if it's an own property and not a prototype property // but only if it's an own property and not a prototype property
if ( if (
s === undefined && s === undefined &&
(effect_active() || updating_derived) && (current_effect !== null || updating_derived) &&
(!(prop in target) || get_descriptor(target, prop)?.writable) (!(prop in target) || get_descriptor(target, prop)?.writable)
) { ) {
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata.o)); s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata.o));
@ -250,7 +255,10 @@ const state_proxy_handler = {
const has = Reflect.has(target, prop); const has = Reflect.has(target, prop);
let s = metadata.s.get(prop); let s = metadata.s.get(prop);
if (s !== undefined || (effect_active() && (!has || get_descriptor(target, prop)?.writable))) { if (
s !== undefined ||
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) { if (s === undefined) {
s = (metadata.i ? source : mutable_source)( s = (metadata.i ? source : mutable_source)(
has ? proxy(target[prop], metadata.i, metadata.o) : UNINITIALIZED has ? proxy(target[prop], metadata.i, metadata.o) : UNINITIALIZED
@ -273,7 +281,7 @@ const state_proxy_handler = {
// we do so otherwise if we read it later, then the write won't be tracked and // we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied // the heuristics of effects will be different vs if we had read the proxied
// object property before writing to that property. // object property before writing to that property.
if (s === undefined && effect_active()) { if (s === undefined && current_effect !== null) {
// the read creates a signal // the read creates a signal
untrack(() => receiver[prop]); untrack(() => receiver[prop]);
s = metadata.s.get(prop); s = metadata.s.get(prop);

@ -18,6 +18,7 @@ import {
} from './dom/hydration.js'; } from './dom/hydration.js';
import { array_from } from './utils.js'; import { array_from } from './utils.js';
import { handle_event_propagation } from './dom/elements/events.js'; import { handle_event_propagation } from './dom/elements/events.js';
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
/** @type {Set<string>} */ /** @type {Set<string>} */
export const all_registered_events = new Set(); export const all_registered_events = new Set();
@ -175,6 +176,7 @@ export function hydrate(component, options) {
} finally { } finally {
set_hydrating(!!previous_hydrate_nodes); set_hydrating(!!previous_hydrate_nodes);
set_hydrate_nodes(previous_hydrate_nodes); set_hydrate_nodes(previous_hydrate_nodes);
reset_head_anchor();
} }
} }

@ -194,9 +194,21 @@ export function check_dirtiness(reaction) {
// is also dirty. // is also dirty.
var version = dependency.version; var version = dependency.version;
if (is_unowned && version > /** @type {import('#client').Derived} */ (reaction).version) { if (is_unowned) {
/** @type {import('#client').Derived} */ (reaction).version = version; if (version > /** @type {import('#client').Derived} */ (reaction).version) {
return true; /** @type {import('#client').Derived} */ (reaction).version = version;
return true;
} else if (!current_skip_reaction && !dependency?.reactions?.includes(reaction)) {
// If we are working with an unowned signal as part of an effect (due to !current_skip_reaction)
// and the version hasn't changed, we still need to check that this reaction
// if linked to the dependency source otherwise future updates will not be caught.
var reactions = dependency.reactions;
if (reactions === null) {
dependency.reactions = [reaction];
} else {
reactions.push(reaction);
}
}
} }
} }
} }

@ -207,10 +207,7 @@ export function render(component, options) {
on_destroy = prev_on_destroy; on_destroy = prev_on_destroy;
return { return {
head: head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '',
payload.head.out || payload.head.title
? payload.head.title + BLOCK_OPEN + payload.head.out + BLOCK_CLOSE
: '',
html: payload.out html: payload.out
}; };
} }
@ -247,7 +244,9 @@ export function escape(value, is_attr = false) {
*/ */
export function head(payload, fn) { export function head(payload, fn) {
const head_payload = payload.head; const head_payload = payload.head;
payload.head.out += BLOCK_OPEN;
fn(head_payload); fn(head_payload);
payload.head.out += BLOCK_CLOSE;
} }
/** /**

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

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
error: {
code: 'conflicting-property-name',
message: 'Cannot have a property and a component export with the same name'
}
});

@ -0,0 +1,4 @@
<script>
let { x: y } = $props();
export function x() {}
</script>

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-host-location',
message: '$host() can only be used inside custom element component instances'
}
});

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 44,
column: 14,
line: 4
},
message: 'Unused CSS selector "p[type=\'B\' s]"',
start: {
character: 31,
column: 1,
line: 4
}
}
]
});

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 33,
column: 6,
line: 6
},
message: 'Unused CSS selector "x y z"',
start: {
character: 28,
column: 1,
line: 6
}
}
]
});

@ -5,8 +5,8 @@ export default test({
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .c"', message: 'Unused CSS selector ".b ~ .c"',
start: { character: 199, column: 1, line: 13 }, start: { character: 198, column: 1, line: 13 },
end: { character: 206, column: 8, line: 13 } end: { character: 205, column: 8, line: 13 }
} }
] ]
}); });

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 479,
column: 19,
line: 22
},
message: 'Unused CSS selector ":global(.x) + .bar"',
start: {
character: 461,
column: 1,
line: 22
}
}
]
});

@ -0,0 +1,13 @@
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
.x + .foo.svelte-xyz { color: green; }
.x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .foo.svelte-xyz { color: green; }
.x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .bar.svelte-xyz { color: green; }
/* no match */
/* (unused) :global(.x) + .bar { color: green; }*/

@ -0,0 +1,23 @@
<div>
<p class="before">before</p>
{@render children()}
<p class="foo">
<span>foo</span>
</p>
<p class="bar">bar</p>
</div>
<style>
.before + .foo { color: green; }
.before ~ .foo { color: green; }
.before ~ .bar { color: green; }
:global(.x) + .foo { color: green; }
:global(.x) + .foo span { color: green; }
:global(.x) ~ .foo { color: green; }
:global(.x) ~ .foo span { color: green; }
:global(.x) ~ .bar { color: green; }
/* no match */
:global(.x) + .bar { color: green; }
</style>

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 472,
column: 19,
line: 22
},
message: 'Unused CSS selector ":global(.x) + .bar"',
start: {
character: 454,
column: 1,
line: 22
}
}
]
});

@ -0,0 +1,13 @@
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
.x + .foo.svelte-xyz { color: green; }
.x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .foo.svelte-xyz { color: green; }
.x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .bar.svelte-xyz { color: green; }
/* no match */
/* (unused) :global(.x) + .bar { color: green; }*/

@ -0,0 +1,23 @@
<div>
<p class="before">before</p>
<slot></slot>
<p class="foo">
<span>foo</span>
</p>
<p class="bar">bar</p>
</div>
<style>
.before + .foo { color: green; }
.before ~ .foo { color: green; }
.before ~ .bar { color: green; }
:global(.x) + .foo { color: green; }
:global(.x) + .foo span { color: green; }
:global(.x) ~ .foo { color: green; }
:global(.x) ~ .foo span { color: green; }
:global(.x) ~ .bar { color: green; }
/* no match */
:global(.x) + .bar { color: green; }
</style>

@ -5,38 +5,38 @@ export default test({
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".a ~ .b"', message: 'Unused CSS selector ".a ~ .b"',
start: { character: 111, column: 1, line: 10 }, start: { character: 110, column: 1, line: 10 },
end: { character: 118, column: 8, line: 10 } end: { character: 117, column: 8, line: 10 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .c"', message: 'Unused CSS selector ".b ~ .c"',
start: { character: 138, column: 1, line: 11 }, start: { character: 137, column: 1, line: 11 },
end: { character: 145, column: 8, line: 11 } end: { character: 144, column: 8, line: 11 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".c ~ .f"', message: 'Unused CSS selector ".c ~ .f"',
start: { character: 165, column: 1, line: 12 }, start: { character: 164, column: 1, line: 12 },
end: { character: 172, column: 8, line: 12 } end: { character: 171, column: 8, line: 12 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".f ~ .g"', message: 'Unused CSS selector ".f ~ .g"',
start: { character: 192, column: 1, line: 13 }, start: { character: 191, column: 1, line: 13 },
end: { character: 199, column: 8, line: 13 } end: { character: 198, column: 8, line: 13 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .f"', message: 'Unused CSS selector ".b ~ .f"',
start: { character: 219, column: 1, line: 14 }, start: { character: 218, column: 1, line: 14 },
end: { character: 226, column: 8, line: 14 } end: { character: 225, column: 8, line: 14 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .g"', message: 'Unused CSS selector ".b ~ .g"',
start: { character: 246, column: 1, line: 15 }, start: { character: 245, column: 1, line: 15 },
end: { character: 253, column: 8, line: 15 } end: { character: 252, column: 8, line: 15 }
} }
] ]
}); });

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 496,
column: 10,
line: 26
},
message: 'Unused CSS selector ".x + .bar"',
start: {
character: 487,
column: 1,
line: 26
}
}
]
});

@ -0,0 +1,13 @@
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
.x.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.x.svelte-xyz + .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .x + .bar { color: green; }*/

@ -0,0 +1,27 @@
<script>
let tag = 'div'
</script>
<div>
<p class="before">before</p>
<svelte:element class="x" this={tag}></svelte:element>
<p class="foo">
<span>foo</span>
</p>
<p class="bar">bar</p>
</div>
<style>
.before + .foo { color: green; }
.before ~ .foo { color: green; }
.before ~ .bar { color: green; }
.x + .foo { color: green; }
.x + .foo span { color: green; }
.x ~ .foo { color: green; }
.x ~ .foo span { color: green; }
.x ~ .bar { color: green; }
/* no match */
.x + .bar { color: green; }
</style>

@ -6,12 +6,12 @@ export default test({
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ":host > span"', message: 'Unused CSS selector ":host > span"',
start: { start: {
character: 147, character: 145,
column: 1, column: 1,
line: 18 line: 18
}, },
end: { end: {
character: 159, character: 157,
column: 13, column: 13,
line: 18 line: 18
} }

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 38,
column: 11,
line: 6
},
message: 'Unused CSS selector "z"',
start: {
character: 37,
column: 10,
line: 6
}
}
]
});

@ -0,0 +1,104 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 239,
column: 13,
line: 20
},
message: 'Unused CSS selector ".unused"',
start: {
character: 232,
column: 6,
line: 20
}
},
{
code: 'css-unused-selector',
end: {
character: 302,
column: 10,
line: 27
},
message: 'Unused CSS selector ".unused"',
start: {
character: 295,
column: 3,
line: 27
}
},
{
code: 'css-unused-selector',
end: {
character: 328,
column: 6,
line: 30
},
message: 'Unused CSS selector ".c"',
start: {
character: 326,
column: 4,
line: 30
}
},
{
code: 'css-unused-selector',
end: {
character: 381,
column: 10,
line: 37
},
message: 'Unused CSS selector ".unused"',
start: {
character: 374,
column: 3,
line: 37
}
},
{
code: 'css-unused-selector',
end: {
character: 471,
column: 7,
line: 47
},
message: 'Unused CSS selector "& &"',
start: {
character: 468,
column: 4,
line: 47
}
},
{
code: 'css-unused-selector',
end: {
character: 634,
column: 5,
line: 66
},
message: 'Unused CSS selector "&.b"',
start: {
character: 631,
column: 2,
line: 66
}
},
{
code: 'css-unused-selector',
end: {
character: 666,
column: 9,
line: 70
},
message: 'Unused CSS selector ".unused"',
start: {
character: 659,
column: 2,
line: 70
}
}
]
});

@ -5,62 +5,62 @@ export default test({
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .c"', message: 'Unused CSS selector ".a + .c"',
start: { character: 479, column: 1, line: 23 }, start: { character: 478, column: 1, line: 23 },
end: { character: 486, column: 8, line: 23 } end: { character: 485, column: 8, line: 23 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .g"', message: 'Unused CSS selector ".a + .g"',
start: { character: 506, column: 1, line: 24 }, start: { character: 505, column: 1, line: 24 },
end: { character: 513, column: 8, line: 24 } end: { character: 512, column: 8, line: 24 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".b + .e"', message: 'Unused CSS selector ".b + .e"',
start: { character: 533, column: 1, line: 25 }, start: { character: 532, column: 1, line: 25 },
end: { character: 540, column: 8, line: 25 } end: { character: 539, column: 8, line: 25 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".c + .g"', message: 'Unused CSS selector ".c + .g"',
start: { character: 560, column: 1, line: 26 }, start: { character: 559, column: 1, line: 26 },
end: { character: 567, column: 8, line: 26 } end: { character: 566, column: 8, line: 26 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".c + .k"', message: 'Unused CSS selector ".c + .k"',
start: { character: 587, column: 1, line: 27 }, start: { character: 586, column: 1, line: 27 },
end: { character: 594, column: 8, line: 27 } end: { character: 593, column: 8, line: 27 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".d + .d"', message: 'Unused CSS selector ".d + .d"',
start: { character: 614, column: 1, line: 28 }, start: { character: 613, column: 1, line: 28 },
end: { character: 621, column: 8, line: 28 } end: { character: 620, column: 8, line: 28 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".e + .f"', message: 'Unused CSS selector ".e + .f"',
start: { character: 641, column: 1, line: 29 }, start: { character: 640, column: 1, line: 29 },
end: { character: 648, column: 8, line: 29 } end: { character: 647, column: 8, line: 29 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".f + .f"', message: 'Unused CSS selector ".f + .f"',
start: { character: 668, column: 1, line: 30 }, start: { character: 667, column: 1, line: 30 },
end: { character: 675, column: 8, line: 30 } end: { character: 674, column: 8, line: 30 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".g + .j"', message: 'Unused CSS selector ".g + .j"',
start: { character: 695, column: 1, line: 31 }, start: { character: 694, column: 1, line: 31 },
end: { character: 702, column: 8, line: 31 } end: { character: 701, column: 8, line: 31 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".g + .h + .i + .j"', message: 'Unused CSS selector ".g + .h + .i + .j"',
start: { character: 722, column: 1, line: 32 }, start: { character: 721, column: 1, line: 32 },
end: { character: 739, column: 18, line: 32 } end: { character: 738, column: 18, line: 32 }
} }
] ]
}); });

@ -5,14 +5,14 @@ export default test({
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .d"', message: 'Unused CSS selector ".a + .d"',
start: { character: 172, column: 1, line: 12 }, start: { character: 171, column: 1, line: 12 },
end: { character: 179, column: 8, line: 12 } end: { character: 178, column: 8, line: 12 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".b + .c"', message: 'Unused CSS selector ".b + .c"',
start: { character: 199, column: 1, line: 13 }, start: { character: 198, column: 1, line: 13 },
end: { character: 206, column: 8, line: 13 } end: { character: 205, column: 8, line: 13 }
} }
] ]
}); });

@ -5,20 +5,20 @@ export default test({
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .b"', message: 'Unused CSS selector ".a + .b"',
start: { character: 84, column: 1, line: 9 }, start: { character: 83, column: 1, line: 9 },
end: { character: 91, column: 8, line: 9 } end: { character: 90, column: 8, line: 9 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".b + .c"', message: 'Unused CSS selector ".b + .c"',
start: { character: 111, column: 1, line: 10 }, start: { character: 110, column: 1, line: 10 },
end: { character: 118, column: 8, line: 10 } end: { character: 117, column: 8, line: 10 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".c + .f"', message: 'Unused CSS selector ".c + .f"',
start: { character: 138, column: 1, line: 11 }, start: { character: 137, column: 1, line: 11 },
end: { character: 145, column: 8, line: 11 } end: { character: 144, column: 8, line: 11 }
} }
] ]
}); });

@ -5,20 +5,20 @@ export default test({
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector "article > *"', message: 'Unused CSS selector "article > *"',
start: { character: 10, column: 1, line: 2 }, start: { character: 9, column: 1, line: 2 },
end: { character: 21, column: 12, line: 2 } end: { character: 20, column: 12, line: 2 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector "article *"', message: 'Unused CSS selector "article *"',
start: { character: 49, column: 1, line: 6 }, start: { character: 47, column: 1, line: 6 },
end: { character: 58, column: 10, line: 6 } end: { character: 56, column: 10, line: 6 }
}, },
{ {
code: 'css-unused-selector', code: 'css-unused-selector',
message: 'Unused CSS selector ".article > *"', message: 'Unused CSS selector ".article > *"',
start: { character: 86, column: 1, line: 10 }, start: { character: 83, column: 1, line: 10 },
end: { character: 98, column: 13, line: 10 } end: { character: 95, column: 13, line: 10 }
} }
] ]
}); });

@ -1,6 +1,6 @@
/* (unused) article > * { /* (unused) article > * {
font-size: 36px; font-size: 36px;
}*/ }*/
/* (unused) article * { /* (unused) article * {
font-size: 36px; font-size: 36px;

@ -1,7 +1,7 @@
<style> <style>
article > * { article > * {
font-size: 36px; font-size: 36px;
} }
article * { article * {
font-size: 36px; font-size: 36px;

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 32,
column: 3,
line: 5
},
message: 'Unused CSS selector "h2"',
start: {
character: 30,
column: 1,
line: 5
}
}
]
});

@ -8,12 +8,17 @@ import { mount, unmount } from 'svelte';
import { suite, type BaseTest } from '../suite.js'; import { suite, type BaseTest } from '../suite.js';
import type { CompileOptions, Warning } from '#compiler'; import type { CompileOptions, Warning } from '#compiler';
// function normalize_warning(warning) { function normalize_warning(warning: Warning) {
// warning.frame = warning.frame.replace(/^\n/, '').replace(/^\t+/gm, '').replace(/\s+$/gm, ''); delete warning.filename;
// delete warning.filename; return warning;
// delete warning.toString; }
// return warning;
// } function load_warnings(path: string) {
if (!fs.existsSync(path)) {
return [];
}
return JSON.parse(fs.readFileSync(path, 'utf-8')).map(normalize_warning);
}
interface CssTest extends BaseTest { interface CssTest extends BaseTest {
compileOptions?: Partial<CompileOptions>; compileOptions?: Partial<CompileOptions>;
@ -22,9 +27,6 @@ interface CssTest extends BaseTest {
} }
const { test, run } = suite<CssTest>(async (config, cwd) => { const { test, run } = suite<CssTest>(async (config, cwd) => {
// TODO
// const expected_warnings = (config.warnings || []).map(normalize_warning);
await compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); await compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions });
await compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); await compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions });
@ -33,11 +35,11 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
assert.equal(dom_css, ssr_css); assert.equal(dom_css, ssr_css);
// TODO reenable const dom_warnings = load_warnings(`${cwd}/_output/client/input.svelte.warnings.json`);
// const dom_warnings = dom.warnings.map(normalize_warning); const ssr_warnings = load_warnings(`${cwd}/_output/server/input.svelte.warnings.json`);
// const ssr_warnings = ssr.warnings.map(normalize_warning); const expected_warnings = (config.warnings || []).map(normalize_warning);
// assert.deepEqual(dom_warnings, ssr_warnings); assert.deepEqual(dom_warnings, ssr_warnings);
// assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings); assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings);
const expected = { const expected = {
html: try_read_file(`${cwd}/expected.html`), html: try_read_file(`${cwd}/expected.html`),

@ -71,7 +71,7 @@ export async function compile_directory(
for (const file of glob('**', { cwd, filesOnly: true })) { for (const file of glob('**', { cwd, filesOnly: true })) {
if (file.startsWith('_')) continue; if (file.startsWith('_')) continue;
let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8'); let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r\n/g, '\n');
let opts = { let opts = {
filename: path.join(cwd, file), filename: path.join(cwd, file),
...compileOptions, ...compileOptions,
@ -138,6 +138,10 @@ export async function compile_directory(
write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t')); write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t'));
} }
} }
if (compiled.warnings.length > 0) {
write(`${output_dir}/${file}.warnings.json`, JSON.stringify(compiled.warnings, null, '\t'));
}
} }
} }
} }

@ -78,6 +78,7 @@
"content": { "content": {
"start": 23, "start": 23,
"end": 48, "end": 48,
"comment": null,
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n" "styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
} }
} }

@ -78,6 +78,7 @@
"content": { "content": {
"start": 23, "start": 23,
"end": 48, "end": 48,
"comment": null,
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n" "styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
} }
} }

@ -1074,6 +1074,7 @@
"content": { "content": {
"start": 7, "start": 7,
"end": 798, "end": 798,
"comment": null,
"styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n\t\th1:nth-of-type(10n+1){\n background: red;\n }\n\t\th1:nth-of-type(-2n+3){\n background: red;\n }\n\t\th1:nth-of-type(+12){\n background: red;\n }\n\t\th1:nth-of-type(+3n){\n background: red;\n }\n" "styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n\t\th1:nth-of-type(10n+1){\n background: red;\n }\n\t\th1:nth-of-type(-2n+3){\n background: red;\n }\n\t\th1:nth-of-type(+12){\n background: red;\n }\n\t\th1:nth-of-type(+3n){\n background: red;\n }\n"
} }
}, },

@ -393,6 +393,7 @@
"content": { "content": {
"start": 7, "start": 7,
"end": 378, "end": 378,
"comment": null,
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n" "styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n"
} }
}, },

@ -71,6 +71,7 @@
"content": { "content": {
"start": 43, "start": 43,
"end": 197, "end": 197,
"comment": null,
"styles": "\n\t@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\");\n\th1 {\n\t\tfont-weight: bold;\n\t\tbackground: url(\"whatever\");\n\t}\n" "styles": "\n\t@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\");\n\th1 {\n\t\tfont-weight: bold;\n\t\tbackground: url(\"whatever\");\n\t}\n"
} }
}, },

@ -0,0 +1,24 @@
import { test } from '../../assert';
const tick = () => Promise.resolve();
export default test({
async test({ assert, target }) {
target.innerHTML = '<custom-element></custom-element>';
/** @type {any} */
const el = target.querySelector('custom-element');
/** @type {string[]} */
const events = [];
const handle_evt = (e) => events.push(e.type, e.detail);
el.addEventListener('greeting', handle_evt);
await tick();
el.shadowRoot.querySelector('button').click();
assert.deepEqual(events, ['greeting', 'hello']);
el.removeEventListener('greeting', handle_evt);
el.shadowRoot.querySelector('button').click();
assert.deepEqual(events, ['greeting', 'hello']);
}
});

@ -0,0 +1,9 @@
<svelte:options customElement="custom-element" />
<script>
function greet(greeting) {
$host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
}
</script>
<button onclick={() => greet('hello')}>say hello</button>

@ -6,5 +6,5 @@ import config from '__CONFIG__';
import { render } from 'svelte/server'; import { render } from 'svelte/server';
export default function () { export default function () {
return render(SvelteComponent, { props: config.props || {} }).html; return render(SvelteComponent, { props: config.props || {} });
} }

@ -1,7 +1,7 @@
import { test } from '../../test'; import { test } from '../../assert';
export default test({ export default test({
test({ assert, component, window }) { test({ assert, window }) {
document.dispatchEvent(new Event('DOMContentLoaded')); document.dispatchEvent(new Event('DOMContentLoaded'));
assert.equal(window.document.querySelector('button')?.textContent, 'Hello world'); assert.equal(window.document.querySelector('button')?.textContent, 'Hello world');
} }

@ -0,0 +1,17 @@
import { test } from '../../assert';
export default test({
// Test that @html does not execute scripts when instantiated in the client.
// Needs to be in this test suite because JSDOM does not quite get this right.
mode: ['client'],
test({ window, assert }) {
// In here to give effects etc time to execute
assert.htmlEqual(
window.document.body.innerHTML,
`<main>
<div><script></script></div><script>document.body.innerHTML = 'this should not be executed'</script>
<script></script><script>document.body.innerHTML = 'this neither'</script>
</main>`
);
}
});

@ -0,0 +1,7 @@
<div>
<script></script>
</div>
{@html `<script>document.body.innerHTML = 'this should not be executed'</script>`}
{#if true}
<script></script>{@html `<script>document.body.innerHTML = 'this neither'</script>`}
{/if}

@ -3,6 +3,12 @@ import { test } from '../../assert';
export default test({ export default test({
// Test that @html does not execute scripts when instantiated in the client. // Test that @html does not execute scripts when instantiated in the client.
// Needs to be in this test suite because JSDOM does not quite get this right. // Needs to be in this test suite because JSDOM does not quite get this right.
html: `<div></div><script>document.body.innerHTML = 'this should not be executed'</script>`, mode: ['client'],
mode: ['client'] test({ window, assert }) {
// In here to give effects etc time to execute
assert.htmlEqual(
window.document.body.innerHTML,
`<main><div></div><script>document.body.innerHTML = 'this should not be executed'</script></main>`
);
}
}); });

@ -0,0 +1,11 @@
import { test } from '../../assert';
export default test({
// Test that template with sole script tag does execute when instantiated in the client.
// Needs to be in this test suite because JSDOM does not quite get this right.
mode: ['client'],
test({ window, assert }) {
// In here to give effects etc time to execute
assert.htmlEqual(window.document.body.innerHTML, 'this should be executed');
}
});

@ -0,0 +1,4 @@
<div></div>
{#if true}
<script>document.body.innerHTML = 'this should be executed'</script>
{/if}

@ -194,10 +194,10 @@ async function run_test(
}); });
if (build_result_ssr) { if (build_result_ssr) {
const html = await page.evaluate( const result: any = await page.evaluate(
build_result_ssr.outputFiles[0].text + '; test_ssr.default()' build_result_ssr.outputFiles[0].text + '; test_ssr.default()'
); );
await page.setContent('<main>' + html + '</main>'); await page.setContent('<head>' + result.head + '</head><main>' + result.html + '</main>');
} else { } else {
await page.setContent('<main></main>'); await page.setContent('<main></main>');
} }

@ -1,7 +1,7 @@
<script> <script>
let foo = () => 1; let foo = () => 1;
function bar() { var bar = function() {
return 2; return 2;
} }

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
// The test has a bunch of queueMicrotasks
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `<div>Zeeba Neighba</div>`);
}
});

@ -0,0 +1,34 @@
<script context="module">
export class Thing {
data = $state();
subscribe() {
queueMicrotask(() => {
this.data = {
name: `Zeeba Neighba`,
};
});
}
name = $derived(this.data?.name);
}
export class Things {
thing = $state();
subscribe() {
queueMicrotask(() => {
this.thing = new Thing();
this.thing.subscribe();
this.thing.name;
});
}
}
</script>
<script>
let model = new Things();
$effect(() => model.subscribe());
</script>
<div>{model.thing?.name}</div>

@ -0,0 +1,12 @@
<script>
let { ...rest } = $props();
let count = $state(0);
export function increment() {
count++;
}
</script>
<!-- test that the binding isn't inside rest -->
{Object.keys(rest).length}
{count}

@ -0,0 +1,17 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true // to ensure we don't throw a false-positive "cannot bind to this" error
},
html: `0 0 <button>increment</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `0 1 <button>increment</button>`);
}
});

@ -0,0 +1,7 @@
<script>
import Counter from './Counter.svelte';
let increment;
</script>
<Counter bind:increment={increment} />
<button onclick={increment}>increment</button>

@ -0,0 +1,9 @@
<script>
let title = $state('Hello world');
let desc = $state('Some description');
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={desc}>
<meta name="author" content="@svelteawesome">
</svelte:head>

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
html: `<div>Hello</div>`,
async test({ assert, target }) {
assert.htmlEqual(
target.ownerDocument.head.innerHTML,
`<script async="" src="https://www.googletagmanager.com/gtag/js?id=12345"></script><meta content="Some description" name="description"><meta content="@svelteawesome" name="author"><title>Hello world</title>`
);
}
});

@ -0,0 +1,9 @@
<script>
import MetaTag from './MetaTag.svelte'
</script>
<svelte:head>
<script async src="https://www.googletagmanager.com/gtag/js?id=12345"></script>
</svelte:head>
<MetaTag />
<div>Hello</div>

@ -0,0 +1,35 @@
import { tick } from 'svelte';
import { test } from '../../test';
/** @type {typeof console.warn} */
let warn;
/** @type {any[]} */
let warnings = [];
export default test({
compileOptions: {
dev: true
},
before_test: () => {
warn = console.warn;
console.warn = (...args) => {
warnings.push(...args);
};
},
after_test: () => {
console.warn = warn;
warnings = [];
},
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
await tick();
assert.deepEqual(warnings.length, 0);
}
});

@ -0,0 +1,25 @@
<script lang="ts">
import { setContext } from 'svelte';
import Sub from './sub.svelte';
class Person1 {
value = $state({ person: 'John', age: 33 })
}
const class_nested_state = $state(new Person1());
class Person2 {
person = $state('John');
age = $state(33);
}
const state_nested_class = $state({ value: new Person2() });
const nested_state = $state({ person: 'John', age: 33 });
setContext('foo', {
nested_state,
get class_nested_state() { return class_nested_state },
get state_nested_class() { return state_nested_class }
})
</script>
<Sub {class_nested_state} {state_nested_class} {nested_state} />

@ -0,0 +1,11 @@
<script>
import { getContext } from "svelte";
const foo = getContext('foo')
</script>
<button onclick={() => {
foo.class_nested_state.value.age++;
foo.state_nested_class.value.age++;
foo.nested_state.age++;
}}>mutate</button>

@ -0,0 +1,5 @@
<script>
const { settings } = $props();
</script>
Child: {settings.showInRgb}

@ -0,0 +1,22 @@
import { flushSync } from '../../../../src/index-client';
import { test } from '../../test';
export default test({
html: `<button>click true</button> Child: true`,
async test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>click false</button> Child: false`);
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>click true</button> Child: true`);
}
});

@ -0,0 +1,19 @@
<script context="module">
export const context = $state({
settings: {
showInRgb: true
}
})
</script>
<script>
import Child from './Child.svelte';
const { settings } = context
</script>
<button onclick={() => settings.showInRgb = !settings.showInRgb}>
click {settings.showInRgb}
</button>
<Child settings={settings} />

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

Loading…
Cancel
Save