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> { export interface DOMAttributes<T extends EventTarget> {
// Implicit children prop every element has // Implicit children prop every element has
// Add this here so that libraries doing `$props<HTMLButtonAttributes>()` don't need a separate interface // 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 // Clipboard Events
'on:copy'?: ClipboardEventHandler<T> | undefined | null; '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); 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]) { for (const scope of [module.scope, instance.scope]) {
outer: for (const [name, binding] of scope.declarations) { 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) { for (const { path } of binding.references) {
if (path[0].type !== 'Fragment') continue; if (path[0].type !== 'Fragment') continue;
for (let i = 1; i < path.length; i += 1) { for (let i = 1; i < path.length; i += 1) {

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

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

@ -16,12 +16,12 @@ import {
is_array, is_array,
object_keys object_keys
} from '../utils.js'; } 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 {{ 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 */ /** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata }} StateObject */
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const READONLY_SYMBOL = Symbol('readonly');
const object_prototype = Object.prototype; const object_prototype = Object.prototype;
const array_prototype = Array.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 * @template {Record<string | symbol, any>} T
* @typedef {T & { [READONLY_SYMBOL]: Proxy<T> }} StateObject * @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; const is_frozen = Object.isFrozen;
/** /**
* Expects a value that was wrapped with `proxy` and makes it readonly.
*
* @template {Record<string | symbol, any>} T * @template {Record<string | symbol, any>} T
* @template {StateObject<T>} U * @template {StateObject<T>} U
* @param {U} value * @param {U} value
@ -26,25 +24,26 @@ export function readonly(value) {
typeof value === 'object' && typeof value === 'object' &&
value != null && value != null &&
!is_frozen(value) && !is_frozen(value) &&
STATE_SYMBOL in value && // TODO handle Map and Set as well
!(READONLY_SYMBOL in value) !(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); const proxy = new Proxy(value, handler);
define_property(value, READONLY_SYMBOL, { value: proxy, writable: false }); define_property(value, READONLY_SYMBOL, { value: proxy, writable: false });
return proxy; return proxy;
} }
}
return value; return value;
} }
/** @returns {never} */ /**
const readonly_error = () => { * @param {any} _
throw new Error(`Props cannot be mutated, unless used with \`bind:\``); * @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>>} */ /** @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 { get_descriptor, get_descriptors, is_array } from './utils.js';
import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES } from '../../constants.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES } from '../../constants.js';
import { readonly } from './proxy/readonly.js'; import { readonly } from './proxy/readonly.js';
import { proxy } from './proxy/proxy.js';
export const SOURCE = 1; export const SOURCE = 1;
export const DERIVED = 1 << 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, * Invokes a function and captures all signals that are read during the invocation,
* then invalidates them. * then invalidates them.
* @param {() => any} fn * @param {() => any} fn
* @returns {Set<import('./types.js').Signal>}
*/ */
export function invalidate_inner_signals(fn) { export function invalidate_inner_signals(fn) {
const previous_is_signals_recorded = is_signals_recorded; var previous_is_signals_recorded = is_signals_recorded;
const previous_captured_signals = captured_signals; var previous_captured_signals = captured_signals;
is_signals_recorded = true; is_signals_recorded = true;
captured_signals = new Set(); captured_signals = new Set();
var captured = captured_signals;
var signal;
try { try {
untrack(fn); untrack(fn);
} finally { } finally {
is_signals_recorded = previous_is_signals_recorded; is_signals_recorded = previous_is_signals_recorded;
let signal; if (is_signals_recorded) {
for (signal of captured_signals) { for (signal of captured_signals) {
previous_captured_signals.add(signal); previous_captured_signals.add(signal);
} }
}
captured_signals = previous_captured_signals; captured_signals = previous_captured_signals;
} }
let signal; for (signal of captured) {
for (signal of captured_signals) {
mutate(signal, null /* doesnt matter */); 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 * @param {() => void | (() => void)} init
* @returns {import('./types.js').EffectSignal} * @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; value = (flags & PROPS_IS_LAZY_INITIAL) !== 0 ? initial() : initial;
if (DEV && runes) { 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>} * @returns {value is PromiseLike<T>}
*/ */
export function is_promise(value) { export function is_promise(value) {
return ( return typeof value?.then === 'function';
!!value &&
(typeof value === 'object' || typeof value === 'function') &&
typeof (/** @type {any} */ (value).then) === 'function'
);
} }
/** @param {Array<() => void>} arr */ /** @param {Array<() => void>} arr */

@ -11,6 +11,7 @@ export {
user_effect, user_effect,
render_effect, render_effect,
pre_effect, pre_effect,
invalidate_effect,
flushSync, flushSync,
bubble_event, bubble_event,
safe_equal, 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>`); 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>`); 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