Merge branch 'main' into hoist-unmodified-var

hoist-unmodified-var
Ben McCann 8 months ago
commit 3e1f4c7a0c

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: deeply unstate objects passed to inspect

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve script `lang` attribute detection

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve pseudo class parsing

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add types for popover attributes and events

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: skip generating $.proxy() calls for unary and binary expressions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow pseudo classes after `:global(..)`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: parse `:nth-of-type(xn+y)` correctly

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure if block is executed in correct order

@ -50,4 +50,11 @@ jobs:
with: with:
node-version: 18 node-version: 18
cache: pnpm cache: pnpm
- run: 'pnpm i && pnpm check && pnpm lint' - name: install
run: pnpm install --frozen-lockfile
- name: type check
run: pnpm check
- name: lint
run: pnpm lint
- name: build and check generated types
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }

@ -28,7 +28,11 @@ jobs:
node-version: 18.x node-version: 18.x
cache: pnpm cache: pnpm
- run: pnpm install --frozen-lockfile - name: Install
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }
- name: Create Release Pull Request or Publish to npm - name: Create Release Pull Request or Publish to npm
id: changesets id: changesets

@ -133,6 +133,10 @@ To typecheck the codebase, run `pnpm check` inside `packages/svelte`. To typeche
- `snake_case` for internal variable names and methods. - `snake_case` for internal variable names and methods.
- `camelCase` for public variable names and methods. - `camelCase` for public variable names and methods.
### Generating types
Types are auto-generated from the source, but the result is checked in to ensure no accidental changes slip through. Run `pnpm generate:types` to regenerate the types.
### Sending your pull request ### Sending your pull request
Please make sure the following is done when submitting a pull request: Please make sure the following is done when submitting a pull request:
@ -141,7 +145,7 @@ Please make sure the following is done when submitting a pull request:
1. Make sure your code lints (`pnpm lint`). 1. Make sure your code lints (`pnpm lint`).
1. Make sure your tests pass (`pnpm test`). 1. Make sure your tests pass (`pnpm test`).
All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. If this change should contribute to a version bump, run `npx changeset` at the root of the repository after a code change and select the appropriate packages.
#### Breaking changes #### Breaking changes

@ -1,4 +1,4 @@
Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors) Copyright (c) 2016-24 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

