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 ### 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 ```js
// @errors: 7006 2554 // @errors: 7006 2554
class Todo { class Todo {
done = $state(false); done = $state(false);
text = $state();
constructor(text) { constructor(text) {
this.text = text; this.text = $state(text);
} }
reset() { reset() {
@ -110,10 +109,9 @@ You can either use an inline function...
// @errors: 7006 2554 // @errors: 7006 2554
class Todo { class Todo {
done = $state(false); done = $state(false);
text = $state();
constructor(text) { constructor(text) {
this.text = text; this.text = $state(text);
} }
+++reset = () => {+++ +++reset = () => {+++

@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand.
<Widget foo={bar} answer={42} text="hello" /> <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. _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 ```svelte
<Widget {...things} /> <Widget a="b" {...things} c="d" />
``` ```
## Events ## 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} {#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 ```svelte
{#each items as item (item.id)} {#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 ```svelte
<!--- file: App.svelte ----> <!--- file: App.svelte ---->
<script> <script>
import { myGlobalState } from 'svelte'; import { myGlobalState } from './state.svelte.js';
let { data } = $props(); 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 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 ### state_invalid_export
``` ```
@ -855,7 +887,7 @@ Cannot export state from a module if it is reassigned. Either export a function
### state_invalid_placement ### 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 ### store_invalid_scoped_subscription

@ -1,5 +1,23 @@
# svelte # 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 ## 5.30.1
### Patch Changes ### Patch Changes

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

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

@ -1,18 +1,23 @@
// @ts-check // @ts-check
import process from 'node:process';
import fs from 'node:fs'; import fs from 'node:fs';
import * as acorn from 'acorn'; import * as acorn from 'acorn';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as esrap from 'esrap'; import * as esrap from 'esrap';
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
const messages = {};
const seen = new Set();
const DIR = '../../documentation/docs/98-reference/.generated'; const DIR = '../../documentation/docs/98-reference/.generated';
fs.rmSync(DIR, { force: true, recursive: true });
fs.mkdirSync(DIR);
for (const category of fs.readdirSync('messages')) { const watch = process.argv.includes('-w');
function run() {
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
const messages = {};
const seen = new Set();
fs.rmSync(DIR, { force: true, recursive: true });
fs.mkdirSync(DIR);
for (const category of fs.readdirSync('messages')) {
if (category.startsWith('.')) continue; if (category.startsWith('.')) continue;
messages[category] = {}; messages[category] = {};
@ -54,6 +59,7 @@ for (const category of fs.readdirSync('messages')) {
} }
sorted.sort((a, b) => (a.code < b.code ? -1 : 1)); sorted.sort((a, b) => (a.code < b.code ? -1 : 1));
fs.writeFileSync( fs.writeFileSync(
`messages/${category}/${file}`, `messages/${category}/${file}`,
sorted.map((x) => x._.trim()).join('\n\n') + '\n' 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' + '<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->\n\n' +
Object.entries(messages[category]) Object.entries(messages[category])
.map(([code, { messages, details }]) => { .map(([code, { messages, details }]) => {
const chunks = [`### ${code}`, ...messages.map((message) => '```\n' + message + '\n```')]; const chunks = [
`### ${code}`,
...messages.map((message) => '```\n' + message + '\n```')
];
if (details) { if (details) {
chunks.push(details); chunks.push(details);
@ -77,13 +86,13 @@ for (const category of fs.readdirSync('messages')) {
.join('\n\n') + .join('\n\n') +
'\n' '\n'
); );
} }
/** /**
* @param {string} name * @param {string} name
* @param {string} dest * @param {string} dest
*/ */
function transform(name, dest) { function transform(name, dest) {
const source = fs const source = fs
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8') .readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
.replace(/\r\n/g, '\n'); .replace(/\r\n/g, '\n');
@ -397,13 +406,36 @@ function transform(name, dest) {
module.code, module.code,
'utf-8' 'utf-8'
); );
}
transform('compile-errors', 'src/compiler/errors.js');
transform('compile-warnings', 'src/compiler/warnings.js');
transform('client-warnings', 'src/internal/client/warnings.js');
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');
} }
transform('compile-errors', 'src/compiler/errors.js'); if (watch) {
transform('compile-warnings', 'src/compiler/warnings.js'); 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();
}
});
}
transform('client-warnings', 'src/internal/client/warnings.js'); run();
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');

@ -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`); 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 * 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 * @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 {null | number | NodeLike} node
* @param {string} rune * @param {string} rune
* @returns {never} * @returns {never}
*/ */
export function state_invalid_placement(node, rune) { 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 { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js'; import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js'; import { OnDirective } from './visitors/OnDirective.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js'; import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js'; import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js'; import { SlotElement } from './visitors/SlotElement.js';
@ -164,6 +165,7 @@ const visitors = {
MemberExpression, MemberExpression,
NewExpression, NewExpression,
OnDirective, OnDirective,
PropertyDefinition,
RegularElement, RegularElement,
RenderTag, RenderTag,
SlotElement, SlotElement,
@ -256,7 +258,8 @@ export function analyze_module(ast, options) {
accessors: false, accessors: false,
runes: true, runes: true,
immutable: true, immutable: true,
tracing: false tracing: false,
classes: new Map()
}; };
walk( walk(
@ -265,7 +268,7 @@ export function analyze_module(ast, options) {
scope, scope,
scopes, scopes,
analysis: /** @type {ComponentAnalysis} */ (analysis), 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, // 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 // 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), ast_type: /** @type {any} */ (null),
@ -429,6 +432,7 @@ export function analyze_component(root, source, options) {
elements: [], elements: [],
runes, runes,
tracing: false, tracing: false,
classes: new Map(),
immutable: runes || options.immutable, immutable: runes || options.immutable,
exports: [], exports: [],
uses_props: false, uses_props: false,
@ -624,7 +628,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false, has_props_rune: false,
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
derived_state: [], state_fields: new Map(),
function_depth: scope.function_depth, function_depth: scope.function_depth,
reactive_statement: null reactive_statement: null
}; };
@ -691,7 +695,7 @@ export function analyze_component(root, source, options) {
reactive_statement: null, reactive_statement: null,
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
derived_state: [], state_fields: new Map(),
function_depth: scope.function_depth function_depth: scope.function_depth
}; };

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

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

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

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

@ -114,12 +114,13 @@ export function CallExpression(node, context) {
case '$state': case '$state':
case '$state.raw': case '$state.raw':
case '$derived': case '$derived':
case '$derived.by': case '$derived.by': {
if ( const valid =
(parent.type !== 'VariableDeclarator' || is_variable_declaration(parent, context) ||
get_parent(context.path, -3).type === 'ConstTag') && is_class_property_definition(parent) ||
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) is_class_property_assignment_at_constructor_root(parent, context);
) {
if (!valid) {
e.state_invalid_placement(node, rune); e.state_invalid_placement(node, rune);
} }
@ -130,6 +131,7 @@ export function CallExpression(node, context) {
} }
break; break;
}
case '$effect': case '$effect':
case '$effect.pre': case '$effect.pre':
@ -270,3 +272,40 @@ function get_function_label(nodes) {
return parent.id.name; 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 { Context } from '../types' */
import * as b from '#compiler/builders';
import { get_rune } from '../../scope.js'; 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 {ClassBody} node
* @param {Context} context * @param {Context} context
*/ */
export function ClassBody(node, context) { export function ClassBody(node, context) {
/** @type {{name: string, private: boolean}[]} */ if (!context.state.analysis.runes) {
const derived_state = []; context.next();
return;
}
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) { for (const prop of node.body) {
if ( if (
definition.type === 'PropertyDefinition' && (prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') && prop.key.type === 'PrivateIdentifier'
definition.value?.type === 'CallExpression'
) { ) {
const rune = get_rune(definition.value, context.state.scope); private_ids.push(prop.key.name);
if (rune === '$derived' || rune === '$derived.by') { }
derived_state.push({ }
name: definition.key.name,
private: definition.key.type === 'PrivateIdentifier' /** @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 * @param {Context} context
*/ */
export function UpdateExpression(node, context) { export function UpdateExpression(node, context) {
validate_assignment(node, node.argument, context.state); validate_assignment(node, node.argument, context);
if (context.state.reactive_statement) { if (context.state.reactive_statement) {
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument; const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;

@ -4,24 +4,25 @@
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
/** @import { NodeLike } from '../../../../errors.js' */ /** @import { NodeLike } from '../../../../errors.js' */
import * as e 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 w from '../../../../warnings.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/** /**
* @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node * @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument * @param {Pattern | Expression} argument
* @param {AnalysisState} state * @param {Context} context
*/ */
export function validate_assignment(node, argument, state) { export function validate_assignment(node, argument, context) {
validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective'); validate_no_const_assignment(node, argument, context.state.scope, node.type === 'BindDirective');
if (argument.type === 'Identifier') { if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name); const binding = context.state.scope.get(argument.name);
if (state.analysis.runes) { if (context.state.analysis.runes) {
if (binding?.node === state.analysis.props_id) { if (binding?.node === context.state.analysis.props_id) {
e.constant_assignment(node, '$props.id()'); e.constant_assignment(node, '$props.id()');
} }
@ -34,6 +35,41 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node); 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(), events: new Set(),
preserve_whitespace: options.preserveWhitespace, preserve_whitespace: options.preserveWhitespace,
public_state: new Map(), state_fields: new Map(),
private_state: new Map(),
transform: {}, transform: {},
in_constructor: false, in_constructor: false,
instance_level_snippets: [], instance_level_snippets: [],
@ -671,8 +670,7 @@ export function client_module(analysis, options) {
options, options,
scope: analysis.module.scope, scope: analysis.module.scope,
scopes: analysis.module.scopes, scopes: analysis.module.scopes,
public_state: new Map(), state_fields: new Map(),
private_state: new Map(),
transform: {}, transform: {},
in_constructor: false in_constructor: false
}; };

@ -9,15 +9,12 @@ import type {
UpdateExpression, UpdateExpression,
VariableDeclaration VariableDeclaration
} from 'estree'; } 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 { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { SourceLocation } from '#shared'; import type { SourceLocation } from '#shared';
export interface ClientTransformState extends TransformState { 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 * `true` if the current lexical scope belongs to a class constructor. this allows
* us to rewrite `this.foo` as `this.#foo.value` * us to rewrite `this.foo` as `this.#foo.value`
@ -94,11 +91,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly module_level_snippets: VariableDeclaration[]; 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 Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, any>; 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 { should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js'; import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js'; import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/** /**
* @param {AssignmentExpression} node * @param {AssignmentExpression} node
@ -50,27 +52,44 @@ const callees = {
* @returns {Expression | null} * @returns {Expression | null}
*/ */
function build_assignment(operator, left, right, context) { function build_assignment(operator, left, right, context) {
// Handle class private/public state assignment cases if (context.state.analysis.runes && left.type === 'MemberExpression') {
if ( const name = get_name(left.property);
context.state.analysis.runes && const field = name && context.state.state_fields.get(name);
left.type === 'MemberExpression' &&
left.property.type === 'PrivateIdentifier' if (field) {
) { // special case — state declaration in class constructor
const private_state = context.state.private_state.get(left.property.name); 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} */ ( let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right)) context.visit(build_assignment_value(operator, left, right))
); );
const needs_proxy = const needs_proxy =
private_state.kind === 'state' && field.type === '$state' &&
is_non_coercive_operator(operator) && is_non_coercive_operator(operator) &&
should_proxy(value, context.state.scope); should_proxy(value, context.state.scope);
return b.call('$.set', left, value, needs_proxy && b.true); return b.call('$.set', left, value, needs_proxy && b.true);
} }
} }
}
let object = left; let object = left;

@ -4,19 +4,52 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js'; import { transform_inspect_rune } from '../../utils.js';
import { should_proxy } from '../utils.js';
/** /**
* @param {CallExpression} node * @param {CallExpression} node
* @param {Context} context * @param {Context} context
*/ */
export function CallExpression(node, 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': case '$host':
return b.id('$$props.$$host'); return b.id('$$props.$$host');
case '$effect.tracking': case '$effect.tracking':
return b.call('$.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': case '$state.snapshot':
return b.call( return b.call(
'$.snapshot', '$.snapshot',

@ -1,184 +1,96 @@
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */ /** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { Context, StateField } from '../types' */ /** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { regex_invalid_identifier_chars } from '../../../patterns.js'; import { get_name } from '../../../nodes.js';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
/** /**
* @param {ClassBody} node * @param {ClassBody} node
* @param {Context} context * @param {Context} context
*/ */
export function ClassBody(node, 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(); context.next();
return; return;
} }
/** @type {Map<string, StateField>} */ /** @type {Array<MethodDefinition | PropertyDefinition | StaticBlock>} */
const public_state = new Map(); const body = [];
/** @type {Map<string, StateField>} */ const child_state = { ...context.state, state_fields };
const private_state = new Map();
/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */ for (const [name, field] of state_fields) {
const definition_names = new Map(); if (name[0] === '#') {
continue;
}
/** @type {string[]} */ // insert backing fields for stuff declared in the constructor
const private_ids = []; if (field.node.type === 'AssignmentExpression') {
const member = b.member(b.this, field.key);
for (const definition of node.body) { const should_proxy = field.type === '$state' && true; // TODO
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);
}
}
}
}
}
// each `foo = $state()` needs a backing `#foo` field const key = b.key(name);
for (const [name, field] of public_state) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted); body.push(
field.id = b.private_id(deconflicted); b.prop_def(field.key, null),
}
/** @type {Array<MethodDefinition | PropertyDefinition>} */ b.method('get', key, [], [b.return(b.call('$.get', member))]),
const body = [];
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 // Replace parts of the class body
for (const definition of node.body) { for (const definition of node.body) {
if ( if (definition.type !== 'PropertyDefinition') {
definition.type === 'PropertyDefinition' && body.push(
(definition.key.type === 'Identifier' || /** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
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)
); );
continue;
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');
} }
if (is_private) { const name = get_name(definition.key);
body.push(b.prop_def(field.id, value)); const field = name && /** @type {StateField} */ (state_fields.get(name));
} else {
// #foo; if (!field) {
const member = b.member(b.this, field.id); body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
body.push(b.prop_def(field.id, value)); continue;
}
// get foo() { return this.#foo; } if (name[0] === '#') {
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))])); 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 should_proxy = field.type === '$state' && true; // TODO
const val = b.id('value');
body.push( 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( b.method(
'set', 'set',
definition.key, definition.key,
[val], [b.id('value')],
[b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))] [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 }; 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) { export function MemberExpression(node, context) {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor // rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') { 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) { 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.member(node, 'v')
: b.call('$.get', node); : 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 * 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: * Resulting code for static looks something like this:
* ```js * ```js
* element.property = value; * element.property = value;

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

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

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

@ -3,6 +3,8 @@
/** @import { Context, ServerTransformState } from '../types.js' */ /** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_assignment_value } from '../../../../utils/ast.js'; 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'; import { visit_assignment_expression } from '../../shared/assignments.js';
/** /**
@ -22,6 +24,29 @@ export function AssignmentExpression(node, context) {
* @returns {Expression | null} * @returns {Expression | null}
*/ */
function build_assignment(operator, left, right, context) { 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; let object = left;
while (object.type === 'MemberExpression') { while (object.type === 'MemberExpression') {

@ -25,6 +25,15 @@ export function CallExpression(node, context) {
return b.arrow([], b.block([])); 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') { if (rune === '$state.snapshot') {
return b.call( return b.call(
'$.snapshot', '$.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 { Context } from '../types.js' */
/** @import { StateField } from '../../client/types.js' */
import { dev } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js'; import { get_name } from '../../../nodes.js';
/** /**
* @param {ClassBody} node * @param {ClassBody} node
* @param {Context} context * @param {Context} context
*/ */
export function ClassBody(node, 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(); context.next();
return; return;
} }
/** @type {Map<string, StateField>} */ /** @type {Array<MethodDefinition | PropertyDefinition | StaticBlock>} */
const public_derived = new Map(); const body = [];
/** @type {Map<string, StateField>} */ const child_state = { ...context.state, state_fields };
const private_derived = new Map();
/** @type {string[]} */ for (const [name, field] of state_fields) {
const private_ids = []; if (name[0] === '#') {
continue;
}
for (const definition of node.body) { // insert backing fields for stuff declared in the constructor
if ( if (
definition.type === 'PropertyDefinition' && field &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier') field.node.type === 'AssignmentExpression' &&
(field.type === '$derived' || field.type === '$derived.by')
) { ) {
const { type, name } = definition.key; const member = b.member(b.this, field.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);
}
}
}
}
}
// each `foo = $derived()` needs a backing `#foo` field body.push(
for (const [name, field] of public_derived) { b.prop_def(field.key, null),
let deconflicted = name; b.method('get', b.key(name), [], [b.return(b.call(member))])
while (private_ids.includes(deconflicted)) { );
deconflicted = '_' + deconflicted;
} }
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 // Replace parts of the class body
for (const definition of node.body) { for (const definition of node.body) {
if ( if (definition.type !== 'PropertyDefinition') {
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')) {
body.push( body.push(
b.method( /** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
); );
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; 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 }; 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 { Scope } from '../scope.js';
import type { AST, ValidatedModuleCompileOptions } from '#compiler'; import type { AST, StateField, ValidatedModuleCompileOptions } from '#compiler';
import type { Analysis } from '../types.js'; import type { Analysis } from '../types.js';
export interface TransformState { export interface TransformState {
@ -7,4 +7,6 @@ export interface TransformState {
readonly options: ValidatedModuleCompileOptions; readonly options: ValidatedModuleCompileOptions;
readonly scope: Scope; readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, 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' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** /**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children * 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 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 { AST, Binding, StateField } from '#compiler';
import type { Identifier, LabeledStatement, Node, Program } from 'estree'; import type {
AssignmentExpression,
ClassBody,
Identifier,
LabeledStatement,
Node,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js'; import type { Scope, ScopeRoot } from './scope.js';
import type { StateCreationRuneName } from '../../utils.js';
import type { AnalysisState } from './2-analyze/types.js';
export interface Js { export interface Js {
ast: Program; ast: Program;
@ -29,6 +38,8 @@ export interface Analysis {
immutable: boolean; immutable: boolean;
tracing: boolean; tracing: boolean;
classes: Map<ClassBody, Map<string, StateField>>;
// TODO figure out if we can move this to ComponentAnalysis // TODO figure out if we can move this to ComponentAnalysis
accessors: boolean; accessors: boolean;
} }

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

@ -547,7 +547,13 @@ export namespace AST {
| AST.SvelteWindow | AST.SvelteWindow
| AST.SvelteBoundary; | 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 = export type TemplateNode =
| AST.Root | AST.Root

@ -12,6 +12,7 @@ import {
hydrate_next, hydrate_next,
hydrate_node, hydrate_node,
hydrating, hydrating,
read_hydration_instruction,
remove_nodes, remove_nodes,
set_hydrate_node, set_hydrate_node,
set_hydrating set_hydrating
@ -160,7 +161,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
let mismatch = false; let mismatch = false;
if (hydrating) { 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)) { if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over // hydration mismatch — remove the server-rendered DOM and start over

@ -4,6 +4,7 @@ import {
hydrate_next, hydrate_next,
hydrate_node, hydrate_node,
hydrating, hydrating,
read_hydration_instruction,
remove_nodes, remove_nodes,
set_hydrate_node, set_hydrate_node,
set_hydrating set_hydrating
@ -56,7 +57,8 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) {
if (hydrating && hydrate_index !== -1) { if (hydrating && hydrate_index !== -1) {
if (root_index === 0) { if (root_index === 0) {
const data = /** @type {Comment} */ (anchor).data; const data = read_hydration_instruction(anchor);
if (data === HYDRATION_START) { if (data === HYDRATION_START) {
hydrate_index = 0; hydrate_index = 0;
} else if (data === HYDRATION_START_ELSE) { } 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')) { if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes // Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So // 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 : // and there no hash/directives :
if (next_class_name == null) { if (next_class_name == null) {
dom.removeAttribute('class'); dom.removeAttribute('class');

@ -103,3 +103,16 @@ export function remove_nodes() {
node = next; 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 = /\(.+\)/; 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. * 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 * @param {boolean} [fallback] Fallback value for the server
*/ */
constructor(query, fallback) { 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); const q = window.matchMedia(final_query);
super( super(
() => q.matches, () => q.matches,

@ -428,15 +428,19 @@ export function is_mathml(name) {
return MATHML_ELEMENTS.includes(name); return MATHML_ELEMENTS.includes(name);
} }
const RUNES = /** @type {const} */ ([ export const STATE_CREATION_RUNES = /** @type {const} */ ([
'$state', '$state',
'$state.raw', '$state.raw',
'$derived',
'$derived.by'
]);
const RUNES = /** @type {const} */ ([
...STATE_CREATION_RUNES,
'$state.snapshot', '$state.snapshot',
'$props', '$props',
'$props.id', '$props.id',
'$bindable', '$bindable',
'$derived',
'$derived.by',
'$effect', '$effect',
'$effect.pre', '$effect.pre',
'$effect.tracking', '$effect.tracking',
@ -447,12 +451,24 @@ const RUNES = /** @type {const} */ ([
'$host' '$host'
]); ]);
/** @typedef {RUNES[number]} RuneName */
/** /**
* @param {string} name * @param {string} name
* @returns {name is RUNES[number]} * @returns {name is RuneName}
*/ */
export function is_rune(name) { 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 */ /** 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. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.30.1'; export const VERSION = '5.31.0';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -4,7 +4,7 @@ export default test({
error: { error: {
code: 'state_invalid_placement', code: 'state_invalid_placement',
message: 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] position: [33, 41]
} }
}); });

@ -3,6 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'state_invalid_placement', 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: { error: {
code: 'state_invalid_placement', code: 'state_invalid_placement',
message: 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({ export default test({
error: { error: {
code: 'state_invalid_placement', 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 }) { async test({ window }) {
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 599px), (min-width: 900px)'); expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 599px), (min-width: 900px)');
expect(window.matchMedia).toHaveBeenCalledWith('(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 mq = new MediaQuery("(max-width: 599px), (min-width: 900px)");
const mq2 = new MediaQuery("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> </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", "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": { "start": {
"line": 2, "line": 2,
"column": 15 "column": 15

@ -1362,7 +1362,13 @@ declare module 'svelte/compiler' {
| AST.SvelteWindow | AST.SvelteWindow
| AST.SvelteBoundary; | 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 = export type TemplateNode =
| AST.Root | AST.Root

Loading…
Cancel
Save