Skip to content

Commit

Permalink
Merge branch 'main' into tweak-snapshot-example
Browse files Browse the repository at this point in the history
  • Loading branch information
Rich-Harris authored Jul 14, 2024
2 parents ba1e2fa + b27113d commit ad20f48
Show file tree
Hide file tree
Showing 44 changed files with 504 additions and 224 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-months-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: show correct errors for invalid runes in `.svelte.js` files
5 changes: 5 additions & 0 deletions .changeset/perfect-actors-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

breaking: use structuredClone inside `$state.snapshot`
2 changes: 2 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@
"heavy-ears-rule",
"hip-balloons-begin",
"hip-garlics-tap",
"hip-months-breathe",
"hip-pumpkins-boil",
"honest-buses-add",
"honest-dragons-turn",
Expand Down Expand Up @@ -408,6 +409,7 @@
"orange-masks-exercise",
"orange-yaks-protect",
"orange-zoos-heal",
"perfect-actors-bake",
"pink-bikes-agree",
"pink-goats-promise",
"pink-mayflies-tie",
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/03-runes/01-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ This can improve performance with large arrays and objects that you weren't plan

In development mode, the argument to `$state.frozen` will be shallowly frozen with `Object.freeze()`, to make it obvious if you accidentally mutate it.

> Objects and arrays passed to `$state.frozen` will have a `Symbol` property added to them to signal to Svelte that they are frozen. If you don't want this, pass in a clone of the object or array instead.
> Objects and arrays passed to `$state.frozen` will have a `Symbol` property added to them to signal to Svelte that they are frozen. If you don't want this, pass in a clone of the object or array instead. The argument cannot be an existing state proxy created with `$state(...)`.
## `$state.snapshot`

Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# svelte

## 5.0.0-next.184

### Patch Changes

