pull/9826/head
Rich Harris 3 years ago
commit 6d06d5aae1

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better readonly checks for proxies

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent infinite loops stemming from invalidation method

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve non state referenced warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve consistency issues around binding invalidation

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: adjust children snippet default type

@ -66,7 +66,7 @@ export type MessageEventHandler<T extends EventTarget> = EventHandler<MessageEve
export interface DOMAttributes<T extends EventTarget> {
// Implicit children prop every element has
// Add this here so that libraries doing `$props<HTMLButtonAttributes>()` don't need a separate interface
children?: import('svelte').Snippet<any>;
children?: import('svelte').Snippet<void>;
// Clipboard Events
'on:copy'?: ClipboardEventHandler<T> | undefined | null;

@ -409,10 +409,10 @@ export function analyze_component(root, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
}
// warn on any nonstate declarations that are a) mutated and b) referenced in the template
// warn on any nonstate declarations that are a) reassigned and mutated and b) referenced in the template
for (const scope of [module.scope, instance.scope]) {
outer: for (const [name, binding] of scope.declarations) {
if (binding.kind === 'normal' && binding.mutated) {
if (binding.kind === 'normal' && binding.reassigned && binding.mutated) {
for (const { path } of binding.references) {
if (path[0].type !== 'Fragment') continue;
for (let i = 1; i < path.length; i += 1) {

@ -74,19 +74,11 @@ export function serialize_get_binding(node, state) {
if (
!state.analysis.accessors &&
!(state.analysis.runes ? binding.reassigned : binding.mutated) &&
!(state.analysis.immutable ? binding.reassigned : binding.mutated) &&
!binding.initial
) {
return b.member(b.id('$$props'), node);
}
if (
!(state.analysis.immutable ? binding.reassigned : binding.mutated) &&
!binding.initial &&
!state.analysis.accessors
) {
return b.call(node);
}
}
if (binding.kind === 'legacy_reactive_import') {

@ -245,7 +245,7 @@ function setup_select_synchronization(value_binding, context) {
context.state.init.push(
b.stmt(
b.call(
'$.pre_effect',
'$.invalidate_effect',
b.thunk(
b.block([
b.stmt(

@ -16,12 +16,12 @@ import {
is_array,
object_keys
} from '../utils.js';
import { READONLY_SYMBOL } from './readonly.js';
/** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean, i: boolean }} Metadata */
/** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata }} StateObject */
export const STATE_SYMBOL = Symbol('$state');
export const READONLY_SYMBOL = Symbol('readonly');
const object_prototype = Object.prototype;
const array_prototype = Array.prototype;

@ -1,18 +1,16 @@
import { define_property, get_descriptor } from '../utils.js';
import { define_property } from '../utils.js';
import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
/**
* @template {Record<string | symbol, any>} T
* @typedef {T & { [READONLY_SYMBOL]: Proxy<T> }} StateObject
*/
export const READONLY_SYMBOL = Symbol('readonly');
const object_prototype = Object.prototype;
const array_prototype = Array.prototype;
const get_prototype_of = Object.getPrototypeOf;
const is_frozen = Object.isFrozen;
/**
* Expects a value that was wrapped with `proxy` and makes it readonly.
*
* @template {Record<string | symbol, any>} T
* @template {StateObject<T>} U
* @param {U} value
@ -26,25 +24,26 @@ export function readonly(value) {
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
STATE_SYMBOL in value && // TODO handle Map and Set as well
!(READONLY_SYMBOL in value)
) {
const prototype = get_prototype_of(value);
// TODO handle Map and Set as well
if (prototype === object_prototype || prototype === array_prototype) {
const proxy = new Proxy(value, handler);
define_property(value, READONLY_SYMBOL, { value: proxy, writable: false });
return proxy;
}
}
return value;
}
/** @returns {never} */
const readonly_error = () => {
throw new Error(`Props cannot be mutated, unless used with \`bind:\``);
/**
* @param {any} _
* @param {string} prop
* @returns {never}
*/
const readonly_error = (_, prop) => {
throw new Error(
`Props cannot be mutated, unless used with \`bind:\`. Use \`bind:prop-in-question={..}\` to make \`${prop}\` settable. Fallback values can never be mutated.`
);
};
/** @type {ProxyHandler<StateObject<any>>} */

@ -4,6 +4,7 @@ import { EMPTY_FUNC, run_all } from '../common.js';
import { get_descriptor, get_descriptors, is_array } from './utils.js';
import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES } from '../../constants.js';
import { readonly } from './proxy/readonly.js';
import { proxy } from './proxy/proxy.js';
export const SOURCE = 1;
export const DERIVED = 1 << 1;
@ -862,28 +863,28 @@ export function set_sync(signal, value) {
* Invokes a function and captures all signals that are read during the invocation,
* then invalidates them.
* @param {() => any} fn
* @returns {Set<import('./types.js').Signal>}
*/
export function invalidate_inner_signals(fn) {
const previous_is_signals_recorded = is_signals_recorded;
const previous_captured_signals = captured_signals;
var previous_is_signals_recorded = is_signals_recorded;
var previous_captured_signals = captured_signals;
is_signals_recorded = true;
captured_signals = new Set();
var captured = captured_signals;
var signal;
try {
untrack(fn);
} finally {
is_signals_recorded = previous_is_signals_recorded;
let signal;
if (is_signals_recorded) {
for (signal of captured_signals) {
previous_captured_signals.add(signal);
}
}
captured_signals = previous_captured_signals;
}
let signal;
for (signal of captured_signals) {
for (signal of captured) {
mutate(signal, null /* doesnt matter */);
}
return captured_signals;
}
/**
@ -1272,6 +1273,17 @@ export function pre_effect(init) {
);
}
/**
* This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the
* bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything.
*
* @param {() => void | (() => void)} init
* @returns {import('./types.js').EffectSignal}
*/
export function invalidate_effect(init) {
return internal_create_effect(PRE_EFFECT, init, true, current_block, true);
}
/**
* @param {() => void | (() => void)} init
* @returns {import('./types.js').EffectSignal}
@ -1412,7 +1424,7 @@ export function prop_source(props, key, flags, initial) {
value = (flags & PROPS_IS_LAZY_INITIAL) !== 0 ? initial() : initial;
if (DEV && runes) {
value = readonly(/** @type {any} */ (value));
value = readonly(proxy(/** @type {any} */ (value)));
}
}

@ -10,11 +10,7 @@ export const EMPTY_FUNC = () => {};
* @returns {value is PromiseLike<T>}
*/
export function is_promise(value) {
return (
!!value &&
(typeof value === 'object' || typeof value === 'function') &&
typeof (/** @type {any} */ (value).then) === 'function'
);
return typeof value?.then === 'function';
}
/** @param {Array<() => void>} arr */

@ -11,6 +11,7 @@ export {
user_effect,
render_effect,
pre_effect,
invalidate_effect,
flushSync,
bubble_event,
safe_equal,

@ -0,0 +1,32 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
html: `
<select>
<option value="a">A</option>
<option value="b">B</option>
</select>
selected: a
`,
test({ assert, target }) {
const select = target.querySelector('select');
ok(select);
const event = new window.Event('change');
select.value = 'b';
select.dispatchEvent(event);
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<select>
<option value="a">A</option>
<option value="b">B</option>
</select>
selected: b
`
);
}
});

@ -0,0 +1,11 @@
<script>
let entries = [{selected: 'a' }]
</script>
{#each entries as entry}
<select bind:value={entry.selected}>
<option value='a'>A</option>
<option value='b'>B</option>
</select>
selected: {entry.selected}
{/each}

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, 'a\n<select></select><button>change</button');
const [b1] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
'a\n<select></select>b\n<select></select><button>change</button'
);
}
});

@ -0,0 +1,13 @@
<script>
let entries = $state([{selected: 'a'}])
</script>
{#each entries as entry}
{entry.selected} <select bind:value={entry.selected}></select>
{/each}
<button
on:click={
() => entries = [{selected: 'a'}, {selected: 'b'}]
}
>change</button>

@ -0,0 +1,8 @@
<script>
/** @type {{ object: { count: number }}} */
let { object } = $props();
</script>
<button onclick={() => object = { count: object.count + 1 } }>
clicks: {object.count}
</button>

@ -0,0 +1,22 @@
import { test } from '../../test';
// Tests that readonly bails on setters/classes
export default test({
html: `<button>clicks: 0</button><button>clicks: 0</button>`,
compileOptions: {
dev: true
},
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
await btn1.click();
await btn2.click();
assert.htmlEqual(target.innerHTML, `<button>clicks: 1</button><button>clicks: 1</button>`);
await btn1.click();
await btn2.click();
assert.htmlEqual(target.innerHTML, `<button>clicks: 2</button><button>clicks: 2</button>`);
}
});

@ -0,0 +1,25 @@
<script>
import Counter from './Counter.svelte';
function createCounter() {
let count = $state(0)
return {
get count() {
return count;
},
set count(upd) {
count = upd
}
}
}
class CounterClass {
count = $state(0);
}
const counterSetter = createCounter();
const counterClass = new CounterClass();
</script>
<Counter object={counterSetter} />
<Counter object={counterClass} />

@ -14,5 +14,6 @@ export default test({
assert.htmlEqual(target.innerHTML, `<button>clicks: 0</button>`);
},
runtime_error: 'Props cannot be mutated, unless used with `bind:`'
runtime_error:
'Props cannot be mutated, unless used with `bind:`. Use `bind:prop-in-question={..}` to make `count` settable. Fallback values can never be mutated.'
});

@ -14,5 +14,6 @@ export default test({
assert.htmlEqual(target.innerHTML, `<button>clicks: 0</button>`);
},
runtime_error: 'Props cannot be mutated, unless used with `bind:`'
runtime_error:
'Props cannot be mutated, unless used with `bind:`. Use `bind:prop-in-question={..}` to make `count` settable. Fallback values can never be mutated.'
});

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

@ -0,0 +1,6 @@
<script>
let a = $state({b: 0});
</script>
<button onclick={() => a.b += 1}>a += 1</button>
<p>{JSON.stringify(a)} + {a.b}</p>
Loading…
Cancel
Save