Merge branch 'main' into clarify-attachment-reruns

pull/15927/head
Rich Harris 4 months ago
commit b0912ad55c

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid auto-parenthesis for special-keywords-only `MediaQuery`

@ -67,16 +67,15 @@ todos[0].done = !todos[0].done;
### Classes
You can also use `$state` in class fields (whether public or private):
You can also 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
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
this.text = $state(text);
}
reset() {
@ -110,10 +109,9 @@ You can either use an inline function...
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
this.text = $state(text);
}
+++reset = () => {+++

@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand.
<Widget foo={bar} answer={42} text="hello" />
```
## Spread attributes
_Spread attributes_ allow many attributes or properties to be passed to an element or component at once.
An element or component can have multiple spread attributes, interspersed with regular ones.
An element or component can have multiple spread attributes, interspersed with regular ones. Order matters — if `things.a` exists it will take precedence over `a="b"`, while `c="d"` would take precedence over `things.c`:
```svelte
<Widget {...things} />
<Widget a="b" {...things} c="d" />
```
## Events

@ -43,7 +43,9 @@ An each block can also specify an _index_, equivalent to the second argument in
{#each expression as name, index (key)}...{/each}
```
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle.
The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
```svelte
{#each items as item (item.id)}

@ -125,7 +125,7 @@ In many cases this is perfectly fine, but there is a risk: if you mutate the sta
```svelte
<!--- file: App.svelte ---->
<script>
import { myGlobalState } from 'svelte';
import { myGlobalState } from './state.svelte.js';
let { data } = $props();

@ -846,6 +846,38 @@ Cannot reassign or bind to snippet parameter
This snippet is shadowing the prop `%prop%` with the same name
```
### state_field_duplicate
```
`%name%` has already been declared on this class
```
An assignment to a class field that uses a `$state` or `$derived` rune is considered a _state field declaration_. The declaration can happen in the class body...
```js
class Counter {
count = $state(0);
}
```
...or inside the constructor...
```js
class Counter {
constructor() {
this.count = $state(0);
}
}
```
...but it can only happen once.
### state_field_invalid_assignment
```
Cannot assign to a state field before its declaration
```
### state_invalid_export
```
@ -855,7 +887,7 @@ Cannot export state from a module if it is reassigned. Either export a function
### state_invalid_placement
```
`%rune%(...)` can only be used as a variable declaration initializer or a class field
`%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
```
### store_invalid_scoped_subscription

@ -1,5 +1,23 @@
# svelte
## 5.31.0
### Minor Changes
- feat: allow state fields to be declared inside class constructors ([#15820](https://github.com/sveltejs/svelte/pull/15820))
### Patch Changes
- fix: Add missing `AttachTag` in `Tag` union type inside the `AST` namespace from `"svelte/compiler"` ([#15946](https://github.com/sveltejs/svelte/pull/15946))
## 5.30.2
### Patch Changes
- fix: falsy attachments types ([#15939](https://github.com/sveltejs/svelte/pull/15939))
- fix: handle more hydration mismatches ([#15851](https://github.com/sveltejs/svelte/pull/15851))
## 5.30.1
### Patch Changes

@ -863,8 +863,8 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
// allow any data- attribute
[key: `data-${string}`]: any;
// allow any attachment
[key: symbol]: Attachment<T>;
// allow any attachment and falsy values (by using false we prevent the usage of booleans values by themselves)
[key: symbol]: Attachment<T> | false | undefined | null;
}
export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {});

@ -212,13 +212,41 @@ It's possible to export a snippet from a `<script module>` block, but only if it
> Cannot reassign or bind to snippet parameter
## state_field_duplicate
> `%name%` has already been declared on this class
An assignment to a class field that uses a `$state` or `$derived` rune is considered a _state field declaration_. The declaration can happen in the class body...
```js
class Counter {
count = $state(0);
}
```
...or inside the constructor...
```js
class Counter {
constructor() {
this.count = $state(0);
}
}
```
...but it can only happen once.
## state_field_invalid_assignment
> Cannot assign to a state field before its declaration
## state_invalid_export
> Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
## state_invalid_placement
> `%rune%(...)` can only be used as a variable declaration initializer or a class field
> `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
## store_invalid_scoped_subscription

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.30.1",
"version": "5.31.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -136,7 +136,7 @@
],
"scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages && rollup -cw",
"dev": "node scripts/process-messages -w & rollup -cw",
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",

