pull/14307/head
Dominic Gannaway 2 years ago
commit e3120e8931

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: disallow invalid attributes for `<svelte:window>` and `<svelte:document>`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure props passed to components via mount are updateable

@ -412,6 +412,12 @@ Expected whitespace
`$host()` can only be used inside custom element component instances
```
### illegal_element_attribute
```
`<%name%>` does not support non-event attributes or spread attributes
```
### import_svelte_internal_forbidden
```

@ -172,6 +172,10 @@
> Expected whitespace
## illegal_element_attribute
> `<%name%>` does not support non-event attributes or spread attributes
## invalid_bind_directive
> Bind directive getter/setter values (`bind:thing={ getter, setter }`) must be two JavaScript expressions separated by a single comma and enclosed in curly braces

@ -993,6 +993,16 @@ export function invalid_bind_directive(node) {
e(node, "invalid_bind_directive", "Bind directive getter/setter values (`bind:thing={ getter, setter }`) must be two JavaScript expressions separated by a single comma and enclosed in curly braces");
}
/**
* `<%name%>` does not support non-event attributes or spread attributes
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function illegal_element_attribute(node, name) {
e(node, "illegal_element_attribute", `\`<${name}>\` does not support non-event attributes or spread attributes`);
}
/**
* %message%
* @param {null | number | NodeLike} node

@ -1,6 +1,8 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
/**
* @param {AST.SvelteDocument} node
@ -8,5 +10,15 @@ import { disallow_children } from './shared/special-element.js';
*/
export function SvelteDocument(node, context) {
disallow_children(node);
for (const attribute of node.attributes) {
if (
attribute.type === 'SpreadAttribute' ||
(attribute.type === 'Attribute' && !is_event_attribute(attribute))
) {
e.illegal_element_attribute(attribute, 'svelte:document');
}
}
context.next();
}

@ -1,6 +1,8 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
/**
* @param {AST.SvelteWindow} node
@ -8,5 +10,15 @@ import { disallow_children } from './shared/special-element.js';
*/
export function SvelteWindow(node, context) {
disallow_children(node);
for (const attribute of node.attributes) {
if (
attribute.type === 'SpreadAttribute' ||
(attribute.type === 'Attribute' && !is_event_attribute(attribute))
) {
e.illegal_element_attribute(attribute, 'svelte:window');
}
}
context.next();
}

@ -22,4 +22,5 @@ export const EFFECT_HAS_DERIVED = 1 << 19;
export const STATE_SYMBOL = Symbol('$state');
export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');

@ -20,7 +20,13 @@ import {
} from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
import { BRANCH_EFFECT, LEGACY_DERIVED_PROP, ROOT_EFFECT } from '../constants.js';
import {
BRANCH_EFFECT,
LEGACY_DERIVED_PROP,
LEGACY_PROPS,
ROOT_EFFECT,
STATE_SYMBOL
} from '../constants.js';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@ -209,6 +215,9 @@ const spread_props_handler = {
}
},
has(target, key) {
// To prevent a false positive `is_entry_props` in the `prop` function
if (key === STATE_SYMBOL || key === LEGACY_PROPS) return false;
for (let p of target.props) {
if (is_function(p)) p = p();
if (p != null && key in p) return true;
@ -282,7 +291,14 @@ export function prop(props, key, flags, fallback) {
} else {
prop_value = /** @type {V} */ (props[key]);
}
var setter = get_descriptor(props, key)?.set;
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
var setter =
get_descriptor(props, key)?.set ??
(is_entry_props && bindable && key in props ? (v) => (props[key] = v) : undefined);
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;

@ -1,4 +1,4 @@
import { dev_current_component_function, untrack } from './runtime.js';
import { dev_current_component_function } from './runtime.js';
import { get_descriptor, is_array } from '../shared/utils.js';
import * as e from './errors.js';
import { FILENAME } from '../../constants.js';
@ -6,15 +6,6 @@ import { render_effect } from './reactivity/effects.js';
import * as w from './warnings.js';
import { capture_store_binding } from './reactivity/store.js';
/** regex of all html void element names */
const void_element_names =
/^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
/** @param {string} tag */
function is_void(tag) {
return void_element_names.test(tag) || tag.toLowerCase() === '!doctype';
}
/**
* @param {() => any} collection
* @param {(item: any, index: number) => string} key_fn

@ -1,5 +1,5 @@
/** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */
import { DIRTY, MAYBE_DIRTY } from '../internal/client/constants.js';
import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.js';
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
@ -89,7 +89,7 @@ class Svelte4Component {
};
// Replicate coarse-grained props through a proxy that has a version source for
// each property, which is increment on updates to the property itself. Do not
// each property, which is incremented on updates to the property itself. Do not
// use our $state proxy because that one has fine-grained reactivity.
const props = new Proxy(
{ ...(options.props || {}), $$events: {} },
@ -98,6 +98,9 @@ class Svelte4Component {
return get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop)));
},
has(target, prop) {
// Necessary to not throw "invalid binding" validation errors on the component side
if (prop === LEGACY_PROPS) return true;
get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop)));
return Reflect.has(target, prop);
},

