Merge remote-tracking branch 'upstream/main' into $state-invalidate

pull/15673/head
ComputerGuy 5 months ago
commit a9eb8462b5

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: set deriveds as `CLEAN` if they are assigned to

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: better scope `:global()` with nesting selector `&`

@ -4,7 +4,7 @@ title: .svelte.js and .svelte.ts files
Besides `.svelte` files, Svelte also operates on `.svelte.js` and `.svelte.ts` files.
These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app.
These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app (though note that you [cannot export reassigned state]($state#Passing-state-across-modules)).
> [!LEGACY]
> This is a concept that didn't exist prior to Svelte 5

@ -291,3 +291,83 @@ console.log(total.value); // 7
```
...though if you find yourself writing code like that, consider using [classes](#Classes) instead.
## Passing state across modules
You can declare state in `.svelte.js` and `.svelte.ts` files, but you can only _export_ that state if it's not directly reassigned. In other words you can't do this:
```js
/// file: state.svelte.js
export let count = $state(0);
export function increment() {
count += 1;
}
```
That's because every reference to `count` is transformed by the Svelte compiler — the code above is roughly equivalent to this:
```js
/// file: state.svelte.js (compiler output)
// @filename: index.ts
interface Signal<T> {
value: T;
}
interface Svelte {
state<T>(value?: T): Signal<T>;
get<T>(source: Signal<T>): T;
set<T>(source: Signal<T>, value: T): void;
}
declare const $: Svelte;
// ---cut---
export let count = $.state(0);
export function increment() {
$.set(count, $.get(count) + 1);
}
```
> [!NOTE] You can see the code Svelte generates by clicking the 'JS Output' tab in the [playground](/playground).
Since the compiler only operates on one file at a time, if another file imports `count` Svelte doesn't know that it needs to wrap each reference in `$.get` and `$.set`:
```js
// @filename: state.svelte.js
export let count = 0;
// @filename: index.js
// ---cut---
import { count } from './state.svelte.js';
console.log(typeof count); // 'object', not 'number'
```
This leaves you with two options for sharing state between modules — either don't reassign it...
```js
// This is allowed — since we're updating
// `counter.count` rather than `counter`,
// Svelte doesn't wrap it in `$.state`
export const counter = $state({
count: 0
});
export function increment() {
counter.count += 1;
}
```
...or don't directly export it:
```js
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count += 1;
}
```

@ -21,7 +21,7 @@ A component is attempting to bind to a non-bindable property `%key%` belonging t
### component_api_changed
```
%parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
```
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.

@ -1,5 +1,13 @@
# svelte
## 5.25.10
### Patch Changes
- fix: set deriveds as `CLEAN` if they are assigned to ([#15592](https://github.com/sveltejs/svelte/pull/15592))
- fix: better scope `:global()` with nesting selector `&` ([#15671](https://github.com/sveltejs/svelte/pull/15671))
## 5.25.9
### Patch Changes

@ -12,7 +12,7 @@
## component_api_changed
> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
> Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.

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

@ -536,9 +536,6 @@ export function client_component(analysis, options) {
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
body.unshift(b.stmt(b.call(b.id('$.mark_module_start'))));
body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name))));
}
if (!analysis.runes) {

@ -1,7 +1,6 @@
import * as e from '../errors.js';
import { component_context } from '../context.js';
import { FILENAME } from '../../../constants.js';
import { get_component } from './ownership.js';
/** @param {Function & { [FILENAME]: string }} target */
export function check_target(target) {
@ -15,9 +14,7 @@ export function legacy_api() {
/** @param {string} method */
function error(method) {
// @ts-expect-error
const parent = get_component()?.[FILENAME] ?? 'Something';
e.component_api_changed(parent, method, component[FILENAME]);
e.component_api_changed(method, component[FILENAME]);
}
return {

@ -7,103 +7,6 @@ import { component_context } from '../context.js';
import * as w from '../warnings.js';
import { sanitize_location } from '../../../utils.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
const firefox_pattern = /@(.+):(\d+):(\d+)$/;
function get_stack() {
const stack = new Error().stack;
if (!stack) return null;
const entries = [];
for (const line of stack.split('\n')) {
let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line);
if (match) {
entries.push({
file: match[1],
line: +match[2],
column: +match[3]
});
}
}
return entries;
}
/**
* Determines which `.svelte` component is responsible for a given state change
* @returns {Function | null}
*/
export function get_component() {
// first 4 lines are svelte internals; adjust this number if we change the internal call stack
const stack = get_stack()?.slice(4);
if (!stack) return null;
for (let i = 0; i < stack.length; i++) {
const entry = stack[i];
const modules = boundaries[entry.file];
if (!modules) {
// If the first entry is not a component, that means the modification very likely happened
// within a .svelte.js file, possibly triggered by a component. Since these files are not part
// of the bondaries/component context heuristic, we need to bail in this case, else we would
// have false positives when the .svelte.ts file provides a state creator function, encapsulating
// the state and its mutations, and is being called from a component other than the one who
// called the state creator function.
if (i === 0) return null;
continue;
}
for (const module of modules) {
if (module.end == null) {
return null;
}
if (module.start.line < entry.line && module.end.line > entry.line) {
return module.component;
}
}
}
return null;
}
/**
* Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
* such that subsequent calls to `get_component` can tell us which component is responsible
* for a given state change
*/
export function mark_module_start() {
const start = get_stack()?.[2];
if (start) {
(boundaries[start.file] ??= []).push({
start,
// @ts-expect-error
end: null,
// @ts-expect-error we add the component at the end, since HMR will overwrite the function
component: null
});
}
}
/**
* @param {Function} component
*/
export function mark_module_end(component) {
const end = get_stack()?.[2];
if (end) {
const boundaries_file = boundaries[end.file];
const boundary = boundaries_file[boundaries_file.length - 1];
boundary.end = end;
boundary.component = component;
}
}
/**
* Sets up a validator that
* - traverses the path of a prop to find out if it is allowed to be mutated

@ -54,15 +54,14 @@ export function bind_not_bindable(key, component, name) {
}
/**
* %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
* @param {string} parent
* Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5
* @param {string} method
* @param {string} component
* @returns {never}
*/
export function component_api_changed(parent, method, component) {
export function component_api_changed(method, component) {
if (DEV) {
const error = new Error(`component_api_changed\n${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
error.name = 'Svelte error';
throw error;

@ -4,7 +4,7 @@ export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
export { mark_module_start, mark_module_end, create_ownership_validator } from './dev/ownership.js';
export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { trace } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';

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

Loading…
Cancel
Save