@ -1,4 +1,5 @@
/types /types/*.map
/types/compiler
/compiler.cjs /compiler.cjs
/action.d.ts /action.d.ts

@ -59,6 +59,7 @@ export type WheelEventHandler<T extends EventTarget> = EventHandler<WheelEvent,
export type AnimationEventHandler<T extends EventTarget> = EventHandler<AnimationEvent, T>; export type AnimationEventHandler<T extends EventTarget> = EventHandler<AnimationEvent, T>;
export type TransitionEventHandler<T extends EventTarget> = EventHandler<TransitionEvent, T>; export type TransitionEventHandler<T extends EventTarget> = EventHandler<TransitionEvent, T>;
export type MessageEventHandler<T extends EventTarget> = EventHandler<MessageEvent, T>; export type MessageEventHandler<T extends EventTarget> = EventHandler<MessageEvent, T>;
export type ToggleEventHandler<T extends EventTarget> = EventHandler<ToggleEvent, T>;
// //
// DOM Attributes // DOM Attributes
@ -136,10 +137,13 @@ export interface DOMAttributes<T extends EventTarget> {
onerror?: EventHandler | undefined | null; // also a Media Event onerror?: EventHandler | undefined | null; // also a Media Event
onerrorcapture?: EventHandler | undefined | null; // also a Media Event onerrorcapture?: EventHandler | undefined | null; // also a Media Event
// Detail Events // Popover Events
'on:toggle'?: EventHandler<Event, T> | undefined | null; 'on:beforetoggle'?: ToggleEventHandler<T> | undefined | null;
ontoggle?: EventHandler<Event, T> | undefined | null; onbeforetoggle?: ToggleEventHandler<T> | undefined | null;
ontogglecapture?: EventHandler<Event, T> | undefined | null; onbeforetogglecapture?: ToggleEventHandler<T> | undefined | null;
'on:toggle'?: ToggleEventHandler<T> | undefined | null;
ontoggle?: ToggleEventHandler<T> | undefined | null;
ontogglecapture?: ToggleEventHandler<T> | undefined | null;
// Keyboard Events // Keyboard Events
'on:keydown'?: KeyboardEventHandler<T> | undefined | null; 'on:keydown'?: KeyboardEventHandler<T> | undefined | null;
@ -727,6 +731,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
title?: string | undefined | null; title?: string | undefined | null;
translate?: 'yes' | 'no' | '' | undefined | null; translate?: 'yes' | 'no' | '' | undefined | null;
inert?: boolean | undefined | null; inert?: boolean | undefined | null;
popover?: 'auto' | 'manual' | '' | undefined | null;
// Unknown // Unknown
radiogroup?: string | undefined | null; // <command>, <menuitem> radiogroup?: string | undefined | null; // <command>, <menuitem>
@ -873,6 +878,8 @@ export interface HTMLButtonAttributes extends HTMLAttributes<HTMLButtonElement>
name?: string | undefined | null; name?: string | undefined | null;
type?: 'submit' | 'reset' | 'button' | undefined | null; type?: 'submit' | 'reset' | 'button' | undefined | null;
value?: string | string[] | number | undefined | null; value?: string | string[] | number | undefined | null;
popovertarget?: string | undefined | null;
popovertargetaction?: 'toggle' | 'show' | 'hide' | undefined | null;
} }
export interface HTMLCanvasAttributes extends HTMLAttributes<HTMLCanvasElement> { export interface HTMLCanvasAttributes extends HTMLAttributes<HTMLCanvasElement> {
@ -897,6 +904,10 @@ export interface HTMLDetailsAttributes extends HTMLAttributes<HTMLDetailsElement
open?: boolean | undefined | null; open?: boolean | undefined | null;
'bind:open'?: boolean | undefined | null; 'bind:open'?: boolean | undefined | null;
'on:toggle'?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontoggle?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontogglecapture?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
} }
export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> { export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> {

@ -90,11 +90,12 @@
"templating" "templating"
], ],
"scripts": { "scripts": {
"build": "rollup -c && node scripts/build.js && node scripts/check-treeshakeability.js", "build": "rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "rollup -cw", "dev": "rollup -cw",
"check": "tsc && cd ./tests/types && tsc", "check": "tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch", "check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js", "generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js",
"prepublishOnly": "pnpm build" "prepublishOnly": "pnpm build"
}, },
"devDependencies": { "devDependencies": {
@ -106,7 +107,7 @@
"@rollup/plugin-virtual": "^3.0.2", "@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.3", "@types/aria-query": "^5.0.3",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",
"dts-buddy": "^0.4.0", "dts-buddy": "^0.4.3",
"esbuild": "^0.19.2", "esbuild": "^0.19.2",
"rollup": "^4.1.5", "rollup": "^4.1.5",
"source-map": "^0.7.4", "source-map": "^0.7.4",

@ -11,7 +11,7 @@ import read_options from './read/options.js';
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;
const regex_lang_attribute = const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/; /<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
export class Parser { export class Parser {
/** /**
@ -49,7 +49,14 @@ export class Parser {
this.template = template.trimRight(); this.template = template.trimRight();
this.ts = regex_lang_attribute.exec(template)?.[2] === 'ts'; let match_lang;
do match_lang = regex_lang_attribute.exec(template);
while (match_lang && match_lang[0][1] !== 's'); // ensure it starts with '<s' to match script tags
regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor
this.ts = match_lang?.[2] === 'ts';
this.root = { this.root = {
css: null, css: null,

@ -6,7 +6,8 @@ const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today,
const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/; const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/; const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF = /^\s*(even|odd|(-?[0-9]?n?(\s*\+\s*[0-9]+)?))(\s*(?=[,)])|\s+of\s+)/; const REGEX_NTH_OF =
/^\s*(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))(\s*(?=[,)])|\s+of\s+)/;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_BRACE_OR_SEMICOLON = /[{;]/; const REGEX_BRACE_OR_SEMICOLON = /[{;]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
@ -226,6 +227,12 @@ function read_selector(parser, inside_pseudo_class = false) {
start, start,
end: parser.index end: parser.index
}); });
// We read the inner selectors of a pseudo element to ensure it parses correctly,
// but we don't do anything with the result.
if (parser.eat('(')) {
read_selector_list(parser, true);
parser.eat(')', true);
}
} else if (parser.eat(':')) { } else if (parser.eat(':')) {
const name = read_identifier(parser); const name = read_identifier(parser);
@ -277,6 +284,14 @@ function read_selector(parser, inside_pseudo_class = false) {
value, value,
flags flags
}); });
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
children.push({
type: 'Nth',
value: /** @type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else if (parser.match_regex(REGEX_COMBINATOR_WHITESPACE)) { } else if (parser.match_regex(REGEX_COMBINATOR_WHITESPACE)) {
parser.allow_whitespace(); parser.allow_whitespace();
const start = parser.index; const start = parser.index;
@ -294,13 +309,6 @@ function read_selector(parser, inside_pseudo_class = false) {
start, start,
end: parser.index end: parser.index
}); });
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
children.push({
type: 'Nth',
value: /** @type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else { } else {
let name = read_identifier(parser); let name = read_identifier(parser);
if (parser.match('|')) { if (parser.match('|')) {

@ -184,7 +184,9 @@ export default class Selector {
selector.name === 'global' && selector.name === 'global' &&
block.selectors.length !== 1 && block.selectors.length !== 1 &&
(i === block.selectors.length - 1 || (i === block.selectors.length - 1 ||
block.selectors.slice(i + 1).some((s) => s.type !== 'PseudoElementSelector')) block.selectors
.slice(i + 1)
.some((s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'))
) { ) {
error(selector, 'invalid-css-global-selector-list'); error(selector, 'invalid-css-global-selector-list');
} }

@ -526,6 +526,8 @@ export function should_proxy_or_freeze(node) {
node.type === 'Literal' || node.type === 'Literal' ||
node.type === 'ArrowFunctionExpression' || node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' || node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined') (node.type === 'Identifier' && node.name === 'undefined')
) { ) {
return false; return false;

@ -64,7 +64,7 @@ let current_dependencies = null;
let current_dependencies_index = 0; let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */ /** @type {null | import('./types.js').Signal[]} */
let current_untracked_writes = null; let current_untracked_writes = null;
/** @type {null | import('./types.js').Signal} */ /** @type {null | import('./types.js').SignalDebug} */
let last_inspected_signal = null; let last_inspected_signal = null;
/** If `true`, `get`ting the signal should not register it as a dependency */ /** If `true`, `get`ting the signal should not register it as a dependency */
export let current_untracking = false; export let current_untracking = false;
@ -81,7 +81,7 @@ let captured_signals = new Set();
/** @type {Function | null} */ /** @type {Function | null} */
let inspect_fn = null; let inspect_fn = null;
/** @type {Array<import('./types.js').SourceSignal & import('./types.js').SourceSignalDebug>} */ /** @type {Array<import('./types.js').SignalDebug>} */
let inspect_captured_signals = []; let inspect_captured_signals = [];
// Handle rendering tree blocks and anchors // Handle rendering tree blocks and anchors
@ -127,7 +127,6 @@ export function batch_inspect(target, prop, receiver) {
} finally { } finally {
is_batching_effect = previously_batching_effect; is_batching_effect = previously_batching_effect;
if (last_inspected_signal !== null) { if (last_inspected_signal !== null) {
// @ts-expect-error
for (const fn of last_inspected_signal.inspect) fn(); for (const fn of last_inspected_signal.inspect) fn();
last_inspected_signal = null; last_inspected_signal = null;
} }
@ -349,7 +348,21 @@ function execute_signal_fn(signal) {
if (current_dependencies !== null) { if (current_dependencies !== null) {
let i; let i;
remove_consumer(signal, current_dependencies_index, false); if (dependencies !== null) {
const dep_length = dependencies.length;
// If we have more than 16 elements in the array then use a Set for faster performance
// TODO: evaluate if we should always just use a Set or not here?
const current_dependencies_set = dep_length > 16 ? new Set(current_dependencies) : null;
for (i = current_dependencies_index; i < dep_length; i++) {
const dependency = dependencies[i];
if (
(current_dependencies_set !== null && !current_dependencies_set.has(dependency)) ||
!current_dependencies.includes(dependency)
) {
remove_consumer(signal, dependency, false);
}
}
}
if (dependencies !== null && current_dependencies_index > 0) { if (dependencies !== null && current_dependencies_index > 0) {
dependencies.length = current_dependencies_index + current_dependencies.length; dependencies.length = current_dependencies_index + current_dependencies.length;
@ -365,16 +378,17 @@ function execute_signal_fn(signal) {
if (!current_skip_consumer) { if (!current_skip_consumer) {
for (i = current_dependencies_index; i < dependencies.length; i++) { for (i = current_dependencies_index; i < dependencies.length; i++) {
const dependency = dependencies[i]; const dependency = dependencies[i];
const consumers = dependency.c;
if (dependency.c === null) { if (consumers === null) {
dependency.c = [signal]; dependency.c = [signal];
} else { } else if (consumers[consumers.length - 1] !== signal) {
dependency.c.push(signal); consumers.push(signal);
} }
} }
} }
} else if (dependencies !== null && current_dependencies_index < dependencies.length) { } else if (dependencies !== null && current_dependencies_index < dependencies.length) {
remove_consumer(signal, current_dependencies_index, false); remove_consumers(signal, current_dependencies_index, false);
dependencies.length = current_dependencies_index; dependencies.length = current_dependencies_index;
} }
return res; return res;
@ -390,6 +404,40 @@ function execute_signal_fn(signal) {
} }
} }
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @param {import('./types.js').Signal<V>} dependency
* @param {boolean} remove_unowned
* @returns {void}
*/
function remove_consumer(signal, dependency, remove_unowned) {
const consumers = dependency.c;
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
}
}
}
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumers(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
0,
true
);
}
}
/** /**
* @template V * @template V
* @param {import('./types.js').ComputationSignal<V>} signal * @param {import('./types.js').ComputationSignal<V>} signal
@ -397,36 +445,13 @@ function execute_signal_fn(signal) {
* @param {boolean} remove_unowned * @param {boolean} remove_unowned
* @returns {void} * @returns {void}
*/ */
function remove_consumer(signal, start_index, remove_unowned) { function remove_consumers(signal, start_index, remove_unowned) {
const dependencies = signal.d; const dependencies = signal.d;
if (dependencies !== null) { if (dependencies !== null) {
let i; let i;
for (i = start_index; i < dependencies.length; i++) { for (i = start_index; i < dependencies.length; i++) {
const dependency = dependencies[i]; const dependency = dependencies[i];
const consumers = dependency.c; remove_consumer(signal, dependency, remove_unowned);
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
}
}
}
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumer(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
0,
true
);
}
} }
} }
} }
@ -446,7 +471,7 @@ function destroy_references(signal) {
if ((reference.f & IS_EFFECT) !== 0) { if ((reference.f & IS_EFFECT) !== 0) {
destroy_signal(reference); destroy_signal(reference);
} else { } else {
remove_consumer(reference, 0, true); remove_consumers(reference, 0, true);
reference.d = null; reference.d = null;
} }
} }
@ -711,8 +736,7 @@ function update_derived(signal, force_schedule) {
// @ts-expect-error // @ts-expect-error
if (DEV && signal.inspect && force_schedule) { if (DEV && signal.inspect && force_schedule) {
// @ts-expect-error for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
for (const fn of signal.inspect) fn();
} }
} }
} }
@ -815,8 +839,7 @@ export function unsubscribe_on_destroy(stores) {
export function get(signal) { export function get(signal) {
// @ts-expect-error // @ts-expect-error
if (DEV && signal.inspect && inspect_fn) { if (DEV && signal.inspect && inspect_fn) {
// @ts-expect-error /** @type {import('./types.js').SignalDebug} */ (signal).inspect.add(inspect_fn);
signal.inspect.add(inspect_fn);
// @ts-expect-error // @ts-expect-error
inspect_captured_signals.push(signal); inspect_captured_signals.push(signal);
} }
@ -841,10 +864,16 @@ export function get(signal) {
!(unowned && current_effect !== null) !(unowned && current_effect !== null)
) { ) {
current_dependencies_index++; current_dependencies_index++;
} else if (current_dependencies === null) { } else if (
current_dependencies = [signal]; dependencies === null ||
} else if (signal !== current_dependencies[current_dependencies.length - 1]) { current_dependencies_index === 0 ||
current_dependencies.push(signal); dependencies[current_dependencies_index - 1] !== signal
) {
if (current_dependencies === null) {
current_dependencies = [signal];
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
current_dependencies.push(signal);
}
} }
if ( if (
current_untracked_writes !== null && current_untracked_writes !== null &&
@ -1079,10 +1108,9 @@ export function set_signal_value(signal, value) {
// @ts-expect-error // @ts-expect-error
if (DEV && signal.inspect) { if (DEV && signal.inspect) {
if (is_batching_effect) { if (is_batching_effect) {
last_inspected_signal = signal; last_inspected_signal = /** @type {import('./types.js').SignalDebug} */ (signal);
} else { } else {
// @ts-expect-error for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
for (const fn of signal.inspect) fn();
} }
} }
} }
@ -1098,7 +1126,7 @@ export function destroy_signal(signal) {
const destroy = signal.y; const destroy = signal.y;
const flags = signal.f; const flags = signal.f;
destroy_references(signal); destroy_references(signal);
remove_consumer(signal, 0, true); remove_consumers(signal, 0, true);
signal.i = signal.i =
signal.r = signal.r =
signal.y = signal.y =
@ -1804,6 +1832,37 @@ function deep_read(value, visited = new Set()) {
} }
} }
/**
* Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_unstate(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = unstate(value);
if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};
for (let key in value) {
const result = deep_unstate(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
}
}
visited.set(value, contains_unstated ? nested_unstated : value);
}
return visited.get(value) ?? value;
}
// TODO remove in a few versions, before 5.0 at the latest // TODO remove in a few versions, before 5.0 at the latest
let warned_inspect_changed = false; let warned_inspect_changed = false;
@ -1817,7 +1876,7 @@ export function inspect(get_value, inspect = console.log) {
pre_effect(() => { pre_effect(() => {
const fn = () => { const fn = () => {
const value = get_value().map(unstate); const value = get_value().map((v) => deep_unstate(v));
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) { if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn( console.warn(

@ -108,6 +108,8 @@ export type ComputationSignal<V = unknown> = {
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>; export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;
export type SignalDebug<V = unknown> = SourceSignalDebug & Signal<V>;
export type EffectSignal = ComputationSignal<null | (() => void)>; export type EffectSignal = ComputationSignal<null | (() => void)>;
export type MaybeSignal<T = unknown> = T | Signal<T>; export type MaybeSignal<T = unknown> = T | Signal<T>;

@ -0,0 +1,4 @@
<!--should not error out-->
<script lang="ts">
let count: number;
</script>

@ -0,0 +1,132 @@
{
"css": null,
"js": [],
"start": 0,
"end": 27,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Comment",
"start": 0,
"end": 27,
"data": "should not error out",
"ignores": []
},
{
"type": "Text",
"start": 27,
"end": 28,
"raw": "\n",
"data": "\n"
}
],
"transparent": false
},
"options": null,
"instance": {
"type": "Script",
"start": 28,
"end": 76,
"context": "default",
"content": {
"type": "Program",
"start": 46,
"end": 67,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 4,
"column": 0
}
},
"body": [
{
"type": "VariableDeclaration",
"start": 48,
"end": 66,
"loc": {
"start": {
"line": 3,
"column": 1
},
"end": {
"line": 3,
"column": 19
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 52,
"end": 65,
"loc": {
"start": {
"line": 3,
"column": 5
},
"end": {
"line": 3,
"column": 18
}
},
"id": {
"type": "Identifier",
"start": 52,
"end": 18,
"loc": {
"start": {
"line": 3,
"column": 5
},
"end": {
"line": 3,
"column": 18
}
},
"name": "count",
"typeAnnotation": {
"type": "TSTypeAnnotation",
"start": 57,
"end": 65,
"loc": {
"start": {
"line": 3,
"column": 10
},
"end": {
"line": 3,
"column": 18
}
},
"typeAnnotation": {
"type": "TSNumberKeyword",
"start": 59,
"end": 65,
"loc": {
"start": {
"line": 3,
"column": 12
},
"end": {
"line": 3,
"column": 18
}
}
}
}
},
"init": null
}
],
"kind": "let"
}
],
"sourceType": "module"
}
}
}

@ -28,6 +28,18 @@
} }
h1:global(nav) { h1:global(nav) {
background: red; background: red;
}
h1:nth-of-type(10n+1){
background: red;
}
h1:nth-of-type(-2n+3){
background: red;
}
h1:nth-of-type(+12){
background: red;
}
h1:nth-of-type(+3n){
background: red;
} }
</style> </style>

@ -2,7 +2,7 @@
"css": { "css": {
"type": "Style", "type": "Style",
"start": 0, "start": 0,
"end": 586, "end": 806,
"attributes": [], "attributes": [],
"children": [ "children": [
{ {
@ -601,32 +601,292 @@
}, },
"start": 530, "start": 530,
"end": 577 "end": 577
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 580,
"end": 601,
"children": [
{
"type": "Selector",
"start": 580,
"end": 601,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 580,
"end": 582
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 595,
"end": 600,
"children": [
{
"type": "Selector",
"start": 595,
"end": 600,
"children": [
{
"type": "Nth",
"value": "10n+1",
"start": 595,
"end": 600
}
]
}
]
},
"start": 582,
"end": 601
}
]
}
]
},
"block": {
"type": "Block",
"start": 601,
"end": 633,
"children": [
{
"type": "Declaration",
"start": 611,
"end": 626,
"property": "background",
"value": "red"
}
]
},
"start": 580,
"end": 633
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 636,
"end": 657,
"children": [
{
"type": "Selector",
"start": 636,
"end": 657,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 636,
"end": 638
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 651,
"end": 656,
"children": [
{
"type": "Selector",
"start": 651,
"end": 656,
"children": [
{
"type": "Nth",
"value": "-2n+3",
"start": 651,
"end": 656
}
]
}
]
},
"start": 638,
"end": 657
}
]
}
]
},
"block": {
"type": "Block",
"start": 657,
"end": 689,
"children": [
{
"type": "Declaration",
"start": 667,
"end": 682,
"property": "background",
"value": "red"
}
]
},
"start": 636,
"end": 689
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 692,
"end": 711,
"children": [
{
"type": "Selector",
"start": 692,
"end": 711,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 692,
"end": 694
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 707,
"end": 710,
"children": [
{
"type": "Selector",
"start": 707,
"end": 710,
"children": [
{
"type": "Nth",
"value": "+12",
"start": 707,
"end": 710
}
]
}
]
},
"start": 694,
"end": 711
}
]
}
]
},
"block": {
"type": "Block",
"start": 711,
"end": 743,
"children": [
{
"type": "Declaration",
"start": 721,
"end": 736,
"property": "background",
"value": "red"
}
]
},
"start": 692,
"end": 743
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 746,
"end": 765,
"children": [
{
"type": "Selector",
"start": 746,
"end": 765,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 746,
"end": 748
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 761,
"end": 764,
"children": [
{
"type": "Selector",
"start": 761,
"end": 764,
"children": [
{
"type": "Nth",
"value": "+3n",
"start": 761,
"end": 764
}
]
}
]
},
"start": 748,
"end": 765
}
]
}
]
},
"block": {
"type": "Block",
"start": 765,
"end": 797,
"children": [
{
"type": "Declaration",
"start": 775,
"end": 790,
"property": "background",
"value": "red"
}
]
},
"start": 746,
"end": 797
} }
], ],
"content": { "content": {
"start": 7, "start": 7,
"end": 578, "end": 798,
"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" "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"
} }
}, },
"js": [], "js": [],
"start": 588, "start": 808,
"end": 600, "end": 820,
"type": "Root", "type": "Root",
"fragment": { "fragment": {
"type": "Fragment", "type": "Fragment",
"nodes": [ "nodes": [
{ {
"type": "Text", "type": "Text",
"start": 586, "start": 806,
"end": 588, "end": 808,
"raw": "\n\n", "raw": "\n\n",
"data": "\n\n" "data": "\n\n"
}, },
{ {
"type": "RegularElement", "type": "RegularElement",
"start": 588, "start": 808,
"end": 600, "end": 820,
"name": "h1", "name": "h1",
"attributes": [], "attributes": [],
"fragment": { "fragment": {
@ -634,8 +894,8 @@
"nodes": [ "nodes": [
{ {
"type": "Text", "type": "Text",
"start": 592, "start": 812,
"end": 595, "end": 815,
"raw": "Foo", "raw": "Foo",
"data": "Foo" "data": "Foo"
} }

@ -0,0 +1,18 @@
<style>
/* test that all these are parsed correctly */
::view-transition-old(x-y) {
color: red;
}
:global(::view-transition-old(x-y)) {
color: red;
}
::highlight(rainbow-color-1) {
color: red;
}
custom-element::part(foo) {
color: red;
}
::slotted(.content) {
color: red;
}
</style>

@ -0,0 +1,246 @@
{
"css": {
"type": "Style",
"start": 0,
"end": 313,
"attributes": [],
"children": [
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 60,
"end": 86,
"children": [
{
"type": "Selector",
"start": 60,
"end": 86,
"children": [
{
"type": "PseudoElementSelector",
"name": "view-transition-old",
"start": 60,
"end": 81
}
]
}
]
},
"block": {
"type": "Block",
"start": 88,
"end": 109,
"children": [
{
"type": "Declaration",
"start": 92,
"end": 102,
"property": "color",
"value": "red"
}
]
},
"start": 60,
"end": 109
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 111,
"end": 146,
"children": [
{
"type": "Selector",
"start": 111,
"end": 146,
"children": [
{
"type": "PseudoClassSelector",
"name": "global",
"args": {
"type": "SelectorList",
"start": 119,
"end": 145,
"children": [
{
"type": "Selector",
"start": 119,
"end": 145,
"children": [
{
"type": "PseudoElementSelector",
"name": "view-transition-old",
"start": 119,
"end": 140
}
]
}
]
},
"start": 111,
"end": 146
}
]
}
]
},
"block": {
"type": "Block",
"start": 148,
"end": 169,
"children": [
{
"type": "Declaration",
"start": 152,
"end": 162,
"property": "color",
"value": "red"
}
]
},
"start": 111,
"end": 169
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 171,
"end": 199,
"children": [
{
"type": "Selector",
"start": 171,
"end": 199,
"children": [
{
"type": "PseudoElementSelector",
"name": "highlight",
"start": 171,
"end": 182
}
]
}
]
},
"block": {
"type": "Block",
"start": 200,
"end": 218,
"children": [
{
"type": "Declaration",
"start": 204,
"end": 214,
"property": "color",
"value": "red"
}
]
},
"start": 171,
"end": 218
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 220,
"end": 245,
"children": [
{
"type": "Selector",
"start": 220,
"end": 245,
"children": [
{
"type": "TypeSelector",
"name": "custom-element",
"start": 220,
"end": 234
},
{
"type": "PseudoElementSelector",
"name": "part",
"start": 234,
"end": 240
}
]
}
]
},
"block": {
"type": "Block",
"start": 246,
"end": 264,
"children": [
{
"type": "Declaration",
"start": 250,
"end": 260,
"property": "color",
"value": "red"
}
]
},
"start": 220,
"end": 264
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 266,
"end": 285,
"children": [
{
"type": "Selector",
"start": 266,
"end": 285,
"children": [
{
"type": "PseudoElementSelector",
"name": "slotted",
"start": 266,
"end": 275
}
]
}
]
},
"block": {
"type": "Block",
"start": 286,
"end": 304,
"children": [
{
"type": "Declaration",
"start": 290,
"end": 300,
"property": "color",
"value": "red"
}
]
},
"start": 266,
"end": 304
}
],
"content": {
"start": 7,
"end": 305,
"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"
}
},
"js": [],
"start": null,
"end": null,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [],
"transparent": false
},
"options": null
}