@ -0,0 +1,47 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
assert.htmlEqual(
target.innerHTML,
// The buz fallback does not propagate back up
`
<button>reset</button> foo baz
<div><button>update</button> foo bar baz buz</div>
<div><button>update</button> foo bar baz buz</div>
`
);
const [btn1, btn2, btn3] = target.querySelectorAll('button');
btn2.click();
btn3.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
// bar is not set in the parent because it's a readonly property
// baz is not set in the parent because while it's a bindable property,
// it wasn't set initially so it's treated as a readonly proeprty
`
<button>reset</button> foo 3
<div><button>update</button> 1 2 3 4</div>
<div><button>update</button> 1 2 3 4</div>
`
);
btn1.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
// Because foo is a readonly property, component.svelte diverges locally from it,
// and the passed in property keeps the initial value of foo. This is why it stays
// at 1, because foo is not updated to a different value.
`
<button>reset</button> foo bar baz buz
<div><button>update</button> 1 bar baz buz</div>
<div><button>update</button> 1 bar baz buz</div>
`
);
}
});

@ -0,0 +1,17 @@
<script>
let { foo, bar = 'bar', baz = $bindable(), buz = $bindable('buz') } = $props();
</script>
<button
onclick={() => {
foo = '1';
bar = '2';
baz = '3';
buz = '4';
}}>update</button
>
{foo}
{bar}
{baz}
{buz}

@ -0,0 +1,30 @@
<script>
import { createClassComponent } from 'svelte/legacy';
import Component from './component.svelte';
import { mount, onMount } from 'svelte';
let div1, div2;
let legacy;
const props = $state({ foo: 'foo', baz: 'baz' });
onMount(() => {
legacy = createClassComponent({
component: Component,
target: div1,
props: { foo: 'foo', baz: 'baz' }
});
mount(Component, { target: div2, props });
});
</script>
<button
onclick={() => {
legacy.$set({ foo: 'foo', bar: 'bar', baz: 'baz', buz: 'buz' });
props.foo = 'foo';
props.bar = 'bar';
props.baz = 'baz';
props.buz = 'buz';
}}>reset</button
> {props.foo} {props.bar} {props.baz} {props.buz}
<div bind:this={div1}></div>
<div bind:this={div2}></div>

@ -0,0 +1,14 @@
[
{
"code": "illegal_element_attribute",
"message": "`<svelte:document>` does not support non-event attributes or spread attributes",
"start": {
"line": 4,
"column": 17
},
"end": {
"line": 4,
"column": 23
}
}
]

@ -0,0 +1,4 @@
<script>
let a = {};
</script>
<svelte:document {...a}></svelte:document>

@ -0,0 +1,14 @@
[
{
"code": "illegal_element_attribute",
"message": "`<svelte:window>` does not support non-event attributes or spread attributes",
"start": {
"line": 4,
"column": 15
},
"end": {
"line": 4,
"column": 21
}
}
]

@ -0,0 +1,4 @@
<script>
let a = {};
</script>
<svelte:window {...a}></svelte:window>
Loading…
Cancel
Save