Merge branch 'async' into async-global-coordination

pull/16197/head
Rich Harris 8 months ago
commit 8aaa5688e4

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ignore typescript abstract methods

@ -33,7 +33,7 @@ Now, a component that uses `<FancyInput>` can add the [`bind:`](bind) directive
<!-- prettier-ignore -->
```svelte
/// App.svelte
/// file: App.svelte
<script>
import FancyInput from './FancyInput.svelte';

@ -267,7 +267,7 @@ Elements with the `contenteditable` attribute support the following bindings:
<!-- for some reason puts the comment and html on same line -->
<!-- prettier-ignore -->
```svelte
<div contenteditable="true" bind:innerHTML={html} />
<div contenteditable="true" bind:innerHTML={html}></div>
```
## Dimensions
@ -307,7 +307,7 @@ To get a reference to a DOM node, use `bind:this`. The value will be `undefined`
});
</script>
<canvas bind:this={canvas} />
<canvas bind:this={canvas}></canvas>
```
Components also support `bind:this`, allowing you to interact with component instances programmatically.

@ -1,5 +1,31 @@
# svelte
## 5.20.2
### Patch Changes
- chore: remove unused `options.uid` in `render` ([#15302](https://github.com/sveltejs/svelte/pull/15302))
- fix: do not warn for `binding_property_non_reactive` if binding is a store in an each ([#15318](https://github.com/sveltejs/svelte/pull/15318))
- fix: prevent writable store value from becoming a proxy when reassigning using $-prefix ([#15283](https://github.com/sveltejs/svelte/pull/15283))
- fix: `muted` reactive without `bind` and select/autofocus attributes working with function calls ([#15326](https://github.com/sveltejs/svelte/pull/15326))
- fix: ensure input elements and elements with `dir` attribute are marked as non-static ([#15259](https://github.com/sveltejs/svelte/pull/15259))
- fix: fire delegated events on target even it was disabled in the meantime ([#15319](https://github.com/sveltejs/svelte/pull/15319))
## 5.20.1
### Patch Changes
- fix: ensure AST analysis on `svelte.js` modules succeeds ([#15297](https://github.com/sveltejs/svelte/pull/15297))
- fix: ignore typescript abstract methods ([#15267](https://github.com/sveltejs/svelte/pull/15267))
- fix: correctly ssr component in `svelte:head` with `$props.id()` or `css='injected'` ([#15291](https://github.com/sveltejs/svelte/pull/15291))
## 5.20.0
### Minor Changes

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

@ -262,10 +262,20 @@ export function analyze_module(ast, options) {
{
scope,
scopes,
// @ts-expect-error TODO
analysis,
// @ts-expect-error TODO
options
analysis: /** @type {ComponentAnalysis} */ (analysis),
derived_state: [],
// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
ast_type: /** @type {any} */ (null),
component_slots: new Set(),
expression: null,
function_depth: 0,
has_props_rune: false,
instance_scope: /** @type {any} */ (null),
options: /** @type {ValidatedCompileOptions} */ (options),
parent_element: null,
reactive_statement: null,
reactive_statements: new Map()
},
visitors
);

@ -45,6 +45,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>;
readonly is_instance: boolean;
readonly store_to_invalidate?: string;
/** Stuff that happens before the render effect(s) */
readonly init: Statement[];

@ -118,6 +118,7 @@ function build_assignment(operator, left, right, context) {
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'store_sub' &&
context.state.analysis.runes &&
should_proxy(right, context.state.scope) &&
is_non_coercive_operator(operator)

@ -143,7 +143,8 @@ export function EachBlock(node, context) {
const child_state = {
...context.state,
transform: { ...context.state.transform }
transform: { ...context.state.transform },
store_to_invalidate
};
/** The state used when generating the key function, if necessary */

@ -29,7 +29,8 @@ import {
build_render_statement,
build_template_chunk,
build_update_assignment,
get_expression_id
get_expression_id,
memoize_expression
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
@ -536,20 +537,30 @@ function build_element_attribute_update_assignment(
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
const is_autofocus = name === 'autofocus';
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.is_async
? get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value)
? // if it's autofocus we will not add this to a template effect so we don't want to get the expression id
// but separately memoize the expression
is_autofocus
? memoize_expression(state, value)
: get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value)
: value
);
if (name === 'autofocus') {
if (is_autofocus) {
state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
return false;
}
// Special case for Firefox who needs it set as a property in order to work
if (name === 'muted') {
state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
if (!has_state) {
state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
return false;
}
state.update.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value)));
return false;
}
@ -666,9 +677,17 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state;
const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
// the inner options could still change, so we need to always treat it as reactive
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.is_async
? get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value)
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
is_select_with_value
? memoize_expression(context.state, value)
: get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value)
: value
);
@ -682,11 +701,6 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
)
);
const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change,
// the inner options could still change, so we need to always treat it as reactive
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const update = b.stmt(
is_select_with_value
? b.sequence([

@ -144,6 +144,17 @@ function is_static_element(node, state) {
return false;
}
if (attribute.name === 'dir') {
return false;
}
if (
['input', 'textarea'].includes(node.name) &&
['value', 'checked'].includes(attribute.name)
) {
return false;
}
if (node.name === 'option' && attribute.name === 'value') {
return false;
}

@ -326,12 +326,16 @@ export function validate_binding(state, binding, expression) {
const loc = locator(binding.start);
const obj = /** @type {Expression} */ (expression.object);
state.init.push(
b.stmt(
b.call(
'$.validate_binding',
b.literal(state.analysis.source.slice(binding.start, binding.end)),
b.thunk(/** @type {Expression} */ (expression.object)),
b.thunk(
state.store_to_invalidate ? b.sequence([b.call('$.mark_store_binding'), obj]) : obj
),
b.thunk(
/** @type {Expression} */ (
expression.computed

@ -237,7 +237,13 @@ export function handle_event_propagation(event) {
// @ts-expect-error
var delegated = current_target['__' + event_name];
if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
if (
delegated !== undefined &&
(!(/** @type {any} */ (current_target).disabled) ||
// DOM could've been updated already by the time this is reached, so we check this as well
// -> the target could not have been disabled because it emits the event in the first place
event.target === current_target)
) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(current_target, [event, ...data]);

@ -252,6 +252,10 @@ export function append(anchor, dom) {
let uid = 1;
export function reset_props_id() {
uid = 1;
}
/**
* Create (or hydrate) an unique UID for the component instance.
*/

@ -34,7 +34,9 @@ export function copy_payload({ out, css, head, uid }) {
css: new Set(css),
head: {
title: head.title,
out: head.out
out: head.out,
css: new Set(head.css),
uid: head.uid
},
uid
};
@ -95,16 +97,17 @@ function props_id_generator() {
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>, uid?: () => string }} [options]
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
const uid = props_id_generator();
/** @type {Payload} */
const payload = {
out: '',
css: new Set(),
head: { title: '', out: '' },
uid: options.uid ?? props_id_generator()
head: { title: '', out: '', css: new Set(), uid },
uid
};
const prev_on_destroy = on_destroy;

@ -17,6 +17,8 @@ export interface Payload {
head: {
title: string;
out: string;
uid: () => string;
css: Set<{ hash: string; code: string }>;
};
/** Function that generates a unique ID */
uid: () => string;

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

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
test(assert, target) {
const p = target.querySelector('p');
assert.equal(p?.dir, 'rtl');
}
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
test(assert, target) {
const input = target.querySelector('input');
assert.equal(input?.checked, true);
}
});

@ -25,7 +25,7 @@ const { test, run } = suite<PreprocessTest>(async (config, cwd) => {
fs.writeFileSync(`${cwd}/_actual.html.map`, JSON.stringify(result.map, null, 2));
}
expect(result.code).toMatchFileSnapshot(`${cwd}/output.svelte`);
await expect(result.code).toMatchFileSnapshot(`${cwd}/output.svelte`);
expect(result.dependencies).toEqual(config.dependencies || []);

@ -11,6 +11,7 @@ import { setup_html_equal } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
import { reset_props_id } from '../../src/internal/client/dom/template.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
@ -348,6 +349,7 @@ async function run_test_variant(
if (runes) {
props = proxy({ ...(config.props || {}) });
reset_props_id();
if (manual_hydrate) {
hydrate_fn = () => {
instance = hydrate(mod.default, {

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, errors }) {
assert.deepEqual(errors, []);
}
});

@ -0,0 +1,6 @@
<svelte:options runes />
<script>
function test(){}
</script>
<input autofocus={test()} />

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
async test({ assert, warnings }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,10 @@
<svelte:options runes />
<script>
import { writable } from 'svelte/store';
const array = writable([{ name: "" }]);
</script>
{#each $array as item}
<div><input bind:value={item.name} /></div>
{/each}

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
ok(btn);
flushSync(() => {
btn.click();
});
assert.deepEqual(logs, [true]);
}
});

@ -0,0 +1,13 @@
<script>
let muted = $state(false);
function volume_change(node){
node.addEventListener("volumechange", () => {
console.log(node.muted);
});
}
</script>
<audio use:volume_change {muted}></audio>
<button onclick={() => (muted = !muted)}></button>

@ -43,8 +43,6 @@ export default test({
`
);
} else {
// `c6` because this runs after the `dom` tests
// (slightly brittle but good enough for now)
assert.htmlEqual(
target.innerHTML,
`
@ -53,7 +51,7 @@ export default test({
<p>s2</p>
<p>s3</p>
<p>s4</p>
<p>c6</p>
<p>c1</p>
`
);
}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, errors }) {
assert.deepEqual(errors, []);
}
});

@ -0,0 +1,6 @@
<svelte:options runes />
<script>
function test(){}
</script>
<select value={test()}></select>

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ target, assert }) {
assert.htmlEqual(target.innerHTML, `<p>bar</p>`);
}
});

@ -0,0 +1,11 @@
<script>
import { writable } from "svelte/store";
const obj = writable({ name: 'foo' });
$obj = { name: 'bar' };
const clone = structuredClone($obj);
</script>
<p>{clone.name}</p>

@ -0,0 +1,5 @@
<script>
let id = $props.id();
</script>
<meta name="id" content={id} />

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

@ -0,0 +1,8 @@
<script>
import HeadNested from './HeadNested.svelte';
</script>
<svelte:head>
<HeadNested />
</svelte:head>
Loading…
Cancel
Save