@ -0,0 +1,9 @@
<script>
const { item } = $props();
</script>
<div>
{#if item}
{item.length}
{/if}
</div>

@ -0,0 +1,26 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, component }) {
const [b1, b2] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
);
flushSync(() => {
b2.click();
});
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
);
flushSync(() => {
b1.click();
});
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div></div><div>3</div><button>set null</button><button>set object</button'
);
}
});

@ -0,0 +1,12 @@
<script>
import Component from './Component.svelte';
let items = $state(['hello', 'world', 'bye']);
</script>
{#each items as item}
<Component {item} />
{/each}
<button onclick={() => (items[1] = null)}> set null </button>
<button onclick={() => (items[1] = 'hello')}> set object </button>

@ -0,0 +1,40 @@
import { test } from '../../test';
/**
* @type {any[]}
*/
let log;
/**
* @type {typeof console.log}}
*/
let original_log;
export default test({
compileOptions: {
dev: true
},
before_test() {
log = [];
original_log = console.log;
console.log = (...v) => {
log.push(...v);
};
},
after_test() {
console.log = original_log;
},
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
assert.deepEqual(log, [
'init',
{ x: { count: 0 } },
[{ count: 0 }],
'update',
{ x: { count: 1 } },
[{ count: 1 }]
]);
}
});