- fix: show correct errors for invalid runes in `.svelte.js` files ([#12432](https://github.com/sveltejs/svelte/pull/12432))

- breaking: use structuredClone inside `$state.snapshot` ([#12413](https://github.com/sveltejs/svelte/pull/12413))

## 5.0.0-next.183

### Patch Changes
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/messages/client-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@

> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
## state_frozen_invalid_argument

> The argument to `$state.frozen(...)` cannot be an object created with `$state(...)`. You should create a copy of it first, for example with `$state.snapshot`
## state_prototype_fixed

> Cannot set prototype of `$state` object
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.183",
"version": "5.0.0-next.184",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
Expand Down
71 changes: 70 additions & 1 deletion packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,75 @@ declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined;

declare namespace $state {
type Primitive = string | number | boolean | null | undefined;

type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array;

/** The things that `structuredClone` can handle — https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm */
export type Cloneable =
| ArrayBuffer
| DataView
| Date
| Error
| Map<any, any>
| RegExp
| Set<any>
| TypedArray
// web APIs
| Blob
| CryptoKey
| DOMException
| DOMMatrix
| DOMMatrixReadOnly
| DOMPoint
| DOMPointReadOnly
| DOMQuad
| DOMRect
| DOMRectReadOnly
| File
| FileList
| FileSystemDirectoryHandle
| FileSystemFileHandle
| FileSystemHandle
| ImageBitmap
| ImageData
| RTCCertificate
| VideoFrame;

/** Turn `SvelteDate`, `SvelteMap` and `SvelteSet` into their non-reactive counterparts. (`URL` is uncloneable.) */
type NonReactive<T> = T extends Date
? Date
: T extends Map<infer K, infer V>
? Map<K, V>
: T extends Set<infer K>
? Set<K>
: T;

type Snapshot<T> = T extends Primitive
? T
: T extends Cloneable
? NonReactive<T>
: T extends { toJSON(): infer R }
? R
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;

/**
* Declares reactive read-only state that is shallowly immutable.
*
Expand Down Expand Up @@ -75,7 +144,7 @@ declare namespace $state {
*
* @param state The value to snapshot
*/
export function snapshot<T>(state: T): T;
export function snapshot<T>(state: T): Snapshot<T>;

/**
* Compare two values, one or both of which is a reactive `$state(...)` proxy.
Expand Down
71 changes: 36 additions & 35 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,41 @@ export const validation_runes_js = {
if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) {
w.perf_avoid_inline_class(node);
}
},
Identifier(node, { path, state }) {
let i = path.length;
let parent = /** @type {import('estree').Expression} */ (path[--i]);

if (
Runes.includes(/** @type {Runes[number]} */ (node.name)) &&
is_reference(node, parent) &&
state.scope.get(node.name) === null &&
state.scope.get(node.name.slice(1)) === null
) {
/** @type {import('estree').Expression} */
let current = node;
let name = node.name;

while (parent.type === 'MemberExpression') {
if (parent.computed) e.rune_invalid_computed_property(parent);
name += `.${/** @type {import('estree').Identifier} */ (parent.property).name}`;

current = parent;
parent = /** @type {import('estree').Expression} */ (path[--i]);

if (!Runes.includes(/** @type {Runes[number]} */ (name))) {
if (name === '$effect.active') {
e.rune_renamed(parent, '$effect.active', '$effect.tracking');
}

e.rune_invalid_name(parent, name);
}
}

if (parent.type !== 'CallExpression') {
e.rune_missing_parentheses(current);
}
}
}
};

Expand Down Expand Up @@ -1153,41 +1188,6 @@ export const validation_runes = merge(validation, a11y_validators, {
e.import_svelte_internal_forbidden(node);
}
},
Identifier(node, { path, state }) {
let i = path.length;
let parent = /** @type {import('estree').Expression} */ (path[--i]);

if (
Runes.includes(/** @type {Runes[number]} */ (node.name)) &&
is_reference(node, parent) &&
state.scope.get(node.name) === null &&
state.scope.get(node.name.slice(1)) === null
) {
/** @type {import('estree').Expression} */
let current = node;
let name = node.name;

while (parent.type === 'MemberExpression') {
if (parent.computed) e.rune_invalid_computed_property(parent);
name += `.${/** @type {import('estree').Identifier} */ (parent.property).name}`;

current = parent;
parent = /** @type {import('estree').Expression} */ (path[--i]);

if (!Runes.includes(/** @type {Runes[number]} */ (name))) {
if (name === '$effect.active') {
e.rune_renamed(parent, '$effect.active', '$effect.tracking');
}

e.rune_invalid_name(parent, name);
}
}

if (parent.type !== 'CallExpression') {
e.rune_missing_parentheses(current);
}
}
},
LabeledStatement(node, { path }) {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
e.legacy_reactive_statement_invalid(node);
Expand Down Expand Up @@ -1368,5 +1368,6 @@ export const validation_runes = merge(validation, a11y_validators, {
// TODO this is a code smell. need to refactor this stuff
ClassBody: validation_runes_js.ClassBody,
ClassDeclaration: validation_runes_js.ClassDeclaration,
Identifier: validation_runes_js.Identifier,
NewExpression: validation_runes_js.NewExpression
});
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,10 @@ const global_visitors = {
}

if (rune === '$state.snapshot') {
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
return b.call(
'$.snapshot',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
);
}

if (rune === '$state.is') {
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/index-client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/client/utils.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
import { lifecycle_outside_component } from './internal/shared/errors.js';
Expand Down
45 changes: 2 additions & 43 deletions packages/svelte/src/internal/client/dev/inspect.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { snapshot } from '../proxy.js';
import { snapshot } from '../../shared/clone.js';
import { inspect_effect, validate_effect } from '../reactivity/effects.js';
import { array_prototype, get_prototype_of, object_prototype } from '../utils.js';

/**
* @param {() => any[]} get_value
Expand All @@ -13,47 +12,7 @@ export function inspect(get_value, inspector = console.log) {
let initial = true;

inspect_effect(() => {
inspector(initial ? 'init' : 'update', ...deep_snapshot(get_value()));
inspector(initial ? 'init' : 'update', ...snapshot(get_value()));
initial = false;
});
}

/**
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_snapshot(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = snapshot(value);

if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}

const prototype = get_prototype_of(value);

// Only deeply snapshot plain objects and arrays
if (prototype === object_prototype || prototype === array_prototype) {
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};

for (let key in value) {
const result = deep_snapshot(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
}
}

visited.set(value, contains_unstated ? nested_unstated : value);
} else {
visited.set(value, value);
}
}

return visited.get(value) ?? value;
}
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dev/ownership.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { STATE_SYMBOL } from '../constants.js';
import { render_effect, user_pre_effect } from '../reactivity/effects.js';
import { dev_current_component_function } from '../runtime.js';
import { get_prototype_of } from '../utils.js';
import { get_prototype_of } from '../../shared/utils.js';
import * as w from '../warnings.js';

/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
resume_effect
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../utils.js';
import { is_array, is_frozen } from '../../../shared/utils.js';
import { INERT, STATE_FROZEN_SYMBOL, STATE_SYMBOL } from '../../constants.js';
import { queue_micro_task } from '../task.js';
import { current_effect } from '../../runtime.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dom/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function append_styles(anchor, css) {

var target = /** @type {ShadowRoot} */ (root).host
? /** @type {ShadowRoot} */ (root)
: /** @type {Document} */ (root).head;
: /** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head;

if (!target.querySelector('#' + css.hash)) {
const style = document.createElement('style');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../utils.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import {
AttributeAliases,
DelegatedEvents,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { teardown } from '../../../reactivity/effects.js';
import { get_descriptor } from '../../../utils.js';
import { get_descriptor } from '../../../../shared/utils.js';

/**
* Makes an `export`ed (non-prop) variable available on the `$$props` object
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { append } from '../template.js';
import { define_property, object_keys } from '../../utils.js';
import { define_property, object_keys } from '../../../shared/utils.js';

/**
* @typedef {Object} CustomElementPropDefinition
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dom/elements/events.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { teardown } from '../../reactivity/effects.js';
import { define_property, is_array } from '../../utils.js';
import { define_property, is_array } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { noop } from '../../../shared/utils.js';
import { noop, is_function } from '../../../shared/utils.js';
import { effect } from '../../reactivity/effects.js';
import { current_effect, untrack } from '../../runtime.js';
import { raf } from '../../timing.js';
import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { is_function } from '../../utils.js';
import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
Expand Down
Loading

0 comments on commit ad20f48

Please sign in to comment.