@ -1,14 +1,19 @@
// @ts-check
import process from 'node:process';
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
const DIR = '../../documentation/docs/98-reference/.generated';
const watch = process.argv.includes('-w');
function run() {
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
const messages = {};
const seen = new Set();
const DIR = '../../documentation/docs/98-reference/.generated';
fs.rmSync(DIR, { force: true, recursive: true });
fs.mkdirSync(DIR);
@ -54,6 +59,7 @@ for (const category of fs.readdirSync('messages')) {
}
sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
fs.writeFileSync(
`messages/${category}/${file}`,
sorted.map((x) => x._.trim()).join('\n\n') + '\n'
@ -65,7 +71,10 @@ for (const category of fs.readdirSync('messages')) {
'<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->\n\n' +
Object.entries(messages[category])
.map(([code, { messages, details }]) => {
const chunks = [`### ${code}`, ...messages.map((message) => '```\n' + message + '\n```')];
const chunks = [
`### ${code}`,
...messages.map((message) => '```\n' + message + '\n```')
];
if (details) {
chunks.push(details);
@ -407,3 +416,26 @@ transform('client-errors', 'src/internal/client/errors.js');
transform('server-errors', 'src/internal/server/errors.js');
transform('shared-errors', 'src/internal/shared/errors.js');
transform('shared-warnings', 'src/internal/shared/warnings.js');
}
if (watch) {
let running = false;
let timeout;
fs.watch('messages', { recursive: true }, (type, file) => {
if (running) {
timeout ??= setTimeout(() => {
running = false;
timeout = null;
});
} else {
running = true;
// eslint-disable-next-line no-console
console.log('Regenerating messages...');
run();
}
});
}
run();

@ -461,6 +461,25 @@ export function snippet_parameter_assignment(node) {
e(node, 'snippet_parameter_assignment', `Cannot reassign or bind to snippet parameter\nhttps://svelte.dev/e/snippet_parameter_assignment`);
}
/**
* `%name%` has already been declared on this class
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function state_field_duplicate(node, name) {
e(node, 'state_field_duplicate', `\`${name}\` has already been declared on this class\nhttps://svelte.dev/e/state_field_duplicate`);
}
/**
* Cannot assign to a state field before its declaration
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function state_field_invalid_assignment(node) {
e(node, 'state_field_invalid_assignment', `Cannot assign to a state field before its declaration\nhttps://svelte.dev/e/state_field_invalid_assignment`);
}
/**
* Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
* @param {null | number | NodeLike} node
@ -471,13 +490,13 @@ export function state_invalid_export(node) {
}
/**
* `%rune%(...)` can only be used as a variable declaration initializer or a class field
* `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
* @param {null | number | NodeLike} node
* @param {string} rune
* @returns {never}
*/
export function state_invalid_placement(node, rune) {
e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer or a class field\nhttps://svelte.dev/e/state_invalid_placement`);
e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.\nhttps://svelte.dev/e/state_invalid_placement`);
}
/**

@ -48,6 +48,7 @@ import { Literal } from './visitors/Literal.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
@ -164,6 +165,7 @@ const visitors = {
MemberExpression,
NewExpression,
OnDirective,
PropertyDefinition,
RegularElement,
RenderTag,
SlotElement,
@ -256,7 +258,8 @@ export function analyze_module(ast, options) {
accessors: false,
runes: true,
immutable: true,
tracing: false
tracing: false,
classes: new Map()
};
walk(
@ -265,7 +268,7 @@ export function analyze_module(ast, options) {
scope,
scopes,
analysis: /** @type {ComponentAnalysis} */ (analysis),
derived_state: [],
state_fields: new Map(),
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
@ -429,6 +432,7 @@ export function analyze_component(root, source, options) {
elements: [],
runes,
tracing: false,
classes: new Map(),
immutable: runes || options.immutable,
exports: [],
uses_props: false,
@ -624,7 +628,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false,
component_slots: new Set(),
expression: null,
derived_state: [],
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
};
@ -691,7 +695,7 @@ export function analyze_component(root, source, options) {
reactive_statement: null,
component_slots: new Set(),
expression: null,
derived_state: [],
state_fields: new Map(),
function_depth: scope.function_depth
};

@ -1,6 +1,6 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler';
export interface AnalysisState {
scope: Scope;
@ -18,7 +18,10 @@ export interface AnalysisState {
component_slots: Set<string>;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
derived_state: { name: string; private: boolean }[];
/** Used to analyze class state */
state_fields: Map<string, StateField>;
function_depth: number;
// legacy stuff

@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js';
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state);
validate_assignment(node, node.left, context);
if (context.state.reactive_statement) {
const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;

@ -211,7 +211,7 @@ function get_delegated_event(event_name, handler, context) {
if (
binding !== null &&
// Bail out if the the binding is a rest param
// Bail out if the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||

@ -158,7 +158,7 @@ export function BindDirective(node, context) {
return;
}
validate_assignment(node, node.expression, context.state);
validate_assignment(node, node.expression, context);
const assignee = node.expression;
const left = object(assignee);

@ -114,12 +114,13 @@ export function CallExpression(node, context) {
case '$state':
case '$state.raw':
case '$derived':
case '$derived.by':
if (
(parent.type !== 'VariableDeclarator' ||
get_parent(context.path, -3).type === 'ConstTag') &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
) {
case '$derived.by': {
const valid =
is_variable_declaration(parent, context) ||
is_class_property_definition(parent) ||
is_class_property_assignment_at_constructor_root(parent, context);
if (!valid) {
e.state_invalid_placement(node, rune);
}
@ -130,6 +131,7 @@ export function CallExpression(node, context) {
}
break;
}
case '$effect':
case '$effect.pre':
@ -270,3 +272,40 @@ function get_function_label(nodes) {
return parent.id.name;
}
}
/**
* @param {AST.SvelteNode} parent
* @param {Context} context
*/
function is_variable_declaration(parent, context) {
return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag';
}
/**
* @param {AST.SvelteNode} parent
*/
function is_class_property_definition(parent) {
return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
}
/**
* @param {AST.SvelteNode} node
* @param {Context} context
*/
function is_class_property_assignment_at_constructor_root(node, context) {
if (
node.type === 'AssignmentExpression' &&
node.operator === '=' &&
node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' &&
((node.left.property.type === 'Identifier' && !node.left.computed) ||
node.left.property.type === 'PrivateIdentifier' ||
node.left.property.type === 'Literal')
) {
// MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1)
const parent = get_parent(context.path, -5);
return parent?.type === 'MethodDefinition' && parent.kind === 'constructor';
}
return false;
}

@ -1,30 +1,107 @@
/** @import { ClassBody } from 'estree' */
/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { is_state_creation_rune } from '../../../../utils.js';
import { get_name } from '../../nodes.js';
import { regex_invalid_identifier_chars } from '../../patterns.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
/** @type {{name: string, private: boolean}[]} */
const derived_state = [];
if (!context.state.analysis.runes) {
context.next();
return;
}
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
for (const prop of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
definition.value?.type === 'CallExpression'
(prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
prop.key.type === 'PrivateIdentifier'
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
derived_state.push({
name: definition.key.name,
private: definition.key.type === 'PrivateIdentifier'
private_ids.push(prop.key.name);
}
}
/** @type {Map<string, StateField>} */
const state_fields = new Map();
context.state.analysis.classes.set(node, state_fields);
/** @type {MethodDefinition | null} */
let constructor = null;
/**
* @param {PropertyDefinition | AssignmentExpression} node
* @param {Expression | PrivateIdentifier} key
* @param {Expression | null | undefined} value
*/
function handle(node, key, value) {
const name = get_name(key);
if (name === null) return;
const rune = get_rune(value, context.state.scope);
if (rune && is_state_creation_rune(rune)) {
if (state_fields.has(name)) {
e.state_field_duplicate(node, name);
}
state_fields.set(name, {
node,
type: rune,
// @ts-expect-error for public state this is filled out in a moment
key: key.type === 'PrivateIdentifier' ? key : null,
value: /** @type {CallExpression} */ (value)
});
}
}
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value);
}
if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
constructor = child;
}
}
if (constructor) {
for (const statement of constructor.value.body.body) {
if (statement.type !== 'ExpressionStatement') continue;
if (statement.expression.type !== 'AssignmentExpression') continue;
const { left, right } = statement.expression;
if (left.type !== 'MemberExpression') continue;
if (left.object.type !== 'ThisExpression') continue;
if (left.computed && left.property.type !== 'Literal') continue;
handle(statement.expression, left.property, right);
}
}
for (const [name, field] of state_fields) {
if (name[0] === '#') {
continue;
}
let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.key = b.private_id(deconflicted);
}
context.next({ ...context.state, derived_state });
context.next({ ...context.state, state_fields });
}

@ -0,0 +1,21 @@
/** @import { PropertyDefinition } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { get_name } from '../../nodes.js';
/**
* @param {PropertyDefinition} node
* @param {Context} context
*/
export function PropertyDefinition(node, context) {
const name = get_name(node.key);
const field = name && context.state.state_fields.get(name);
if (field && node !== field.node && node.value) {
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
e.state_field_invalid_assignment(node);
}
}
context.next();
}

@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js';
* @param {Context} context
*/
export function UpdateExpression(node, context) {
validate_assignment(node, node.argument, context.state);
validate_assignment(node, node.argument, context);
if (context.state.reactive_statement) {
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;

@ -4,24 +4,25 @@
/** @import { Scope } from '../../../scope' */
/** @import { NodeLike } from '../../../../errors.js' */
import * as e from '../../../../errors.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import { extract_identifiers, get_parent } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**
* @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
* @param {Context} context
*/
export function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective');
export function validate_assignment(node, argument, context) {
validate_no_const_assignment(node, argument, context.state.scope, node.type === 'BindDirective');
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);
const binding = context.state.scope.get(argument.name);
if (state.analysis.runes) {
if (binding?.node === state.analysis.props_id) {
if (context.state.analysis.runes) {
if (binding?.node === context.state.analysis.props_id) {
e.constant_assignment(node, '$props.id()');
}
@ -34,6 +35,41 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
if (argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression') {
const name =
argument.computed && argument.property.type !== 'Literal'
? null
: get_name(argument.property);
const field = name !== null && context.state.state_fields?.get(name);
// check we're not assigning to a state field before its declaration in the constructor
if (field && field.node.type === 'AssignmentExpression' && node !== field.node) {
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression'
) {
const grandparent = get_parent(context.path, i - 1);
if (
grandparent.type === 'MethodDefinition' &&
grandparent.kind === 'constructor' &&
/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)
) {
e.state_field_invalid_assignment(node);
}
break;
}
}
}
}
}
/**

@ -163,8 +163,7 @@ export function client_component(analysis, options) {
},
events: new Set(),
preserve_whitespace: options.preserveWhitespace,
public_state: new Map(),
private_state: new Map(),
state_fields: new Map(),
transform: {},
in_constructor: false,
instance_level_snippets: [],
@ -671,8 +670,7 @@ export function client_module(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
public_state: new Map(),
private_state: new Map(),
state_fields: new Map(),
transform: {},
in_constructor: false
};

@ -9,15 +9,12 @@ import type {
UpdateExpression,
VariableDeclaration
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { AST, Namespace, StateField, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared';
export interface ClientTransformState extends TransformState {
readonly private_state: Map<string, StateField>;
readonly public_state: Map<string, StateField>;
/**
* `true` if the current lexical scope belongs to a class constructor. this allows
* us to rewrite `this.foo` as `this.#foo.value`
@ -94,11 +91,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly module_level_snippets: VariableDeclaration[];
}
export interface StateField {
kind: 'state' | 'raw_state' | 'derived' | 'derived_by';
id: PrivateIdentifier;
}
export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, any>;

@ -11,6 +11,8 @@ import { dev, locate_node } from '../../../../state.js';
import { should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**
* @param {AssignmentExpression} node
@ -50,27 +52,44 @@ const callees = {
* @returns {Expression | null}
*/
function build_assignment(operator, left, right, context) {
// Handle class private/public state assignment cases
if (
context.state.analysis.runes &&
left.type === 'MemberExpression' &&
left.property.type === 'PrivateIdentifier'
) {
const private_state = context.state.private_state.get(left.property.name);
if (context.state.analysis.runes && left.type === 'MemberExpression') {
const name = get_name(left.property);
const field = name && context.state.state_fields.get(name);
if (field) {
// special case — state declaration in class constructor
if (field.node.type === 'AssignmentExpression' && left === field.node.left) {
const rune = get_rune(right, context.state.scope);
if (rune) {
const child_state = {
...context.state,
in_constructor: rune !== '$derived' && rune !== '$derived.by'
};
return b.assignment(
operator,
b.member(b.this, field.key),
/** @type {Expression} */ (context.visit(right, child_state))
);
}
}
if (private_state !== undefined) {
// special case — assignment to private state field
if (left.property.type === 'PrivateIdentifier') {
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
const needs_proxy =
private_state.kind === 'state' &&
field.type === '$state' &&
is_non_coercive_operator(operator) &&
should_proxy(value, context.state.scope);
return b.call('$.set', left, value, needs_proxy && b.true);
}
}
}
let object = left;

@ -4,19 +4,52 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
switch (get_rune(node, context.state.scope)) {
const rune = get_rune(node, context.state.scope);
switch (rune) {
case '$host':
return b.id('$$props.$$host');
case '$effect.tracking':
return b.call('$.effect_tracking');
// transform state field assignments in constructors
case '$state':
case '$state.raw': {
let arg = node.arguments[0];
/** @type {Expression | undefined} */
let value = undefined;
if (arg) {
value = /** @type {Expression} */ (context.visit(node.arguments[0]));
if (
rune === '$state' &&
should_proxy(/** @type {Expression} */ (arg), context.state.scope)
) {
value = b.call('$.proxy', value);
}
}
return b.call('$.state', value);
}
case '$derived':
case '$derived.by': {
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
if (rune === '$derived') fn = b.thunk(fn);
return b.call('$.derived', fn);
}
case '$state.snapshot':
return b.call(
'$.snapshot',

@ -1,184 +1,96 @@
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
/** @import { Context, StateField } from '../types' */
/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { get_name } from '../../../nodes.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
if (!context.state.analysis.runes) {
const state_fields = context.state.analysis.classes.get(node);
if (!state_fields) {
// in legacy mode, do nothing
context.next();
return;
}
/** @type {Map<string, StateField>} */
const public_state = new Map();
/** @type {Array<MethodDefinition | PropertyDefinition | StaticBlock>} */
const body = [];
/** @type {Map<string, StateField>} */
const private_state = new Map();
const child_state = { ...context.state, state_fields };
/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
const definition_names = new Map();
for (const [name, field] of state_fields) {
if (name[0] === '#') {
continue;
}
/** @type {string[]} */
const private_ids = [];
// insert backing fields for stuff declared in the constructor
if (field.node.type === 'AssignmentExpression') {
const member = b.member(b.this, field.key);
for (const definition of node.body) {
if (
(definition.type === 'PropertyDefinition' || definition.type === 'MethodDefinition') &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const type = definition.key.type;
const name = get_name(definition.key, public_state);
if (!name) continue;
// we store the deconflicted name in the map so that we can access it later
definition_names.set(definition.key, name);
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, context.state.scope);
if (
rune === '$state' ||
rune === '$state.raw' ||
rune === '$derived' ||
rune === '$derived.by'
) {
/** @type {StateField} */
const field = {
kind:
rune === '$state'
? 'state'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived.by'
? 'derived_by'
: 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_state.set(name, field);
} else {
public_state.set(name, field);
}
}
}
}
}
const should_proxy = field.type === '$state' && true; // TODO
// each `foo = $state()` needs a backing `#foo` field
for (const [name, field] of public_state) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
const key = b.key(name);
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
body.push(
b.prop_def(field.key, null),
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const body = [];
b.method('get', key, [], [b.return(b.call('$.get', member))]),
const child_state = { ...context.state, public_state, private_state };
b.method(
'set',
key,
[b.id('value')],
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
)
);
}
}
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const name = definition_names.get(definition.key);
if (!name) continue;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_state : public_state).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
let value = null;
if (definition.value.arguments.length > 0) {
const init = /** @type {Expression} **/ (
context.visit(definition.value.arguments[0], child_state)
if (definition.type !== 'PropertyDefinition') {
body.push(
/** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
);
value =
field.kind === 'state'
? b.call(
'$.state',
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'raw_state'
? b.call('$.state', init)
: field.kind === 'derived_by'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
value = b.call('$.state');
continue;
}
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
const name = get_name(definition.key);
const field = name && /** @type {StateField} */ (state_fields.get(name));
if (!field) {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
continue;
}
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
if (name[0] === '#') {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
} else if (field.node === definition) {
const member = b.member(b.this, field.key);
// set foo(value) { this.#foo = value; }
const val = b.id('value');
const should_proxy = field.type === '$state' && true; // TODO
body.push(
b.prop_def(
field.key,
/** @type {CallExpression} */ (context.visit(field.value, child_state))
),
b.method('get', definition.key, [], [b.return(b.call('$.get', member))]),
b.method(
'set',
definition.key,
[val],
[b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
[b.id('value')],
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
)
);
}
continue;
}
}
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
}
return { ...node, body };
}
/**
* @param {Identifier | PrivateIdentifier | Literal} node
* @param {Map<string, StateField>} public_state
*/
function get_name(node, public_state) {
if (node.type === 'Literal') {
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
// the above could generate conflicts because it has to generate a valid identifier
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
// so we have to de-conflict. We can only check `public_state` because private state
// can't have literal keys
while (name && public_state.has(name)) {
name = '_' + name;
}
return name;
} else {
return node.name;
}
}

@ -9,9 +9,11 @@ import * as b from '#compiler/builders';
export function MemberExpression(node, context) {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
const field = context.state.private_state.get(node.property.name);
const field = context.state.state_fields.get('#' + node.property.name);
if (field) {
return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state')
return context.state.in_constructor &&
(field.type === '$state.raw' || field.type === '$state')
? b.member(node, 'v')
: b.call('$.get', node);
}

@ -564,7 +564,7 @@ export function build_style_directives_object(style_directives, context) {
/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
* the init or the init and update arrays, depending on whether or not the value is dynamic.
* Resulting code for static looks something like this:
* ```js
* element.property = value;

@ -15,7 +15,7 @@ export function UpdateExpression(node, context) {
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
argument.property.type === 'PrivateIdentifier' &&
context.state.private_state.has(argument.property.name)
context.state.state_fields.has('#' + argument.property.name)
) {
let fn = '$.update';
if (node.prefix) fn += '_pre';

@ -23,7 +23,6 @@ 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';
@ -49,7 +48,6 @@ const global_visitors = {
ExpressionStatement,
Identifier,
LabeledStatement,
MemberExpression,
PropertyDefinition,
UpdateExpression,
VariableDeclaration
@ -99,7 +97,7 @@ export function server_component(analysis, options) {
template: /** @type {any} */ (null),
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
private_derived: new Map(),
state_fields: new Map(),
skip_hydration_boundaries: false
};
@ -395,7 +393,7 @@ export function server_module(analysis, options) {
// to be present for `javascript_visitors_legacy` and so is included in module
// transform state as well as component transform state
legacy_reactive_statements: new Map(),
private_derived: new Map()
state_fields: new Map()
};
const module = /** @type {Program} */ (

@ -2,12 +2,10 @@ import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { StateField } from '../client/types.js';
export interface ServerTransformState extends TransformState {
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
readonly private_derived: Map<string, StateField>;
}
export interface ComponentServerTransformState extends ServerTransformState {

@ -3,6 +3,8 @@
/** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '#compiler/builders';
import { build_assignment_value } from '../../../../utils/ast.js';
import { get_name } from '../../../nodes.js';
import { get_rune } from '../../../scope.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
/**
@ -22,6 +24,29 @@ export function AssignmentExpression(node, context) {
* @returns {Expression | null}
*/
function build_assignment(operator, left, right, context) {
if (context.state.analysis.runes && left.type === 'MemberExpression') {
const name = get_name(left.property);
const field = name && context.state.state_fields.get(name);
// special case — state declaration in class constructor
if (field && field.node.type === 'AssignmentExpression' && left === field.node.left) {
const rune = get_rune(right, context.state.scope);
if (rune) {
const key =
left.property.type === 'PrivateIdentifier' || rune === '$state' || rune === '$state.raw'
? left.property
: field.key;
return b.assignment(
operator,
b.member(b.this, key, key.type === 'Literal'),
/** @type {Expression} */ (context.visit(right))
);
}
}
}
let object = left;
while (object.type === 'MemberExpression') {

@ -25,6 +25,15 @@ export function CallExpression(node, context) {
return b.arrow([], b.block([]));
}
if (rune === '$state' || rune === '$state.raw') {
return node.arguments[0] ? context.visit(node.arguments[0]) : b.void0;
}
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);
}
if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',

@ -1,120 +1,77 @@
/** @import { ClassBody, Expression, MethodDefinition, PropertyDefinition } from 'estree' */
/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { Context } from '../types.js' */
/** @import { StateField } from '../../client/types.js' */
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
if (!context.state.analysis.runes) {
const state_fields = context.state.analysis.classes.get(node);
if (!state_fields) {
// in legacy mode, do nothing
context.next();
return;
}
/** @type {Map<string, StateField>} */
const public_derived = new Map();
/** @type {Array<MethodDefinition | PropertyDefinition | StaticBlock>} */
const body = [];
/** @type {Map<string, StateField>} */
const private_derived = new Map();
const child_state = { ...context.state, state_fields };
/** @type {string[]} */
const private_ids = [];
for (const [name, field] of state_fields) {
if (name[0] === '#') {
continue;
}
for (const definition of node.body) {
// insert backing fields for stuff declared in the constructor
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
field &&
field.node.type === 'AssignmentExpression' &&
(field.type === '$derived' || field.type === '$derived.by')
) {
const { type, name } = definition.key;
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
/** @type {StateField} */
const field = {
kind: rune === '$derived.by' ? 'derived_by' : 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_derived.set(name, field);
} else {
public_derived.set(name, field);
}
}
}
}
}
const member = b.member(b.this, field.key);
// each `foo = $derived()` needs a backing `#foo` field
for (const [name, field] of public_derived) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
body.push(
b.prop_def(field.key, null),
b.method('get', b.key(name), [], [b.return(b.call(member))])
);
}
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const body = [];
const child_state = { ...context.state, private_derived };
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const name = definition.key.name;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_derived : public_derived).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
const init = /** @type {Expression} **/ (
context.visit(definition.value.arguments[0], child_state)
);
const value =
field.kind === 'derived_by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) {
if (definition.type !== 'PropertyDefinition') {
body.push(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
/** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
);
continue;
}
}
const name = get_name(definition.key);
const field = name && state_fields.get(name);
if (!field) {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
continue;
}
}
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
} else if (field.node === definition) {
const member = b.member(b.this, field.key);
body.push(
b.prop_def(
field.key,
/** @type {CallExpression} */ (context.visit(field.value, child_state))
),
b.method('get', definition.key, [], [b.return(b.call(member))])
);
}
}
return { ...node, body };

@ -1,23 +0,0 @@
/** @import { 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.private_derived.get(node.property.name);
if (field) {
return b.call(node);
}
}
context.next();
}

@ -1,5 +1,5 @@
import type { Scope } from '../scope.js';
import type { AST, ValidatedModuleCompileOptions } from '#compiler';
import type { AST, StateField, ValidatedModuleCompileOptions } from '#compiler';
import type { Analysis } from '../types.js';
export interface TransformState {
@ -7,4 +7,6 @@ export interface TransformState {
readonly options: ValidatedModuleCompileOptions;
readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, Scope>;
readonly state_fields: Map<string, StateField>;
}

@ -1,4 +1,6 @@
/** @import { Expression, PrivateIdentifier } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
*/
@ -64,3 +66,14 @@ export function create_expression_metadata() {
has_call: false
};
}
/**
* @param {Expression | PrivateIdentifier} node
*/
export function get_name(node) {
if (node.type === 'Literal') return String(node.value);
if (node.type === 'PrivateIdentifier') return '#' + node.name;
if (node.type === 'Identifier') return node.name;
return null;
}

@ -1,6 +1,15 @@
import type { AST, Binding } from '#compiler';
import type { Identifier, LabeledStatement, Node, Program } from 'estree';
import type { AST, Binding, StateField } from '#compiler';
import type {
AssignmentExpression,
ClassBody,
Identifier,
LabeledStatement,
Node,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
import type { StateCreationRuneName } from '../../utils.js';
import type { AnalysisState } from './2-analyze/types.js';
export interface Js {
ast: Program;
@ -29,6 +38,8 @@ export interface Analysis {
immutable: boolean;
tracing: boolean;
classes: Map<ClassBody, Map<string, StateField>>;
// TODO figure out if we can move this to ComponentAnalysis
accessors: boolean;
}

@ -2,6 +2,13 @@ import type { SourceMap } from 'magic-string';
import type { Binding } from '../phases/scope.js';
import type { AST, Namespace } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
import type { StateCreationRuneName } from '../../utils.js';
import type {
AssignmentExpression,
CallExpression,
PrivateIdentifier,
PropertyDefinition
} from 'estree';
/** The return value of `compile` from `svelte/compiler` */
export interface CompileResult {
@ -269,6 +276,13 @@ export interface ExpressionMetadata {
has_call: boolean;
}
export interface StateField {
type: StateCreationRuneName;
node: PropertyDefinition | AssignmentExpression;
key: PrivateIdentifier;
value: CallExpression;
}
export * from './template.js';
export { Binding, Scope } from '../phases/scope.js';

@ -547,7 +547,13 @@ export namespace AST {
| AST.SvelteWindow
| AST.SvelteBoundary;
export type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag;
export type Tag =
| AST.AttachTag
| AST.ConstTag
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag
| AST.RenderTag;
export type TemplateNode =
| AST.Root

@ -12,6 +12,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
set_hydrate_node,
set_hydrating
@ -160,7 +161,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
let mismatch = false;
if (hydrating) {
var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
var is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE;
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over

@ -4,6 +4,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
set_hydrate_node,
set_hydrating
@ -56,7 +57,8 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
if (hydrating && hydrate_index !== -1) {
if (root_index === 0) {
const data = /** @type {Comment} */ (anchor).data;
const data = read_hydration_instruction(anchor);
if (data === HYDRATION_START) {
hydrate_index = 0;
} else if (data === HYDRATION_START_ELSE) {

@ -24,7 +24,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish
// we should only remove the class if the value is nullish
// and there no hash/directives :
if (next_class_name == null) {
dom.removeAttribute('class');

@ -103,3 +103,16 @@ export function remove_nodes() {
node = next;
}
}
/**
*
* @param {TemplateNode} node
*/
export function read_hydration_instruction(node) {
if (!node || node.nodeType !== 8) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
return /** @type {Comment} */ (node).data;
}

@ -3,6 +3,19 @@ import { ReactiveValue } from './reactive-value.js';
const parenthesis_regex = /\(.+\)/;
// these keywords are valid media queries but they need to be without parenthesis
//
// eg: new MediaQuery('screen')
//
// however because of the auto-parenthesis logic in the constructor since there's no parentehesis
// in the media query they'll be surrounded by parenthesis
//
// however we can check if the media query is only composed of these keywords
// and skip the auto-parenthesis
//
// https://github.com/sveltejs/svelte/issues/15930
const non_parenthesized_keywords = new Set(['all', 'print', 'screen', 'and', 'or', 'not', 'only']);
/**
* Creates a media query and provides a `current` property that reflects whether or not it matches.
*
@ -27,7 +40,12 @@ export class MediaQuery extends ReactiveValue {
* @param {boolean} [fallback] Fallback value for the server
*/
constructor(query, fallback) {
let final_query = parenthesis_regex.test(query) ? query : `(${query})`;
let final_query =
parenthesis_regex.test(query) ||
// we need to use `some` here because technically this `window.matchMedia('random,screen')` still returns true
query.split(/[\s,]+/).some((keyword) => non_parenthesized_keywords.has(keyword.trim()))
? query
: `(${query})`;
const q = window.matchMedia(final_query);
super(
() => q.matches,

@ -428,15 +428,19 @@ export function is_mathml(name) {
return MATHML_ELEMENTS.includes(name);
}
const RUNES = /** @type {const} */ ([
export const STATE_CREATION_RUNES = /** @type {const} */ ([
'$state',
'$state.raw',
'$derived',
'$derived.by'
]);
const RUNES = /** @type {const} */ ([
...STATE_CREATION_RUNES,
'$state.snapshot',
'$props',
'$props.id',
'$bindable',
'$derived',
'$derived.by',
'$effect',
'$effect.pre',
'$effect.tracking',
@ -447,12 +451,24 @@ const RUNES = /** @type {const} */ ([
'$host'
]);
/** @typedef {RUNES[number]} RuneName */
/**
* @param {string} name
* @returns {name is RUNES[number]}
* @returns {name is RuneName}
*/
export function is_rune(name) {
return RUNES.includes(/** @type {RUNES[number]} */ (name));
return RUNES.includes(/** @type {RuneName} */ (name));
}
/** @typedef {STATE_CREATION_RUNES[number]} StateCreationRuneName */
/**
* @param {string} name
* @returns {name is StateCreationRuneName}
*/
export function is_state_creation_rune(name) {
return STATE_CREATION_RUNES.includes(/** @type {StateCreationRuneName} */ (name));
}
/** List of elements that require raw contents and should not have SSR comments put in them */

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

@ -4,7 +4,7 @@ export default test({
error: {
code: 'state_invalid_placement',
message:
'`$state(...)` can only be used as a variable declaration initializer or a class field',
'`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.',
position: [33, 41]
}
});

@ -3,6 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'state_invalid_placement',
message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
message:
'`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.'
}
});

@ -4,6 +4,6 @@ export default test({
error: {
code: 'state_invalid_placement',
message:
'`$derived(...)` can only be used as a variable declaration initializer or a class field'
'`$derived(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.'
}
});

@ -3,6 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'state_invalid_placement',
message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
message:
'`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.'
}
});

@ -0,0 +1,6 @@
import { test } from '../../test';
// https://github.com/sveltejs/svelte/issues/15819
export default test({
expect_hydration_error: true
});

@ -0,0 +1 @@
<!--[--> <p>start</p><!--[--><p>cond</p><!--]--><!--]-->

@ -0,0 +1,5 @@
<script>
const cond = true;
</script>
<p>start</p>{#if cond}<p>cond</p>{/if}

@ -0,0 +1,6 @@
import { test } from '../../test';
// https://github.com/sveltejs/svelte/issues/15819
export default test({
expect_hydration_error: true
});

@ -0,0 +1 @@
<!--[--><p>start</p>pre<a>123</a> <!--[-->mid<!--]--><!--]-->

@ -0,0 +1,9 @@
<script>
let cond = true;
</script>
<p>start</p>
pre123
{#if cond}
mid
{/if}

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
ssrHtml: `<button>0</button>`,
async test({ assert, target }) {
flushSync();
assert.htmlEqual(target.innerHTML, `<button>10</button>`);
}
});

@ -0,0 +1,13 @@
<script>
class Counter {
constructor() {
this.count = $state(0);
$effect(() => {
this.count = 10;
});
}
}
const counter = new Counter();
</script>
<button on:click={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
ssrHtml: `<button>0</button>`,
async test({ assert, target }) {
flushSync();
assert.htmlEqual(target.innerHTML, `<button>10</button>`);
}
});

@ -0,0 +1,12 @@
<script>
const counter = new class Counter {
constructor() {
this.count = $state(0);
$effect(() => {
this.count = 10;
});
}
}
</script>
<button on:click={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,9 @@
<script>
class Test {
0 = $state();
constructor() {
this[1] = $state();
}
}
</script>

@ -0,0 +1,45 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
// The component context class instance gets shared between tests, strangely, causing hydration to fail?
mode: ['client', 'server'],
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1]);
flushSync(() => {
btn?.click();
});
assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2]);
flushSync(() => {
btn?.click();
});
assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2, 3]);
flushSync(() => {
btn?.click();
});
assert.deepEqual(logs, [
0,
'class trigger false',
'local trigger false',
1,
2,
3,
4,
'class trigger true',
'local trigger true'
]);
}
});

@ -0,0 +1,37 @@
<script module>
class SomeLogic {
trigger() {
this.someValue++;
}
constructor() {
this.someValue = $state(0);
this.isAboveThree = $derived(this.someValue > 3);
}
}
const someLogic = new SomeLogic();
</script>
<script>
function increment() {
someLogic.trigger();
}
let localDerived = $derived(someLogic.someValue > 3);
$effect(() => {
console.log(someLogic.someValue);
});
$effect(() => {
console.log('class trigger ' + someLogic.isAboveThree)
});
$effect(() => {
console.log('local trigger ' + localDerived)
});
</script>
<button on:click={increment}>
clicks: {someLogic.someValue}
</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,12 @@
<script>
class Counter {
count;
constructor(count) {
this.count = $state(count);
}
}
const counter = new Counter(0);
</script>
<button onclick={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10: 20</button>`,
test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>11: 22</button>`);
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>12: 24</button>`);
}
});

@ -0,0 +1,22 @@
<script>
class Counter {
constructor(initial) {
this.count = $state(initial);
}
increment = () => {
this.count++;
}
}
class PluggableCounter extends Counter {
constructor(initial, plugin) {
super(initial)
this.custom = $derived(plugin(this.count));
}
}
const counter = new PluggableCounter(10, (count) => count * 2);
</script>
<button onclick={counter.increment}>{counter.count}: {counter.custom}</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>20</button>`,
test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>22</button>`);
flushSync(() => {
btn?.click();
});
assert.htmlEqual(target.innerHTML, `<button>24</button>`);
}
});

@ -0,0 +1,18 @@
<script>
class Counter {
/** @type {number} */
#count;
constructor(initial) {
this.#count = $state(initial);
this.doubled = $derived(this.#count * 2);
}
increment = () => {
this.#count++;
}
}
const counter = new Counter(10);
</script>
<button onclick={counter.increment}>{counter.doubled}</button>

@ -5,5 +5,10 @@ export default test({
async test({ window }) {
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 599px), (min-width: 900px)');
expect(window.matchMedia).toHaveBeenCalledWith('(min-width: 900px)');
expect(window.matchMedia).toHaveBeenCalledWith('screen');
expect(window.matchMedia).toHaveBeenCalledWith('not print');
expect(window.matchMedia).toHaveBeenCalledWith('screen,print');
expect(window.matchMedia).toHaveBeenCalledWith('screen, print');
expect(window.matchMedia).toHaveBeenCalledWith('screen, random');
}
});

@ -3,4 +3,9 @@
const mq = new MediaQuery("(max-width: 599px), (min-width: 900px)");
const mq2 = new MediaQuery("min-width: 900px");
const mq3 = new MediaQuery("screen");
const mq4 = new MediaQuery("not print");
const mq5 = new MediaQuery("screen,print");
const mq6 = new MediaQuery("screen, print");
const mq7 = new MediaQuery("screen, random");
</script>

@ -0,0 +1,14 @@
[
{
"code": "state_field_duplicate",
"message": "`count` has already been declared on this class",
"start": {
"line": 5,
"column": 2
},
"end": {
"line": 5,
"column": 24
}
}
]

@ -0,0 +1,7 @@
export class Counter {
count = $state(0);
constructor() {
this.count = $state(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_field_invalid_assignment",
"message": "Cannot assign to a state field before its declaration",
"start": {
"line": 4,
"column": 3
},
"end": {
"line": 4,
"column": 18
}
}
]

@ -0,0 +1,9 @@
export class Counter {
constructor() {
if (true) {
this.count = -1;
}
this.count = $state(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_field_duplicate",
"message": "`count` has already been declared on this class",
"start": {
"line": 5,
"column": 2
},
"end": {
"line": 5,
"column": 24
}
}
]

@ -0,0 +1,7 @@
export class Counter {
constructor() {
this.count = $state(0);
this.count = 1;
this.count = $state(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_field_duplicate",
"message": "`count` has already been declared on this class",
"start": {
"line": 5,
"column": 2
},
"end": {
"line": 5,
"column": 28
}
}
]

@ -0,0 +1,7 @@
export class Counter {
constructor() {
this.count = $state(0);
this.count = 1;
this.count = $state.raw(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_invalid_placement",
"message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
"start": {
"line": 4,
"column": 16
},
"end": {
"line": 4,
"column": 25
}
}
]

@ -0,0 +1,7 @@
export class Counter {
constructor() {
if (true) {
this.count = $state(0);
}
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_field_duplicate",
"message": "`count` has already been declared on this class",
"start": {
"line": 5,
"column": 2
},
"end": {
"line": 5,
"column": 27
}
}
]

@ -0,0 +1,7 @@
export class Counter {
// prettier-ignore
'count' = $state(0);
constructor() {
this['count'] = $state(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_field_duplicate",
"message": "`count` has already been declared on this class",
"start": {
"line": 4,
"column": 2
},
"end": {
"line": 4,
"column": 27
}
}
]

@ -0,0 +1,6 @@
export class Counter {
count = $state(0);
constructor() {
this['count'] = $state(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_invalid_placement",
"message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
"start": {
"line": 5,
"column": 16
},
"end": {
"line": 5,
"column": 25
}
}
]

@ -0,0 +1,7 @@
const count = 'count';
export class Counter {
constructor() {
this[count] = $state(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_field_invalid_assignment",
"message": "Cannot assign to a state field before its declaration",
"start": {
"line": 3,
"column": 2
},
"end": {
"line": 3,
"column": 17
}
}
]

@ -0,0 +1,6 @@
export class Counter {
constructor() {
this.count = -1;
this.count = $state(0);
}
}

@ -0,0 +1,14 @@
[
{
"code": "state_field_invalid_assignment",
"message": "Cannot assign to a state field before its declaration",
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 12
}
}
]

@ -0,0 +1,7 @@
export class Counter {
count = -1;
constructor() {
this.count = $state(0);
}
}

@ -1,7 +1,7 @@
[
{
"code": "state_invalid_placement",
"message": "`$derived(...)` can only be used as a variable declaration initializer or a class field",
"message": "`$derived(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
"start": {
"line": 2,
"column": 15

@ -1362,7 +1362,13 @@ declare module 'svelte/compiler' {
| AST.SvelteWindow
| AST.SvelteBoundary;
export type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag;
export type Tag =
| AST.AttachTag
| AST.ConstTag
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag
| AST.RenderTag;
export type TemplateNode =
| AST.Root

Loading…
Cancel
Save