@ -0,0 +1,7 @@
<script>
let x = $state({count: 0});
$inspect({x}, [x]);
</script>
<button on:click={() => x.count++}>{x.count}</button>

@ -9,16 +9,18 @@ export default function Function_prop_no_getter($$anchor, $$props) {
let count = $.source(0); let count = $.source(0);
function onmouseup() { function onmouseup() {
$.set(count, $.proxy($.get(count) + 2)); $.set(count, $.get(count) + 2);
} }
const plusOne = (num) => num + 1;
/* Init */ /* Init */
var fragment = $.comment($$anchor); var fragment = $.comment($$anchor);
var node = $.child_frag(fragment); var node = $.child_frag(fragment);
Button(node, { Button(node, {
onmousedown: () => $.set(count, $.proxy($.get(count) + 1)), onmousedown: () => $.set(count, $.get(count) + 1),
onmouseup, onmouseup,
onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
children: ($$anchor, $$slotProps) => { children: ($$anchor, $$slotProps) => {
/* Init */ /* Init */
var node_1 = $.space($$anchor); var node_1 = $.space($$anchor);

@ -11,6 +11,7 @@ export default function Function_prop_no_getter($$payload, $$props) {
count += 2; count += 2;
} }
const plusOne = (num) => num + 1;
const anchor = $.create_anchor($$payload); const anchor = $.create_anchor($$payload);
$$payload.out += `${anchor}`; $$payload.out += `${anchor}`;
@ -18,6 +19,7 @@ export default function Function_prop_no_getter($$payload, $$props) {
Button($$payload, { Button($$payload, {
onmousedown: () => count += 1, onmousedown: () => count += 1,
onmouseup, onmouseup,
onmouseenter: () => count = plusOne(count),
children: ($$payload, $$slotProps) => { children: ($$payload, $$slotProps) => {
$$payload.out += `clicks: ${$.escape(count)}`; $$payload.out += `clicks: ${$.escape(count)}`;
} }

@ -4,8 +4,10 @@
function onmouseup() { function onmouseup() {
count += 2; count += 2;
} }
const plusOne = (num) => num + 1;
</script> </script>
<Button onmousedown={() => count += 1} {onmouseup}> <Button onmousedown={() => count += 1} {onmouseup} onmouseenter={() => count = plusOne(count)}>
clicks: {count} clicks: {count}
</Button> </Button>

@ -3,11 +3,11 @@
"code": "invalid-css-global-placement", "code": "invalid-css-global-placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 2, "line": 5,
"column": 6 "column": 6
}, },
"end": { "end": {
"line": 2, "line": 5,
"column": 19 "column": 19
} }
} }

@ -1,4 +1,7 @@
<style> <style>
.foo :global(.bar):first-child {
color: red;
}
.foo :global(.bar):first-child .baz { .foo :global(.bar):first-child .baz {
color: red; color: red;
} }

File diff suppressed because it is too large Load Diff

@ -121,8 +121,8 @@ importers:
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5 version: 1.0.5
dts-buddy: dts-buddy:
specifier: ^0.4.0 specifier: ^0.4.3
version: 0.4.0(typescript@5.2.2) version: 0.4.3(typescript@5.2.2)
esbuild: esbuild:
specifier: ^0.19.2 specifier: ^0.19.2
version: 0.19.5 version: 0.19.5
@ -3646,11 +3646,11 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/dts-buddy@0.4.0(typescript@5.2.2): /dts-buddy@0.4.3(typescript@5.2.2):
resolution: {integrity: sha512-L8sHp1mmpufZhz/+HLIA40hJG6T937rsWhEIED2W2QMbGSpdg1G5pc2EK1WLKvmd1DvGZ0Qs8uoeSUaqK5mqkw==} resolution: {integrity: sha512-vytwDCQAj8rqYPbGsrjiOCRv3O2ipwyUwSc5/II1MpS/Eq6KNZNkGU1djOA31nL7jh7092W/nwbwZHCKedf8Vw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: '>=5.0.4 <5.3' typescript: '>=5.0.4 <5.4'
dependencies: dependencies:
'@jridgewell/source-map': 0.3.5 '@jridgewell/source-map': 0.3.5
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15

@ -156,7 +156,7 @@ Within the template, snippets are values just like any other. As such, they can
<Table data={fruits} {header} {row} /> <Table data={fruits} {header} {row} />
``` ```
As an authoring convenience, snippets declare directly _inside_ a component implicitly become props _on_ the component ([demo](/#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)): As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)):
```svelte ```svelte
<!-- this is semantically the same as the above --> <!-- this is semantically the same as the above -->

Loading…
Cancel
Save