pull/16197/head
Rich Harris 4 months ago
commit 29406c9f63

@ -20,9 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin
If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates.
> [!NOTE] Class instances are not proxied. You can create [reactive state fields](#Classes) on classes that you define. Svelte provides reactive implementations of built-ins like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity).
State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this...
State is proxified recursively until Svelte finds something other than an array or simple object (like a class). In a case like this...
```js
let todos = $state([
@ -67,7 +65,7 @@ todos[0].done = !todos[0].done;
### Classes
You can also use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`:
Class instances are not proxied. Instead, you can use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`:
```js
// @errors: 7006 2554
@ -121,6 +119,8 @@ class Todo {
}
```
> Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity).
## `$state.raw`
In cases where you don't want objects and arrays to be deeply reactive you can use `$state.raw`.
@ -145,6 +145,8 @@ person = {
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects).
As with `$state`, you can declare class fields using `$state.raw`.
## `$state.snapshot`
To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:

@ -269,11 +269,11 @@ In general, `$effect` is best considered something of an escape hatch — useful
If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)):
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)):
```svelte
<script>
let total = 100;
const total = 100;
let spent = $state(0);
let left = $state(total);
@ -297,32 +297,26 @@ You might be tempted to do something convoluted with effects to link one value t
</label>
```
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE51SsW6DMBT8FcvqABINdOhCIFKXTt06lg4GHpElYyz8iECIf69tcIIipo6-u3f3fPZMJWuBpvRzkBXyTpKSy5rLq6YRbbgATdOfmeKkrMgCBt9GPpQ66RsItFjJNBzhVScRJBobmumq5wovhSxQABLskAmSk7ckOXtMKyM22ItGhhAk4Z0R0OwIN-tIQzd-90HVhvy2HsGNiQFCMltBgd7XoecV2xzXNV7XaEcth7ZfRv7kujnsTX2Qd7USb5rFjwZkJlgJwpWRcakG04cpOS9oz-QVCuoeInXW-RyEJL-sG0b7Wy6kZWM-u7CFxM5tdrIl9qg72vB74H-y7T2iXROHyVb0CLanp1yNk4D1A1jQ91hzrQSbUtIIGLcir0ylJDm9Q7urz42bX4UwIk2xH2D5Xf4A7SeMcMQCAAA=)):
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE5VRvW7CMBB-FcvqECQK6dDFJEgsnfoGTQdDLsjSxVjxhYKivHvPBwFUsXS8774_nwftbQva6I_e78gdvNo6Xzu_j3quG4cQtfkaNJ1DIiWA8atkE8IiHgEpYVsb4Rm-O3gCT2yji7jrXKB15StiOJKiA1lUpXrL81VCEUjFwHTGXiJZgiyf3TYIjSxq6NwR6uyifr0ohMbEZnpHH2rWf7ImS8KZGtK6osl_UqelRIyVL5b3ir5AuwWUtoXzoee6fIWy0p31e6i0XMocLfZQDuI6qtaeykGcR7UU6XWznFAZU9LN_X9B2UyVayk9f3ji0-REugen6U9upDOCcAWcLlS7GNCejWoQTqsLtrfBqHzxDu3DrUTOf0xwIm2o62H85sk6_OHG2jQWI4y_3byXXGMCAAA=)):
```svelte
<script>
let total = 100;
const total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(value) {
spent = value;
left = total - spent;
}
let left = $derived(total - spent);
function updateLeft(value) {
left = value;
+++ function updateLeft(left) {
spent = total - left;
}
}+++
</script>
<label>
<input type="range" bind:value={() => spent, updateSpent} max={total} />
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={() => left, updateLeft} max={total} />
<input type="range" +++bind:value={() => left, updateLeft}+++ max={total} />
{left}/{total} left
</label>
```

@ -4,7 +4,7 @@ title: bind:
Data ordinarily flows down, from parent to child. The `bind:` directive allows data to flow the other way, from child to parent.
The general syntax is `bind:property={expression}`, where `expression` is an _lvalue_ (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent:
The general syntax is `bind:property={expression}`, where `expression` is an [_lvalue_](https://press.rebus.community/programmingfundamentals/chapter/lvalue-and-rvalue/) (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent:
<!-- prettier-ignore -->
```svelte
@ -142,26 +142,27 @@ Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs
## `<input bind:group>`
Inputs that work together can use `bind:group`.
Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sIAAAAAAAAE62T32_TMBDH_5XDQkpbrct7SCMGEvCEECDxsO7BSW6L2c227EvbKOv_jp0f6jYhQKJv5_P3PvdL1wstH1Bk4hMSGdgbRzUssFaM9VJciFtF6EV23QvubNRFR_BPUVfWXvodEkdfKT3-zl8Zzag5YETuK6csF1u9ZUIGNo4VkYQNvPYsGRfJF5JKJ8s3QRJE6WoFb2Nq6K-ck13u2Sl9Vxxhlc6QUBIFnz9Brm9ifJ6esun81XoNd860FmtwslYGlLYte5AO4aHlVhJ1gIeKWq92COt1iMtJlkhFPkgh1rHZiiF6K6BUus4G5KafGznCTlIbVUMfQZUWMJh5OrL-C_qjMYSwb1DyiH7iOEuCb1ZpWTUjfHqcwC_GWDVY3ZfmME_SGttSmD9IHaYatvWHIc6xLyqad3mq6KuqcCwnWn9p8p-p71BqP2IH81zc9w2in-od7XORP7ayCpd5YCeXI_-p59mObPF9WmwGpx3nqS2Gzw8TO3zOaS5_GqUXyQUkS3h8hOSz0ZhMESHGc0c4Hm3MAn00t1wrb0l2GZRkqvt4sXwczm6Qh8vnUJzI2LV4vAkvqWgfehTZrSSPx19WiVfFfAQAAA==)):
```svelte
<!--- file: BurritoChooser.svelte --->
<script>
let tortilla = $state('Plain');
/** @type {Array<string>} */
/** @type {string[]} */
let fillings = $state([]);
</script>
<!-- grouped radio inputs are mutually exclusive -->
<input type="radio" bind:group={tortilla} value="Plain" />
<input type="radio" bind:group={tortilla} value="Whole wheat" />
<input type="radio" bind:group={tortilla} value="Spinach" />
<label><input type="radio" bind:group={tortilla} value="Plain" /> Plain</label>
<label><input type="radio" bind:group={tortilla} value="Whole wheat" /> Whole wheat</label>
<label><input type="radio" bind:group={tortilla} value="Spinach" /> Spinach</label>
<!-- grouped checkbox inputs populate an array -->
<input type="checkbox" bind:group={fillings} value="Rice" />
<input type="checkbox" bind:group={fillings} value="Beans" />
<input type="checkbox" bind:group={fillings} value="Cheese" />
<input type="checkbox" bind:group={fillings} value="Guac (extra)" />
<label><input type="checkbox" bind:group={fillings} value="Rice" /> Rice</label>
<label><input type="checkbox" bind:group={fillings} value="Beans" /> Beans</label>
<label><input type="checkbox" bind:group={fillings} value="Cheese" /> Cheese</label>
<label><input type="checkbox" bind:group={fillings} value="Guac (extra)" /> Guac (extra)</label>
```
> [!NOTE] `bind:group` only works if the inputs are in the same Svelte component.

@ -114,6 +114,8 @@ When constructing a custom element, you can tailor several aspects by defining `
...
```
> [!NOTE] While Typescript is supported in the `extend` function, it is subject to limitations: you need to set `lang="ts"` on one of the scripts AND you can only use [erasable syntax](https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly) in it. They are not processed by script preprocessors.
## Caveats and limitations
Custom elements can be a useful way to package components for consumption in a non-Svelte app, as they will work with vanilla HTML and JavaScript as well as [most frameworks](https://custom-elements-everywhere.com/). There are, however, some important differences to be aware of:

@ -216,6 +216,19 @@ Consider the following code:
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
### select_multiple_invalid_value
```
The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.
```
When using `<select multiple value={...}>`, Svelte will mark all selected `<option>` elements as selected by iterating over the array passed to `value`. If `value` is not an array, Svelte will emit this warning and keep the selected options as they are.
To silence the warning, ensure that `value`:
- is an array for an explicit selection
- is `null` or `undefined` to keep the selection as is
### state_proxy_equality_mismatch
```

@ -632,6 +632,12 @@ In some situations a selector may target an element that is not 'visible' to the
</style>
```
### custom_element_props_identifier
```
Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option.
```
### element_implicitly_closed
```

@ -1,5 +1,59 @@
# svelte
## 5.33.4
### Patch Changes
- fix: narrow `defaultChecked` to boolean ([#16009](https://github.com/sveltejs/svelte/pull/16009))
- fix: warn when using rest or identifier in custom elements without props option ([#16003](https://github.com/sveltejs/svelte/pull/16003))
## 5.33.3
### Patch Changes
- fix: allow using typescript in `customElement.extend` option ([#16001](https://github.com/sveltejs/svelte/pull/16001))
- fix: cleanup event handlers on media elements ([#16005](https://github.com/sveltejs/svelte/pull/16005))
## 5.33.2
### Patch Changes
- fix: correctly parse escaped unicode characters in css selector ([#15976](https://github.com/sveltejs/svelte/pull/15976))
- fix: don't mark deriveds as clean if updating during teardown ([#15997](https://github.com/sveltejs/svelte/pull/15997))
## 5.33.1
### Patch Changes
- fix: make deriveds on the server lazy again ([#15964](https://github.com/sveltejs/svelte/pull/15964))
## 5.33.0
### Minor Changes
- feat: XHTML compliance ([#15538](https://github.com/sveltejs/svelte/pull/15538))
- feat: add `fragments: 'html' | 'tree'` option for wider CSP compliance ([#15538](https://github.com/sveltejs/svelte/pull/15538))
## 5.32.2
### Patch Changes
- chore: simplify `<pre>` cleaning ([#15980](https://github.com/sveltejs/svelte/pull/15980))
- fix: attach `__svelte_meta` correctly to elements following a CSS wrapper ([#15982](https://github.com/sveltejs/svelte/pull/15982))
- fix: import untrack directly from client in `svelte/attachments` ([#15974](https://github.com/sveltejs/svelte/pull/15974))
## 5.32.1
### Patch Changes
- Warn when an invalid `<select multiple>` value is given ([#14816](https://github.com/sveltejs/svelte/pull/14816))
## 5.32.0
### Minor Changes

@ -1115,8 +1115,8 @@ export interface HTMLInputAttributes extends HTMLAttributes<HTMLInputElement> {
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: any;
defaultvalue?: any;
defaultChecked?: any;
defaultchecked?: any;
defaultChecked?: boolean | undefined | null;
defaultchecked?: boolean | undefined | null;
width?: number | string | undefined | null;
webkitdirectory?: boolean | undefined | null;
@ -2066,7 +2066,7 @@ export interface SvelteHTMLElements {
failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>;
};
[name: string]: { [name: string]: any };
[name: string & {}]: { [name: string]: any };
}
export type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary;

@ -180,6 +180,17 @@ Consider the following code:
To fix it, either create callback props to communicate changes, or mark `person` as [`$bindable`]($bindable).
## select_multiple_invalid_value
> The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.
When using `<select multiple value={...}>`, Svelte will mark all selected `<option>` elements as selected by iterating over the array passed to `value`. If `value` is not an array, Svelte will emit this warning and keep the selected options as they are.
To silence the warning, ensure that `value`:
- is an array for an explicit selection
- is `null` or `undefined` to keep the selection as is
## state_proxy_equality_mismatch
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results

@ -1,3 +1,7 @@
## custom_element_props_identifier
> Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option.
## export_let_unused
> Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`

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

@ -2,7 +2,7 @@
/** @import { Attachment } from './public' */
import { noop, render_effect } from 'svelte/internal/client';
import { ATTACHMENT_KEY } from '../constants.js';
import { untrack } from 'svelte';
import { untrack } from '../index-client.js';
import { teardown } from '../internal/client/reactivity/effects.js';
/**

@ -43,6 +43,11 @@ export function compile(source, options) {
instance: parsed.instance && remove_typescript_nodes(parsed.instance),
module: parsed.module && remove_typescript_nodes(parsed.module)
};
if (combined_options.customElementOptions?.extend) {
combined_options.customElementOptions.extend = remove_typescript_nodes(
combined_options.customElementOptions?.extend
);
}
}
const analysis = analyze_component(parsed, source, combined_options);

@ -12,6 +12,7 @@ const REGEX_NTH_OF =
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_UNICODE_SEQUENCE = /^\\[0-9a-fA-F]{1,6}(\r\n|\s)?/;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
@ -580,25 +581,26 @@ function read_identifier(parser) {
e.css_expected_identifier(start);
}
let escaped = false;
while (parser.index < parser.template.length) {
const char = parser.template[parser.index];
if (escaped) {
identifier += '\\' + char;
escaped = false;
} else if (char === '\\') {
escaped = true;
if (char === '\\') {
const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE);
if (sequence) {
identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16));
parser.index += sequence.length;
} else {
identifier += '\\' + parser.template[parser.index + 1];
parser.index += 2;
}
} else if (
/** @type {number} */ (char.codePointAt(0)) >= 160 ||
REGEX_VALID_IDENTIFIER_CHAR.test(char)
) {
identifier += char;
parser.index++;
} else {
break;
}
parser.index++;
}
if (identifier === '') {

@ -4,6 +4,7 @@
import { get_rune } from '../../scope.js';
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js';
@ -52,6 +53,19 @@ export function VariableDeclarator(node, context) {
e.props_invalid_identifier(node);
}
if (
context.state.analysis.custom_element &&
context.state.options.customElementOptions?.props == null
) {
let warn_on;
if (
node.id.type === 'Identifier' ||
(warn_on = node.id.properties.find((p) => p.type === 'RestElement')) != null
) {
w.custom_element_props_identifier(warn_on ?? node.id);
}
}
context.state.analysis.needs_props = true;
if (node.id.type === 'Identifier') {

@ -156,10 +156,6 @@ export function client_component(analysis, options) {
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace: options.namespace,
bound_contenteditable: false,
async: []
@ -178,8 +174,7 @@ export function client_component(analysis, options) {
expressions: /** @type {any} */ (null),
async_expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
locations: /** @type {any} */ (null)
template: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (

@ -0,0 +1,18 @@
const svg_attributes =
'accent-height accumulate additive alignment-baseline allowReorder alphabetic amplitude arabic-form ascent attributeName attributeType autoReverse azimuth baseFrequency baseline-shift baseProfile bbox begin bias by calcMode cap-height class clip clipPathUnits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor cx cy d decelerate descent diffuseConstant direction display divisor dominant-baseline dur dx dy edgeMode elevation enable-background end exponent externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format from fr fx fy g1 g2 glyph-name glyph-orientation-horizontal glyph-orientation-vertical glyphRef gradientTransform gradientUnits hanging height href horiz-adv-x horiz-origin-x id ideographic image-rendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength kerning keyPoints keySplines keyTimes lang lengthAdjust letter-spacing lighting-color limitingConeAngle local marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask maskContentUnits maskUnits mathematical max media method min mode name numOctaves offset onabort onactivate onbegin onclick onend onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onrepeat onresize onscroll onunload opacity operator order orient orientation origin overflow overline-position overline-thickness panose-1 paint-order pathLength patternContentUnits patternTransform patternUnits pointer-events points pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits r radius refX refY rendering-intent repeatCount repeatDur requiredExtensions requiredFeatures restart result rotate rx ry scale seed shape-rendering slope spacing specularConstant specularExponent speed spreadMethod startOffset stdDeviation stemh stemv stitchTiles stop-color stop-opacity strikethrough-position strikethrough-thickness string stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale systemLanguage tabindex tableValues target targetX targetY text-anchor text-decoration text-rendering textLength to transform type u1 u2 underline-position underline-thickness unicode unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical values version vert-adv-y vert-origin-x vert-origin-y viewBox viewTarget visibility width widths word-spacing writing-mode x x-height x1 x2 xChannelSelector xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y y1 y2 yChannelSelector z zoomAndPan'.split(
' '
);
const svg_attribute_lookup = new Map();
svg_attributes.forEach((name) => {
svg_attribute_lookup.set(name.toLowerCase(), name);
});
/**
* @param {string} name
*/
export default function fix_attribute_casing(name) {
name = name.toLowerCase();
return svg_attribute_lookup.get(name) || name;
}

@ -0,0 +1,68 @@
/** @import { Location } from 'locate-character' */
/** @import { Namespace } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types.js' */
/** @import { Node } from './types.js' */
import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../../../constants.js';
import { dev, locator } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {Node[]} nodes
*/
function build_locations(nodes) {
const array = b.array([]);
for (const node of nodes) {
if (node.type !== 'element') continue;
const { line, column } = /** @type {Location} */ (locator(node.start));
const expression = b.array([b.literal(line), b.literal(column)]);
const children = build_locations(node.children);
if (children.elements.length > 0) {
expression.elements.push(children);
}
array.elements.push(expression);
}
return array;
}
/**
* @param {ComponentClientTransformState} state
* @param {Namespace} namespace
* @param {number} [flags]
*/
export function transform_template(state, namespace, flags = 0) {
const tree = state.options.fragments === 'tree';
const expression = tree ? state.template.as_tree() : state.template.as_html();
if (tree) {
if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
}
let call = b.call(
tree ? `$.from_tree` : `$.from_${namespace}`,
expression,
flags ? b.literal(flags) : undefined
);
if (state.template.contains_script_tag) {
call = b.call(`$.with_script`, call);
}
if (dev) {
call = b.call(
'$.add_locations',
call,
b.member(b.id(state.analysis.name), '$.FILENAME', true),
build_locations(state.template.nodes)
);
}
return call;
}

@ -0,0 +1,162 @@
/** @import { AST } from '#compiler' */
/** @import { Node, Element } from './types'; */
import { escape_html } from '../../../../../escaping.js';
import { is_void } from '../../../../../utils.js';
import * as b from '#compiler/builders';
import fix_attribute_casing from './fix-attribute-casing.js';
import { regex_starts_with_newline } from '../../../patterns.js';
export class Template {
/**
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
*/
contains_script_tag = false;
/** `true` if the HTML template needs to be instantiated with `importNode` */
needs_import_node = false;
/** @type {Node[]} */
nodes = [];
/** @type {Node[][]} */
#stack = [this.nodes];
/** @type {Element | undefined} */
#element;
#fragment = this.nodes;
/**
* @param {string} name
* @param {number} start
*/
push_element(name, start) {
this.#element = {
type: 'element',
name,
attributes: {},
children: [],
start
};
this.#fragment.push(this.#element);
this.#fragment = /** @type {Element} */ (this.#element).children;
this.#stack.push(this.#fragment);
}
/** @param {string} [data] */
push_comment(data) {
this.#fragment.push({ type: 'comment', data });
}
/** @param {AST.Text[]} nodes */
push_text(nodes) {
this.#fragment.push({ type: 'text', nodes });
}
pop_element() {
this.#stack.pop();
this.#fragment = /** @type {Node[]} */ (this.#stack.at(-1));
}
/**
* @param {string} key
* @param {string | undefined} value
*/
set_prop(key, value) {
/** @type {Element} */ (this.#element).attributes[key] = value;
}
as_html() {
return b.template([b.quasi(this.nodes.map(stringify).join(''), true)], []);
}
as_tree() {
// if the first item is a comment we need to add another comment for effect.start
if (this.nodes[0].type === 'comment') {
this.nodes.unshift({ type: 'comment', data: undefined });
}
return b.array(this.nodes.map(objectify));
}
}
/**
* @param {Node} item
*/
function stringify(item) {
if (item.type === 'text') {
return item.nodes.map((node) => node.raw).join('');
}
if (item.type === 'comment') {
return item.data ? `<!--${item.data}-->` : '<!>';
}
let str = `<${item.name}`;
for (const key in item.attributes) {
const value = item.attributes[key];
str += ` ${key}`;
if (value !== undefined) str += `="${escape_html(value, true)}"`;
}
if (is_void(item.name)) {
str += '/>'; // XHTML compliance
} else {
str += `>`;
str += item.children.map(stringify).join('');
str += `</${item.name}>`;
}
return str;
}
/** @param {Node} item */
function objectify(item) {
if (item.type === 'text') {
return b.literal(item.nodes.map((node) => node.data).join(''));
}
if (item.type === 'comment') {
return item.data ? b.array([b.literal(`// ${item.data}`)]) : null;
}
const element = b.array([b.literal(item.name)]);
const attributes = b.object([]);
for (const key in item.attributes) {
const value = item.attributes[key];
attributes.properties.push(
b.prop(
'init',
b.key(fix_attribute_casing(key)),
value === undefined ? b.void0 : b.literal(value)
)
);
}
if (attributes.properties.length > 0 || item.children.length > 0) {
element.elements.push(attributes.properties.length > 0 ? attributes : b.null);
}
if (item.children.length > 0) {
const children = item.children.map(objectify);
element.elements.push(...children);
// special case — strip leading newline from `<pre>` and `<textarea>`
if (item.name === 'pre' || item.name === 'textarea') {
const first = children[0];
if (first?.type === 'Literal') {
first.value = /** @type {string} */ (first.value).replace(regex_starts_with_newline, '');
}
}
}
return element;
}

@ -0,0 +1,22 @@
import type { AST } from '#compiler';
export interface Element {
type: 'element';
name: string;
attributes: Record<string, string | undefined>;
children: Node[];
/** used for populating __svelte_meta */
start: number;
}
export interface Text {
type: 'text';
nodes: AST.Text[];
}
export interface Comment {
type: 'comment';
data: string | undefined;
}
export type Node = Element | Text | Comment;

@ -3,16 +3,15 @@ import type {
Statement,
LabeledStatement,
Identifier,
PrivateIdentifier,
Expression,
AssignmentExpression,
UpdateExpression,
VariableDeclaration
} from 'estree';
import type { AST, Namespace, StateField, ValidatedCompileOptions } from '#compiler';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared';
import type { Template } from './transform-template/template.js';
export interface ClientTransformState extends TransformState {
/**
@ -55,26 +54,10 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** Expressions used inside the render effect */
readonly async_expressions: Array<{ id: Identifier; expression: Expression }>;
/** The HTML template string */
readonly template: Array<string | Expression>;
readonly locations: SourceLocation[];
readonly template: Template;
readonly metadata: {
namespace: Namespace;
bound_contenteditable: boolean;
/**
* Stuff that is set within the children of one `Fragment` visitor that is relevant
* to said fragment. Shouldn't be destructured or otherwise spread unless inside the
* `Fragment` visitor to keep the object reference intact (it's also nested
* within `metadata` for this reason).
*/
context: {
/** `true` if the HTML template needs to be instantiated with `importNode` */
template_needs_import_node: boolean;
/**
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
*/
template_contains_script_tag: boolean;
};
/**
* Synthetic async deriveds belonging to the current fragment
*/

@ -11,7 +11,7 @@ import { get_value } from './shared/declarations.js';
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));

@ -7,5 +7,5 @@
*/
export function Comment(node, context) {
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true
context.state.template.push(`<!--${node.data}-->`);
context.state.template.push_comment(node.data);
}

@ -32,7 +32,7 @@ export function EachBlock(node, context) {
);
if (!each_node_meta.is_controlled) {
context.state.template.push('<!>');
context.state.template.push_comment();
}
let flags = 0;

@ -1,14 +1,13 @@
/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
* @param {AST.Fragment} node
@ -18,7 +17,7 @@ export function Fragment(node, context) {
// Creates a new block which looks roughly like this:
// ```js
// // hoisted:
// const block_name = $.template(`...`);
// const block_name = $.from_html(`...`);
//
// // for the main block:
// const id = block_name();
@ -66,14 +65,9 @@ export function Fragment(node, context) {
expressions: [],
async_expressions: [],
after_update: [],
template: [],
locations: [],
template: new Template(),
transform: { ...context.state.transform },
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable,
async: []
@ -89,24 +83,6 @@ export function Fragment(node, context) {
body.push(b.stmt(b.call('$.next')));
}
/**
* @param {Identifier} template_name
* @param {Expression[]} args
*/
const add_template = (template_name, args) => {
let call = b.call(get_template_function(namespace, state), ...args);
if (dev) {
call = b.call(
'$.add_locations',
call,
b.member(b.id(context.state.analysis.name), '$.FILENAME', true),
build_locations(state.locations)
);
}
context.state.hoisted.push(b.var(template_name, call));
};
if (is_single_element) {
const element = /** @type {AST.RegularElement} */ (trimmed[0]);
@ -117,14 +93,10 @@ export function Fragment(node, context) {
node: id
});
/** @type {Expression[]} */
const args = [join_template(state.template)];
let flags = state.template.needs_import_node ? TEMPLATE_USE_IMPORT_NODE : undefined;
if (state.metadata.context.template_needs_import_node) {
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
}
add_template(template_name, args);
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@ -164,15 +136,16 @@ export function Fragment(node, context) {
let flags = TEMPLATE_FRAGMENT;
if (state.metadata.context.template_needs_import_node) {
if (state.template.needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
if (state.template.length === 1 && state.template[0] === '<!>') {
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
} else {
add_template(template_name, [join_template(state.template), b.literal(flags)]);
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
body.push(b.var(id, b.call(template_name)));
}
@ -199,86 +172,3 @@ export function Fragment(node, context) {
return b.block(body);
}
/**
* @param {Array<string | Expression>} items
*/
function join_template(items) {
let quasi = b.quasi('');
const template = b.template([quasi], []);
/**
* @param {Expression} expression
*/
function push(expression) {
if (expression.type === 'TemplateLiteral') {
for (let i = 0; i < expression.expressions.length; i += 1) {
const q = expression.quasis[i];
const e = expression.expressions[i];
quasi.value.cooked += /** @type {string} */ (q.value.cooked);
push(e);
}
const last = /** @type {TemplateElement} */ (expression.quasis.at(-1));
quasi.value.cooked += /** @type {string} */ (last.value.cooked);
} else if (expression.type === 'Literal') {
/** @type {string} */ (quasi.value.cooked) += expression.value;
} else {
template.expressions.push(expression);
template.quasis.push((quasi = b.quasi('')));
}
}
for (const item of items) {
if (typeof item === 'string') {
quasi.value.cooked += item;
} else {
push(item);
}
}
for (const quasi of template.quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
quasi.tail = true;
return template;
}
/**
*
* @param {Namespace} namespace
* @param {ComponentClientTransformState} state
* @returns
*/
function get_template_function(namespace, state) {
const contains_script_tag = state.metadata.context.template_contains_script_tag;
return namespace === 'svg'
? contains_script_tag
? '$.svg_template_with_script'
: '$.ns_template'
: namespace === 'mathml'
? '$.mathml_template'
: contains_script_tag
? '$.template_with_script'
: '$.template';
}
/**
* @param {SourceLocation[]} locations
*/
function build_locations(locations) {
return b.array(
locations.map((loc) => {
const expression = b.array([b.literal(loc[0]), b.literal(loc[1])]);
if (loc.length === 3) {
expression.elements.push(build_locations(loc[2]));
}
return expression;
})
);
}

@ -9,7 +9,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function HtmlTag(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const { has_await } = node.metadata.expression;

@ -8,7 +8,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));

@ -8,7 +8,7 @@ import * as b from '#compiler/builders';
* @param {ComponentContext} context
*/
export function KeyBlock(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const key = /** @type {Expression} */ (context.visit(node.expression));
const body = /** @type {Expression} */ (context.visit(node.fragment));

@ -1,17 +1,14 @@
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import {
cannot_be_set_statically,
is_boolean_attribute,
is_dom_property,
is_load_error_element,
is_void
is_load_error_element
} from '../../../../../utils.js';
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import { is_ignored } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../nodes.js';
@ -39,40 +36,24 @@ import { visit_event_attribute } from './shared/events.js';
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
/** @type {SourceLocation} */
let location = [-1, -1];
if (dev) {
const loc = locator(node.start);
if (loc) {
location[0] = loc.line;
location[1] = loc.column;
context.state.locations.push(location);
}
}
context.state.template.push_element(node.name, node.start);
if (node.name === 'noscript') {
context.state.template.push('<noscript></noscript>');
context.state.template.pop_element();
return;
}
const is_custom_element = is_custom_element_node(node);
if (node.name === 'video' || is_custom_element) {
// cloneNode is faster, but it does not instantiate the underlying class of the
// custom element until the template is connected to the dom, which would
// cause problems when setting properties on the custom element.
// Therefore we need to use importNode instead, which doesn't have this caveat.
// Additionally, Webkit browsers need importNode for video elements for autoplay
// to work correctly.
context.state.metadata.context.template_needs_import_node = true;
}
if (node.name === 'script') {
context.state.metadata.context.template_contains_script_tag = true;
}
// cloneNode is faster, but it does not instantiate the underlying class of the
// custom element until the template is connected to the dom, which would
// cause problems when setting properties on the custom element.
// Therefore we need to use importNode instead, which doesn't have this caveat.
// Additionally, Webkit browsers need importNode for video elements for autoplay
// to work correctly.
context.state.template.needs_import_node ||= node.name === 'video' || is_custom_element;
context.state.template.push(`<${node.name}`);
context.state.template.contains_script_tag ||= node.name === 'script';
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = [];
@ -110,7 +91,7 @@ export function RegularElement(node, context) {
const { value } = build_attribute_value(attribute.value, context);
if (value.type === 'Literal' && typeof value.value === 'string') {
context.state.template.push(` is="${escape_html(value.value, true)}"`);
context.state.template.set_prop('is', value.value);
continue;
}
}
@ -290,12 +271,9 @@ export function RegularElement(node, context) {
}
if (name !== 'class' || value) {
context.state.template.push(
` ${attribute.name}${
is_boolean_attribute(name) && value === true
? ''
: `="${value === true ? '' : escape_html(value, true)}"`
}`
context.state.template.set_prop(
attribute.name,
is_boolean_attribute(name) && value === true ? undefined : value === true ? '' : value
);
}
} else if (name === 'autofocus') {
@ -335,8 +313,6 @@ export function RegularElement(node, context) {
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
}
context.state.template.push('>');
const metadata = {
...context.state.metadata,
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
@ -358,7 +334,6 @@ export function RegularElement(node, context) {
const state = {
...context.state,
metadata,
locations: [],
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
@ -456,14 +431,7 @@ export function RegularElement(node, context) {
context.state.update.push(b.stmt(b.assignment('=', dir, dir)));
}
if (state.locations.length > 0) {
// @ts-expect-error
location.push(state.locations);
}
if (!is_void(node.name)) {
context.state.template.push(`</${node.name}>`);
}
context.state.template.pop_element();
}
/**

@ -11,7 +11,7 @@ import { get_expression_id } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function RenderTag(node, context) {
context.state.template.push('<!>');
context.state.template.push_comment();
const expression = unwrap_optional(node.expression);

@ -11,7 +11,7 @@ import { memoize_expression } from './shared/utils.js';
*/
export function SlotElement(node, context) {
// <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback);
context.state.template.push('<!>');
context.state.template.push_comment();
/** @type {Property[]} */
const props = [];

@ -91,7 +91,7 @@ export function SvelteBoundary(node, context) {
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
);
context.state.template.push('<!>');
context.state.template.push_comment();
context.state.init.push(
external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary
);

@ -13,7 +13,7 @@ import { build_render_statement } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function SvelteElement(node, context) {
context.state.template.push(`<!>`);
context.state.template.push_comment();
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = [];

@ -469,11 +469,17 @@ export function build_component(node, component_name, context, anchor = context.
}
if (Object.keys(custom_css_props).length > 0) {
context.state.template.push(
context.state.metadata.namespace === 'svg'
? '<g><!></g>'
: '<svelte-css-wrapper style="display: contents"><!></svelte-css-wrapper>'
);
if (context.state.metadata.namespace === 'svg') {
// this boils down to <g><!></g>
context.state.template.push_element('g', node.start);
} else {
// this boils down to <svelte-css-wrapper style='display: contents'><!></svelte-css-wrapper>
context.state.template.push_element('svelte-css-wrapper', node.start);
context.state.template.set_prop('style', 'display: contents');
}
context.state.template.push_comment();
context.state.template.pop_element();
statements.push(
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))),
@ -481,7 +487,7 @@ export function build_component(node, component_name, context, anchor = context.
b.stmt(b.call('$.reset', anchor))
);
} else {
context.state.template.push('<!>');
context.state.template.push_comment();
statements.push(b.stmt(fn(anchor)));
}

@ -64,11 +64,11 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function flush_sequence(sequence) {
if (sequence.every((node) => node.type === 'Text')) {
skipped += 1;
state.template.push(sequence.map((node) => node.raw).join(''));
state.template.push_text(sequence);
return;
}
state.template.push(' ');
state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]);
const { has_state, value } = build_template_chunk(sequence, visit, state);

@ -24,6 +24,7 @@ import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@ -50,6 +51,7 @@ const global_visitors = {
ExpressionStatement,
Identifier,
LabeledStatement,
MemberExpression,
PropertyDefinition,
UpdateExpression,
VariableDeclaration

@ -44,6 +44,11 @@ function build_assignment(operator, left, right, context) {
/** @type {Expression} */ (context.visit(right))
);
}
} else if (field && (field.type === '$derived' || field.type === '$derived.by')) {
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
return b.call(b.member(b.this, name), value);
}
}

@ -35,7 +35,7 @@ export function CallExpression(node, context) {
if (rune === '$derived' || rune === '$derived.by') {
const fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
return b.call('$.once', rune === '$derived' ? b.thunk(fn) : fn);
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}
if (rune === '$state.snapshot') {

@ -36,7 +36,8 @@ export function ClassBody(node, context) {
body.push(
b.prop_def(field.key, null),
b.method('get', b.key(name), [], [b.return(b.call(member))])
b.method('get', b.key(name), [], [b.return(b.call(member))]),
b.method('set', b.key(name), [b.id('$$value')], [b.return(b.call(member, b.id('$$value')))])
);
}
}
@ -61,6 +62,7 @@ export function ClassBody(node, context) {
if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
} else if (field.node === definition) {
// $derived / $derived.by
const member = b.member(b.this, field.key);
body.push(
@ -69,7 +71,8 @@ export function ClassBody(node, context) {
/** @type {CallExpression} */ (context.visit(field.value, child_state))
),
b.method('get', definition.key, [], [b.return(b.call(member))])
b.method('get', definition.key, [], [b.return(b.call(member))]),
b.method('set', b.key(name), [b.id('$$value')], [b.return(b.call(member, b.id('$$value')))])
);
}
}

@ -0,0 +1,23 @@
/** @import { ClassBody, MemberExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '#compiler/builders';
/**
* @param {MemberExpression} node
* @param {Context} context
*/
export function MemberExpression(node, context) {
if (
context.state.analysis.runes &&
node.object.type === 'ThisExpression' &&
node.property.type === 'PrivateIdentifier'
) {
const field = context.state.state_fields?.get(`#${node.property.name}`);
if (field?.type === '$derived' || field?.type === '$derived.by') {
return b.call(node);
}
}
context.next();
}

@ -11,7 +11,7 @@ export function PropertyDefinition(node, context) {
if (context.state.analysis.runes && node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, context.state.scope);
if (rune === '$state' || rune === '$state.raw' || rune === '$derived') {
if (rune === '$state' || rune === '$state.raw') {
return {
...node,
value:
@ -21,13 +21,14 @@ export function PropertyDefinition(node, context) {
};
}
if (rune === '$derived.by') {
if (rune === '$derived.by' || rune === '$derived') {
const fn = /** @type {Expression} */ (context.visit(node.value.arguments[0]));
return {
...node,
value:
node.value.arguments.length === 0
? null
: b.call(/** @type {Expression} */ (context.visit(node.value.arguments[0])))
: b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn)
};
}
}

@ -24,9 +24,11 @@ export function RegularElement(node, context) {
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
};
const node_is_void = is_void(node.name);
context.state.template.push(b.literal(`<${node.name}`));
const body = build_element_attributes(node, { ...context, state });
context.state.template.push(b.literal('>'));
context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
context.state.template.push(
@ -94,7 +96,7 @@ export function RegularElement(node, context) {
);
}
if (!is_void(node.name)) {
if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
}

@ -271,19 +271,12 @@ export function clean_nodes(
var first = trimmed[0];
// initial newline inside a `<pre>` is disregarded, if not followed by another newline
// if first text node inside a <pre> is a single newline, discard it, because otherwise
// the browser will do it for us which could break hydration
if (parent.type === 'RegularElement' && parent.name === 'pre' && first?.type === 'Text') {
const text = first.data.replace(regex_starts_with_newline, '');
if (text !== first.data) {
const tmp = text.replace(regex_starts_with_newline, '');
if (text === tmp) {
first.data = text;
first.raw = first.raw.replace(regex_starts_with_newline, '');
if (first.data === '') {
trimmed.shift();
first = trimmed[0];
}
}
if (first.data === '\n' || first.data === '\r\n') {
trimmed.shift();
first = trimmed[0];
}
}
@ -331,7 +324,7 @@ export function clean_nodes(
}
/**
* Infers the namespace for the children of a node that should be used when creating the `$.template(...)`.
* Infers the namespace for the children of a node that should be used when creating the fragment
* @param {Namespace} namespace
* @param {AST.SvelteNode} parent
* @param {AST.SvelteNode[]} nodes

@ -122,6 +122,16 @@ export interface CompileOptions extends ModuleCompileOptions {
* @default false
*/
preserveWhitespace?: boolean;
/**
* Which strategy to use when cloning DOM fragments:
*
* - `html` populates a `<template>` with `innerHTML` and clones it. This is faster, but cannot be used if your app's [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) includes [`require-trusted-types-for 'script'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for)
* - `tree` creates the fragment one element at a time and _then_ clones it. This is slower, but works everywhere
*
* @default 'html'
* @since 5.33
*/
fragments?: 'html' | 'tree';
/**
* Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage.
* Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.

@ -114,6 +114,8 @@ export const validate_component_options =
preserveComments: boolean(false),
fragments: list(['html', 'tree']),
preserveWhitespace: boolean(false),
runes: boolean(undefined),

@ -96,6 +96,7 @@ export const codes = [
'options_removed_hydratable',
'options_removed_loop_guard_timeout',
'options_renamed_ssr_dom',
'custom_element_props_identifier',
'export_let_unused',
'legacy_component_creation',
'non_reactive_update',
@ -592,6 +593,14 @@ export function options_renamed_ssr_dom(node) {
w(node, 'options_renamed_ssr_dom', `\`generate: "dom"\` and \`generate: "ssr"\` options have been renamed to "client" and "server" respectively\nhttps://svelte.dev/e/options_renamed_ssr_dom`);
}
/**
* Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option.
* @param {null | NodeLike} node
*/
export function custom_element_props_identifier(node) {
w(node, 'custom_element_props_identifier', `Using a rest element or a non-destructured declaration with \`$props()\` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the \`customElement.props\` option.\nhttps://svelte.dev/e/custom_element_props_identifier`);
}
/**
* Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`
* @param {null | NodeLike} node

@ -17,6 +17,8 @@ export const TRANSITION_GLOBAL = 1 << 2;
export const TEMPLATE_FRAGMENT = 1;
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
export const TEMPLATE_USE_SVG = 1 << 2;
export const TEMPLATE_USE_MATHML = 1 << 3;
export const HYDRATION_START = '[';
/** used to indicate that an `{:else}...` block was rendered */

@ -1,4 +1,4 @@
/** @import { SourceLocation } from '#shared' */
/** @import { SourceLocation } from '#client' */
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';

@ -2,6 +2,8 @@ import { effect } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import { untrack } from '../../../runtime.js';
import { is } from '../../../proxy.js';
import { is_array } from '../../../../shared/utils.js';
import * as w from '../../../warnings.js';
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
@ -12,6 +14,17 @@ import { is } from '../../../proxy.js';
*/
export function select_option(select, value, mounting) {
if (select.multiple) {
// If value is null or undefined, keep the selection as is
if (value == undefined) {
return;
}
// If not an array, warn and keep the selection as is
if (!is_array(value)) {
return w.select_multiple_invalid_value();
}
// Otherwise, update the selection
return select_options(select, value);
}
@ -124,14 +137,12 @@ export function bind_select_value(select, get, set = get) {
}
/**
* @template V
* @param {HTMLSelectElement} select
* @param {V} value
* @param {unknown[]} value
*/
function select_options(select, value) {
for (var option of select.options) {
// @ts-ignore
option.selected = ~value.indexOf(get_option_value(option));
option.selected = value.includes(get_option_value(option));
}
}

@ -112,8 +112,15 @@ export function event(event_name, dom, handler, capture, passive) {
var options = { capture, passive };
var target_handler = create_event(event_name, dom, handler, options);
// @ts-ignore
if (dom === document.body || dom === window || dom === document) {
if (
dom === document.body ||
// @ts-ignore
dom === window ||
// @ts-ignore
dom === document ||
// Firefox has quirky behavior, it can happen that we still get "canplay" events when the element is already removed
dom instanceof HTMLMediaElement
) {
teardown(() => {
dom.removeEventListener(event_name, target_handler, options);
});

@ -217,3 +217,44 @@ export function should_defer_append() {
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;
}
/**
*
* @param {string} tag
* @param {string} [namespace]
* @param {string} [is]
* @returns
*/
export function create_element(tag, namespace, is) {
let options = is ? { is } : undefined;
if (namespace) {
return document.createElementNS(namespace, tag, options);
}
return document.createElement(tag, options);
}
export function create_fragment() {
return document.createDocumentFragment();
}
/**
* @param {string} data
* @returns
*/
export function create_comment(data = '') {
return document.createComment(data);
}
/**
* @param {Element} element
* @param {string} key
* @param {string} value
* @returns
*/
export function set_attribute(element, key, value = '') {
if (key.startsWith('xlink:')) {
element.setAttributeNS('http://www.w3.org/1999/xlink', key, value);
return;
}
return element.setAttribute(key, value);
}

@ -1,6 +1,6 @@
/** @param {string} html */
export function create_fragment_from_html(html) {
var elem = document.createElement('template');
elem.innerHTML = html;
elem.innerHTML = html.replaceAll('<!>', '<!---->'); // XHTML compliance
return elem.content;
}

@ -1,9 +1,25 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { TemplateStructure } from './types' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { create_text, get_first_child, is_firefox } from './operations.js';
import {
create_text,
get_first_child,
is_firefox,
create_element,
create_fragment,
create_comment,
set_attribute
} from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import {
NAMESPACE_MATHML,
NAMESPACE_SVG,
TEMPLATE_FRAGMENT,
TEMPLATE_USE_IMPORT_NODE,
TEMPLATE_USE_MATHML,
TEMPLATE_USE_SVG
} from '../../../constants.js';
/**
* @param {TemplateNode} start
@ -23,7 +39,7 @@ export function assign_nodes(start, end) {
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function template(content, flags) {
export function from_html(content, flags) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
@ -64,17 +80,6 @@ export function template(content, flags) {
};
}
/**
* @param {string} content
* @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function template_with_script(content, flags) {
var fn = template(content, flags);
return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
}
/**
* @param {string} content
* @param {number} flags
@ -82,7 +87,7 @@ export function template_with_script(content, flags) {
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function ns_template(content, flags, ns = 'svg') {
function from_namespace(content, flags, ns = 'svg') {
/**
* Whether or not the first item is a text/element node. If not, we need to
* create an additional comment node to act as `effect.nodes.start`
@ -133,22 +138,120 @@ export function ns_template(content, flags, ns = 'svg') {
/**
* @param {string} content
* @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function svg_template_with_script(content, flags) {
var fn = ns_template(content, flags);
return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
export function from_svg(content, flags) {
return from_namespace(content, flags, 'svg');
}
/**
* @param {string} content
* @param {number} flags
*/
/*#__NO_SIDE_EFFECTS__*/
export function from_mathml(content, flags) {
return from_namespace(content, flags, 'math');
}
/**
* @param {TemplateStructure[]} structure
* @param {NAMESPACE_SVG | NAMESPACE_MATHML | undefined} [ns]
*/
function fragment_from_tree(structure, ns) {
var fragment = create_fragment();
for (var item of structure) {
if (typeof item === 'string') {
fragment.append(create_text(item));
continue;
}
// if `preserveComments === true`, comments are represented as `['// <data>']`
if (item === undefined || item[0][0] === '/') {
fragment.append(create_comment(item ? item[0].slice(3) : ''));
continue;
}
const [name, attributes, ...children] = item;
const namespace = name === 'svg' ? NAMESPACE_SVG : name === 'math' ? NAMESPACE_MATHML : ns;
var element = create_element(name, namespace, attributes?.is);
for (var key in attributes) {
set_attribute(element, key, attributes[key]);
}
if (children.length > 0) {
var target =
element.tagName === 'TEMPLATE'
? /** @type {HTMLTemplateElement} */ (element).content
: element;
target.append(
fragment_from_tree(children, element.tagName === 'foreignObject' ? undefined : namespace)
);
}
fragment.append(element);
}
return fragment;
}
/**
* @param {TemplateStructure[]} structure
* @param {number} flags
* @returns {() => Node | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function mathml_template(content, flags) {
return ns_template(content, flags, 'math');
export function from_tree(structure, flags) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
/** @type {Node} */
var node;
return () => {
if (hydrating) {
assign_nodes(hydrate_node, null);
return hydrate_node;
}
if (node === undefined) {
const ns =
(flags & TEMPLATE_USE_SVG) !== 0
? NAMESPACE_SVG
: (flags & TEMPLATE_USE_MATHML) !== 0
? NAMESPACE_MATHML
: undefined;
node = fragment_from_tree(structure, ns);
if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
}
var clone = /** @type {TemplateNode} */ (
use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
);
if (is_fragment) {
var start = /** @type {TemplateNode} */ (get_first_child(clone));
var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);
} else {
assign_nodes(clone, clone);
}
return clone;
};
}
/**
* @param {() => Element | DocumentFragment} fn
*/
export function with_script(fn) {
return () => run_scripts(fn());
}
/**

@ -0,0 +1,4 @@
export type TemplateStructure =
| string
| undefined
| [string, Record<string, string> | undefined, ...TemplateStructure[]];

@ -89,13 +89,13 @@ export {
export {
append,
comment,
ns_template,
svg_template_with_script,
mathml_template,
template,
template_with_script,
from_html,
from_mathml,
from_svg,
from_tree,
text,
props_id
props_id,
with_script
} from './dom/template.js';
export {
async_derived,

@ -21,7 +21,8 @@ import {
increment_write_version,
set_active_effect,
handle_error,
push_reaction_value
push_reaction_value,
is_destroying_effect
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
@ -326,13 +327,18 @@ export function execute_derived(derived) {
*/
export function update_derived(derived) {
var value = execute_derived(derived);
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
if (!derived.equals(value)) {
derived.v = value;
derived.wv = increment_write_version();
}
// don't mark derived clean if we're reading it inside a
// cleanup function, or it will cache a stale value
if (is_destroying_effect) return;
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
}

@ -183,4 +183,8 @@ export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: T;
};
export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export * from './reactivity/types';

@ -181,6 +181,17 @@ export function ownership_invalid_mutation(name, location, prop, parent) {
}
}
/**
* The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.
*/
export function select_multiple_invalid_value() {
if (DEV) {
console.warn(`%c[svelte] select_multiple_invalid_value\n%cThe \`value\` property of a \`<select multiple>\` element should be an array, but it received a non-array value. The selection will be kept as is.\nhttps://svelte.dev/e/select_multiple_invalid_value`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/select_multiple_invalid_value`);
}
}
/**
* Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results
* @param {string} operator

@ -516,3 +516,24 @@ export {
export { escape_html as escape };
export { await_outside_boundary } from '../shared/errors.js';
/**
* @template T
* @param {()=>T} fn
* @returns {(new_value?: T) => (T | void)}
*/
export function derived(fn) {
const get_value = once(fn);
/**
* @type {T | undefined}
*/
let updated_value;
return function (new_value) {
if (arguments.length === 0) {
return updated_value ?? get_value();
}
updated_value = new_value;
return updated_value;
};
}

@ -3,10 +3,6 @@ export type Store<V> = {
set(value: V): void;
};
export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export type Getters<T> = {
[K in keyof T]: () => T[K];
};

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.32.0';
export const VERSION = '5.33.4';
export const PUBLIC_VERSION = '5';

@ -8,7 +8,7 @@ import * as svelteElements from './elements.js';
/**
* @internal do not use
*/
type HTMLProps<Property extends string, Override> = Omit<
type HTMLProps<Property extends keyof svelteElements.SvelteHTMLElements, Override> = Omit<
import('./elements.js').SvelteHTMLElements[Property],
keyof Override
> &
@ -250,7 +250,7 @@ declare global {
};
// don't type svelte:options, it would override the types in svelte/elements and it isn't extendable anyway
[name: string]: { [name: string]: any };
[name: string & {}]: { [name: string]: any };
}
}
}

@ -0,0 +1,76 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".\\61 sdf"',
start: {
line: 22,
column: 1,
character: 465
},
end: {
line: 22,
column: 10,
character: 474
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".\\61\n\tsdf"',
start: {
line: 23,
column: 1,
character: 492
},
end: {
line: 24,
column: 4,
character: 501
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".\\61\n sdf"',
start: {
line: 25,
column: 1,
character: 519
},
end: {
line: 26,
column: 4,
character: 528
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "#\\31span"',
start: {
line: 28,
column: 1,
character: 547
},
end: {
line: 28,
column: 9,
character: 555
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "#\\31 span"',
start: {
line: 29,
column: 1,
character: 573
},
end: {
line: 29,
column: 10,
character: 582
}
}
]
});

@ -0,0 +1,21 @@
#\31\32\33 .svelte-xyz{ color: green; }
#\31 23.svelte-xyz { color: green; }
#line\a break.svelte-xyz { color: green; }
#line\a
break.svelte-xyz { color: green; }
#line\00000abreak.svelte-xyz { color: green; }
#line\00000a break.svelte-xyz { color: green; }
#line\00000a break.svelte-xyz { color: green; }
.a\1f642 b.svelte-xyz { color: green; }
.\61sdf.svelte-xyz { color: green; }
/* (unused) .\61 sdf { color: red; }*/
/* (unused) .\61
sdf { color: red; }*/
/* (unused) .\61
sdf { color: red; }*/
/* (unused) #\31span { color: red; }*/
/* (unused) #\31 span { color: red; }*/
#\31 .svelte-xyz span:where(.svelte-xyz) { color: green; }

@ -0,0 +1,7 @@
<div id="123" class="svelte-xyz"></div>
<div class="svelte-xyz" id="line
break"></div>
<div class="a🙂b svelte-xyz"></div>
<div class="asdf svelte-xyz"></div>
<div class="&#97;sdf svelte-xyz"></div>
<div id="1" class="svelte-xyz"><span class="svelte-xyz"></span></div>

@ -0,0 +1,31 @@
<div id="123"></div>
<div id="line
break"></div>
<div class="a🙂b"></div>
<div class="asdf"></div>
<div class="&#97;sdf"></div>
<div id="1"><span></span></div>
<style>
#\31\32\33 { color: green; }
#\31 23 { color: green; }
#line\a break { color: green; }
#line\a
break { color: green; }
#line\00000abreak { color: green; }
#line\00000a break { color: green; }
#line\00000a break { color: green; }
.a\1f642 b { color: green; }
.\61sdf { color: green; }
.\61 sdf { color: red; }
.\61
sdf { color: red; }
.\61
sdf { color: red; }
#\31span { color: red; }
#\31 span { color: red; }
#\31 span { color: green; }
</style>

@ -191,3 +191,5 @@ if (typeof window !== 'undefined') {
};
});
}
export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html';

@ -4,6 +4,7 @@ import * as fs from 'node:fs';
import { assert } from 'vitest';
import { compile_directory } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js';
import { fragments } from '../helpers.js';
import { assert_ok, suite, type BaseTest } from '../suite.js';
import { createClassComponent } from 'svelte/legacy';
import { render } from 'svelte/server';
@ -43,7 +44,12 @@ function read(path: string): string | void {
const { test, run } = suite<HydrationTest>(async (config, cwd) => {
if (!config.load_compiled) {
await compile_directory(cwd, 'client', { accessors: true, ...config.compileOptions });
await compile_directory(cwd, 'client', {
accessors: true,
fragments,
...config.compileOptions
});
await compile_directory(cwd, 'server', config.compileOptions);
}
@ -125,7 +131,8 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
flushSync();
const normalize = (string: string) => string.trim().replace(/\r\n/g, '\n');
const normalize = (string: string) =>
string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>');
const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
assert.equal(normalize(target.innerHTML), normalize(expected));

@ -0,0 +1,19 @@
import { test } from '../../assert';
const tick = () => Promise.resolve();
export default test({
async test({ assert, target }) {
target.innerHTML = '<custom-element name="world"></custom-element>';
await tick();
/** @type {any} */
const el = target.querySelector('custom-element');
assert.htmlEqual(
el.shadowRoot.innerHTML,
`
<p>name: world</p>
`
);
assert.equal(el.test, `test`);
}
});

@ -0,0 +1,14 @@
<svelte:options customElement={{
tag: "custom-element",
extend: (customClass)=>{
return class extends customClass{
public test: string = "test";
}
},
}}/>
<script lang="ts">
let { name } = $props();
</script>
<p>name: {name}</p>

@ -5,7 +5,7 @@ import * as path from 'node:path';
import { compile } from 'svelte/compiler';
import { afterAll, assert, beforeAll, describe } from 'vitest';
import { suite, suite_with_variants } from '../suite';
import { write } from '../helpers';
import { write, fragments } from '../helpers';
import type { Warning } from '#compiler';
const assert_file = path.resolve(__dirname, 'assert.js');
@ -87,6 +87,7 @@ async function run_test(
build.onLoad({ filter: /\.svelte$/ }, (args) => {
const compiled = compile(fs.readFileSync(args.path, 'utf-8').replace(/\r/g, ''), {
generate: 'client',
fragments,
...config.compileOptions,
immutable: config.immutable,
customElement: test_dir.includes('custom-elements-samples'),

@ -6,7 +6,7 @@ import { proxy } from 'svelte/internal/client';
import { flushSync, hydrate, mount, unmount } from 'svelte';
import { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest';
import { compile_directory } from '../helpers.js';
import { compile_directory, fragments } from '../helpers.js';
import { setup_html_equal } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
@ -175,6 +175,7 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run
experimental: {
async: true
},
fragments,
...config.compileOptions,
immutable: config.immutable,
accessors: 'accessors' in config ? config.accessors : true,

@ -5,6 +5,7 @@ export default test({
html: `
<button>0</button>
<p>doubled: 0</p>
<p>tripled: 0</p>
`,
test({ assert, target }) {
@ -17,6 +18,7 @@ export default test({
`
<button>1</button>
<p>doubled: 2</p>
<p>tripled: 3</p>
`
);
@ -27,6 +29,7 @@ export default test({
`
<button>2</button>
<p>doubled: 4</p>
<p>tripled: 6</p>
`
);
}

@ -2,14 +2,24 @@
class Counter {
count = $state(0);
#doubled = $derived(this.count * 2);
#tripled = $derived.by(() => this.count * this.by);
get embiggened() {
constructor(by) {
this.by = by;
}
get embiggened1() {
return this.#doubled;
}
get embiggened2() {
return this.#tripled;
}
}
const counter = new Counter();
const counter = new Counter(3);
</script>
<button on:click={() => counter.count++}>{counter.count}</button>
<p>doubled: {counter.embiggened}</p>
<p>doubled: {counter.embiggened1}</p>
<p>tripled: {counter.embiggened2}</p>

@ -1,18 +1,20 @@
<script module>
customElements.define('value-element', class extends HTMLElement {
if(!customElements.get('value-element')) {
customElements.define('value-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
set value(v) {
if (this.__value !== v) {
this.__value = v;
this.shadowRoot.innerHTML = `<span>${v}</span>`;
}
}
});
set value(v) {
if (this.__value !== v) {
this.__value = v;
this.shadowRoot.innerHTML = `<span>${v}</span>`;
}
}
});
}
</script>
<my-element string="test" object={{ test: true }}></my-element>

@ -0,0 +1,22 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
html: `<button>toggle (false)</button>`,
async test({ assert, target, logs }) {
assert.deepEqual(logs, ['up', { foo: false, bar: false }]);
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.deepEqual(logs, [
'up',
{ foo: false, bar: false },
'down',
{ foo: false, bar: false },
'up',
{ foo: true, bar: true }
]);
}
});

@ -0,0 +1,14 @@
<script>
let foo = $state(false)
let bar = $derived(foo)
$effect(() => {
console.log('up', { foo, bar });
return () =>{
console.log('down', { foo, bar });
};
});
</script>
<button onclick={() => foo = !foo}>toggle ({foo})</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { expect, vi } from 'vitest';
const handler = vi.fn();
export default test({
props: {
handler
},
async test({ target }) {
const button = target.querySelector('button');
const video = target.querySelector('video');
button?.click();
flushSync();
video?.dispatchEvent(new Event('someevent'));
expect(handler).not.toHaveBeenCalled();
}
});

@ -0,0 +1,9 @@
<script>
const { handler } = $props();
let show = $state(true);
</script>
<button onclick={() => show = false}>show/hide</button>
{#if show}
<video onsomeevent={handler}></video>
{/if}

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
compileOptions: {
fragments: 'tree'
},
html: `<p>hello</p>`
});

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
warnings: [
'The `value` property of a `<select multiple>` element should be an array, but it received a non-array value. The selection will be kept as is.'
]
});

@ -0,0 +1,9 @@
<select multiple value={null}>
<option>option</option>
</select>
<select multiple value={undefined}>
<option>option</option>
</select>
<select multiple value={123}>
<option>option</option>
</select>

@ -0,0 +1,7 @@
<h2>hello from component</h2>
<style>
h2 {
color: var(--color);
}
</style>

@ -0,0 +1,42 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
html: `
<h1>hello</h1>
<svelte-css-wrapper style="display: contents; --color: red;">
<h2 class="svelte-13kae5a">hello from component</h2>
</svelte-css-wrapper>
<p>goodbye</p>
`,
async test({ target, assert }) {
const h1 = target.querySelector('h1');
const h2 = target.querySelector('h2');
const p = target.querySelector('p');
// @ts-expect-error
assert.deepEqual(h1.__svelte_meta.loc, {
file: 'main.svelte',
line: 5,
column: 0
});
// @ts-expect-error
assert.deepEqual(h2.__svelte_meta.loc, {
file: 'Component.svelte',
line: 1,
column: 0
});
// @ts-expect-error
assert.deepEqual(p.__svelte_meta.loc, {
file: 'main.svelte',
line: 7,
column: 0
});
}
});

@ -0,0 +1,7 @@
<script>
import Component from './Component.svelte';
</script>
<h1>hello</h1>
<Component --color="red" />
<p>goodbye</p>

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

@ -0,0 +1,29 @@
<script>
class X {
x = $state(1);
on_class = $derived(this.x * 2);
#on_class_private = $derived(this.x * 2);
#in_constructor_private
constructor() {
this.#in_constructor_private = $derived(this.x * 2);
this.in_constructor = $derived(this.x * 2);
this.#on_class_private = 3;
this.#in_constructor_private = 3;
}
get on_class_private() {
return this.#on_class_private;
}
get in_constructor_private() {
return this.#in_constructor_private;
}
}
const x = new X();
x.on_class = 3;
x.in_constructor = 3;
</script>
{x.on_class} {x.in_constructor} {x.on_class_private} {x.in_constructor_private}

@ -5,7 +5,7 @@ function increment(_, counter) {
counter.count += 1;
}
var root = $.template(`<button> </button> <!> `, 1);
var root = $.from_html(`<button> </button> <!> `, 1);
export default function Await_block_scope($$anchor) {
let counter = $.proxy({ count: 0 });

@ -10,7 +10,7 @@ const snippet = ($$anchor) => {
$.append($$anchor, text);
};
var root = $.template(`<!> `, 1);
var root = $.from_html(`<!> `, 1);
export default function Bind_component_snippet($$anchor) {
let value = $.state('');

@ -1,7 +1,7 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var root = $.template(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, 3);
var root = $.from_html(`<div></div> <svg></svg> <custom-element></custom-element> <div></div> <svg></svg> <custom-element></custom-element>`, 3);
export default function Main($$anchor) {
// needs to be a snapshot test because jsdom does auto-correct the attribute casing

@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root_1 = $.template(`<p></p>`);
var root_1 = $.from_html(`<p></p>`);
export default function Each_index_non_null($$anchor) {
var fragment = $.comment();

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
compileOptions: {
fragments: 'tree'
}
});

@ -0,0 +1,25 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root = $.from_tree(
[
['h1', null, 'hello'],
' ',
[
'div',
{ class: 'potato' },
['p', null, 'child element'],
' ',
['p', null, 'another child element']
]
],
1
);
export default function Functional_templating($$anchor) {
var fragment = root();
$.next(2);
$.append($$anchor, fragment);
}

@ -0,0 +1,5 @@
import * as $ from 'svelte/internal/server';
export default function Functional_templating($$payload) {
$$payload.out += `<h1>hello</h1> <div class="potato"><p>child element</p> <p>another child element</p></div>`;
}

@ -0,0 +1,6 @@
<h1>hello</h1>
<div class="potato">
<p>child element</p>
<p>another child element</p>
</div>

@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root = $.template(`<h1>hello world</h1>`);
var root = $.from_html(`<h1>hello world</h1>`);
export default function Hello_world($$anchor) {
var h1 = root();

@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root = $.template(`<h1>hello world</h1>`);
var root = $.from_html(`<h1>hello world</h1>`);
function Hmr($$anchor) {
var h1 = root();

@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var on_click = (_, count) => $.update(count);
var root = $.template(`<h1></h1> <b></b> <button> </button> <h1></h1>`, 1);
var root = $.from_html(`<h1></h1> <b></b> <button> </button> <h1></h1>`, 1);
export default function Nullish_coallescence_omittance($$anchor) {
let name = 'world';

@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root = $.template(`<p></p> <p></p> <!>`, 1);
var root = $.from_html(`<p></p> <p></p> <!>`, 1);
export default function Purity($$anchor) {
var fragment = root();

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

Loading…
Cancel
Save