Merge branch 'main' into remove-binding-expression

pull/12530/head
Rich Harris 2 years ago
commit 3ca04c9e43

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: tidy up dynamic event handler generated code

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: dynamic event delegation for stateful call expressions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure $state.snapshot correctly clones Date objects

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve validation error that occurs when using `{@render ...}` to render default slotted content

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove runtime validation of components/snippets, rely on types instead

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: bail-out of hydrating head if no anchor is found

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: add warning for invalid render function of createRawSnippet

@ -185,6 +185,7 @@
"fluffy-dolls-share",
"fluffy-humans-worry",
"fluffy-ravens-juggle",
"forty-bikes-buy",
"forty-comics-invent",
"forty-dogs-divide",
"forty-dolls-wave",
@ -193,6 +194,7 @@
"four-balloons-beam",
"four-flies-hammer",
"four-mice-hammer",
"four-papayas-turn",
"four-pugs-listen",
"fresh-beds-wash",
"fresh-dots-destroy",
@ -383,10 +385,12 @@
"nasty-mayflies-smoke",
"nasty-yaks-peel",
"neat-boats-shake",
"neat-boxes-chew",
"neat-dingos-clap",
"neat-files-rescue",
"neat-jokes-beam",
"nervous-berries-boil",
"nervous-dolphins-allow",
"nervous-ducks-repeat",
"nervous-spoons-relax",
"nervous-turkeys-end",
@ -562,6 +566,7 @@
"small-chefs-sing",
"small-owls-remain",
"small-papayas-laugh",
"small-planets-destroy",
"small-sheep-type",
"small-spiders-fail",
"smart-cherries-leave",
@ -643,6 +648,7 @@
"ten-worms-reflect",
"tender-lemons-judge",
"tender-rocks-walk",
"tender-suns-love",
"thick-cycles-rule",
"thick-pans-tell",
"thick-shirts-deliver",
@ -693,6 +699,7 @@
"two-dogs-accept",
"two-dragons-yell",
"two-falcons-buy",
"two-keys-watch",
"unlucky-boxes-obey",
"unlucky-steaks-warn",
"unlucky-trees-lick",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly update store values

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: update original source in HMR update

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly set filename on HMR wrappers

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: only emit binding_property_non_reactive warning in runes mode

@ -72,13 +72,31 @@ If you're using TypeScript, you can declare the prop types:
```svelte
<script lang="ts">
interface Props {
a: number;
b: boolean;
c: string;
required: string;
optional?: number;
[key: string]: unknown;
}
let { a, b, c, ...everythingElse }: Props = $props();
let { required, optional, ...everythingElse }: Props = $props();
</script>
```
If you're using JavaScript, you can declare the prop types using JSDoc:
```svelte
<script>
/** @type {{ x: string }} */
let { x } = $props();
// or use @typedef if you want to document the properties:
/**
* @typedef {Object} MyProps
* @property {string} y Some documentation
*/
/** @type {MyProps} */
let { y } = $props();
</script>
```

@ -1,5 +1,31 @@
# svelte
## 5.0.0-next.195
### Patch Changes
- fix: update original source in HMR update ([#12547](https://github.com/sveltejs/svelte/pull/12547))
## 5.0.0-next.194
### Patch Changes
- fix: bail-out of hydrating head if no anchor is found ([#12541](https://github.com/sveltejs/svelte/pull/12541))
- chore: add warning for invalid render function of createRawSnippet ([#12535](https://github.com/sveltejs/svelte/pull/12535))
- fix: correctly set filename on HMR wrappers ([#12543](https://github.com/sveltejs/svelte/pull/12543))
- fix: only emit binding_property_non_reactive warning in runes mode ([#12544](https://github.com/sveltejs/svelte/pull/12544))
## 5.0.0-next.193
### Patch Changes
- fix: improve validation error that occurs when using `{@render ...}` to render default slotted content ([#12521](https://github.com/sveltejs/svelte/pull/12521))
- fix: reset hydrate node after `hydrate(...)` ([#12512](https://github.com/sveltejs/svelte/pull/12512))
## 5.0.0-next.192
### Patch Changes

@ -20,6 +20,10 @@
> Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location%
## invalid_raw_snippet_render
> The `render` function passed to `createRawSnippet` should return HTML for a single element
## lifecycle_double_unmount
> Tried to unmount a component that was not mounted

@ -1,14 +1,10 @@
## lifecycle_outside_component
> `%name%(...)` can only be used during component initialisation
## invalid_default_snippet
## render_tag_invalid_argument
> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
> The argument to `{@render ...}` must be a snippet function, not a component or a slot with a `let:` directive or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`
## snippet_used_as_component
## lifecycle_outside_component
> A snippet must be rendered with `{@render ...}`
> `%name%(...)` can only be used during component initialisation
## store_invalid_shape

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

@ -418,8 +418,19 @@ export function client_component(source, analysis, options) {
);
if (options.hmr) {
const id = b.id(analysis.name);
const HMR = b.id('$.HMR');
const existing = b.member(id, HMR, true);
const incoming = b.member(b.id('module.default'), HMR, true);
const accept_fn_body = [
b.stmt(b.call('$.set', b.id('s'), b.member(b.id('module.default'), b.id('$.ORIGINAL'), true)))
b.stmt(
b.assignment('=', b.member(incoming, b.id('source')), b.member(existing, b.id('source')))
),
b.stmt(
b.call('$.set', b.member(existing, b.id('source')), b.member(incoming, b.id('original')))
)
];
if (analysis.css.hash) {
@ -439,20 +450,10 @@ export function client_component(source, analysis, options) {
}
const hmr = b.block([
b.const(b.id('s'), b.call('$.source', b.id(analysis.name))),
b.const(b.id('filename'), b.member(b.id(analysis.name), b.id('filename'))),
b.const(b.id('$$original'), b.id(analysis.name)),
b.stmt(b.assignment('=', b.id(analysis.name), b.call('$.hmr', b.id('s')))),
b.stmt(b.assignment('=', b.member(b.id(analysis.name), b.id('filename')), b.id('filename'))),
// Assign the original component to the wrapper so we can use it on hot reload patching,
// else we would call the HMR function two times
b.stmt(
b.assignment(
'=',
b.member(b.id(analysis.name), b.id('$.ORIGINAL'), true),
b.id('$$original')
)
b.assignment('=', id, b.call('$.hmr', id, b.thunk(b.member(existing, b.id('source')))))
),
b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body))))
]);

@ -1,6 +1,5 @@
/** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */
/** @import { BindDirective } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types' */
import {
extract_identifiers,
extract_paths,
@ -780,7 +779,11 @@ function serialize_inline_component(node, component_name, context, anchor = cont
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (expression.type === 'MemberExpression' && context.state.options.dev) {
if (
expression.type === 'MemberExpression' &&
context.state.options.dev &&
context.state.analysis.runes
) {
context.state.init.push(serialize_validate_binding(context.state, attribute, expression));
}
@ -884,23 +887,27 @@ function serialize_inline_component(node, component_name, context, anchor = cont
])
);
if (
slot_name === 'default' &&
!has_children_prop &&
lets.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
push_prop(
b.init(
'children',
context.state.options.dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn)
: slot_fn
)
);
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
serialized_slots.push(b.init(slot_name, b.true));
if (slot_name === 'default' && !has_children_prop) {
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
// create `children` prop...
push_prop(
b.init(
'children',
context.state.options.dev
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn)
: slot_fn
)
);
// and `$$slots.default: true` so that `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true));
} else {
// create `$$slots.default`...
serialized_slots.push(b.init(slot_name, slot_fn));
// and a `children` prop that errors
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
}
} else {
serialized_slots.push(b.init(slot_name, slot_fn));
}
@ -925,13 +932,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
/** @param {Expression} node_id */
let fn = (node_id) => {
return b.call(
context.state.options.dev
? b.call('$.validate_component', b.id(component_name))
: component_name,
node_id,
props_expression
);
return b.call(component_name, node_id, props_expression);
};
if (bind_this !== null) {
@ -1136,9 +1137,12 @@ function serialize_event_handler(node, { state, visit }) {
null,
[b.rest(b.id('$$args'))],
b.block([
b.const('$$callback', /** @type {Expression} */ (visit(handler))),
b.return(
b.call(b.member(b.id('$$callback'), b.id('apply'), false, true), b.this, b.id('$$args'))
b.call(
b.member(/** @type {Expression} */ (visit(handler)), b.id('apply'), false, true),
b.this,
b.id('$$args')
)
)
])
);
@ -1161,7 +1165,11 @@ function serialize_event_handler(node, { state, visit }) {
} else {
handler = /** @type {Expression} */ (visit(handler));
}
} else if (handler.type === 'ConditionalExpression' || handler.type === 'LogicalExpression') {
} else if (
handler.type === 'CallExpression' ||
handler.type === 'ConditionalExpression' ||
handler.type === 'LogicalExpression'
) {
handler = dynamic_handler();
} else {
handler = /** @type {Expression} */ (visit(handler));
@ -1869,15 +1877,6 @@ export const template_visitors = {
}
let snippet_function = /** @type {Expression} */ (context.visit(callee));
if (context.state.options.dev) {
snippet_function = b.call(
'$.validate_snippet',
snippet_function,
args.length && callee.type === 'Identifier' && callee.name === 'children'
? b.id('$$props')
: undefined
);
}
if (node.metadata.dynamic) {
context.state.init.push(
@ -2369,7 +2368,6 @@ export const template_visitors = {
EachBlock(node, context) {
const each_node_meta = node.metadata;
const collection = /** @type {Expression} */ (context.visit(node.expression));
let each_item_is_reactive = true;
if (!each_node_meta.is_controlled) {
context.state.template.push('<!>');
@ -2379,36 +2377,29 @@ export const template_visitors = {
context.state.init.push(b.const(each_node_meta.array_name, b.thunk(collection)));
}
// The runtime needs to know what kind of each block this is in order to optimize for the
// key === item (we avoid extra allocations). In that case, the item doesn't need to be reactive.
// We can guarantee this by knowing that in order for the item of the each block to change, they
// would need to mutate the key/item directly in the array. Given that in runes mode we use ===
// equality, we can apply a fast-path (as long as the index isn't reactive).
let each_type = 0;
let flags = 0;
if (
node.key &&
(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index)
) {
each_type |= EACH_KEYED;
// If there's a destructuring, then we likely need the generated $$index
if (node.index || node.context.type !== 'Identifier') {
each_type |= EACH_INDEX_REACTIVE;
flags |= EACH_KEYED;
if (node.index) {
flags |= EACH_INDEX_REACTIVE;
}
if (
context.state.analysis.runes &&
// In runes mode, if key === item, we don't need to wrap the item in a source
const key_is_item =
node.key.type === 'Identifier' &&
node.context.type === 'Identifier' &&
node.context.name === node.key.name &&
(each_type & EACH_INDEX_REACTIVE) === 0
) {
// Fast-path for when the key === item
each_item_is_reactive = false;
} else {
each_type |= EACH_ITEM_REACTIVE;
node.context.name === node.key.name;
if (!context.state.analysis.runes || !key_is_item) {
flags |= EACH_ITEM_REACTIVE;
}
} else {
each_type |= EACH_ITEM_REACTIVE;
flags |= EACH_ITEM_REACTIVE;
}
// Since `animate:` can only appear on elements that are the sole child of a keyed each block,
@ -2421,15 +2412,15 @@ export const template_visitors = {
return child.attributes.some((attr) => attr.type === 'AnimateDirective');
})
) {
each_type |= EACH_IS_ANIMATED;
flags |= EACH_IS_ANIMATED;
}
if (each_node_meta.is_controlled) {
each_type |= EACH_IS_CONTROLLED;
flags |= EACH_IS_CONTROLLED;
}
if (context.state.analysis.runes) {
each_type |= EACH_IS_STRICT_EQUALS;
flags |= EACH_IS_STRICT_EQUALS;
}
// If the array is a store expression, we need to invalidate it when the array is changed.
@ -2454,10 +2445,12 @@ export const template_visitors = {
);
return [array, ...transitive_dependencies];
});
if (each_node_meta.array_name) {
indirect_dependencies.push(b.call(each_node_meta.array_name));
} else {
indirect_dependencies.push(collection);
const transitive_dependencies = serialize_transitive_dependencies(
each_node_meta.references,
context
@ -2476,6 +2469,7 @@ export const template_visitors = {
// into separate expressions, at which point this is called again with an identifier or member expression
return serialize_set_binding(assignment, context, () => assignment);
}
const left = object(assignment.left);
const value = get_assignment_value(assignment, context);
const invalidate = b.call(
@ -2516,10 +2510,12 @@ export const template_visitors = {
const item_with_loc = with_loc(item, id);
return b.call('$.unwrap', item_with_loc);
};
if (node.index) {
const index_binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(node.index)
);
index_binding.expression = (id) => {
const index_with_loc = with_loc(index, id);
return b.call('$.unwrap', index_with_loc);
@ -2582,7 +2578,7 @@ export const template_visitors = {
declarations.push(b.let(node.index, index));
}
if (context.state.options.dev && (each_type & EACH_KEYED) !== 0) {
if (context.state.options.dev && (flags & EACH_KEYED) !== 0) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
@ -2591,7 +2587,7 @@ export const template_visitors = {
/** @type {Expression[]} */
const args = [
context.state.node,
b.literal(each_type),
b.literal(flags),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(block.body)))
@ -2838,7 +2834,11 @@ export const template_visitors = {
const { state, path, visit } = context;
const expression = node.expression;
if (expression.type === 'MemberExpression' && context.state.options.dev) {
if (
expression.type === 'MemberExpression' &&
context.state.options.dev &&
context.state.analysis.runes
) {
context.state.init.push(
serialize_validate_binding(
context.state,

@ -968,25 +968,22 @@ function serialize_inline_component(node, expression, context) {
])
);
if (
slot_name === 'default' &&
!has_children_prop &&
lets.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
push_prop(
b.prop(
'init',
b.id('children'),
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
)
);
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
serialized_slots.push(b.init('default', b.true));
if (slot_name === 'default' && !has_children_prop) {
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
// create `children` prop...
push_prop(b.prop('init', b.id('children'), slot_fn));
// and `$$slots.default: true` so that `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true));
} else {
// create `$$slots.default`...
serialized_slots.push(b.init(slot_name, slot_fn));
// and a `children` prop that errors
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
}
} else {
const slot = b.prop('init', b.literal(slot_name), slot_fn);
serialized_slots.push(slot);
serialized_slots.push(b.init(slot_name, slot_fn));
}
}
@ -1006,7 +1003,7 @@ function serialize_inline_component(node, expression, context) {
/** @type {import('estree').Statement} */
let statement = b.stmt(
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
context.state.options.dev ? b.call('$.validate_component', expression) : expression,
expression,
b.id('$$payload'),
props_expression
)
@ -1214,16 +1211,7 @@ const template_visitors = {
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const expression = /** @type {import('estree').Expression} */ (context.visit(callee));
const snippet_function = context.state.options.dev
? b.call(
'$.validate_snippet',
expression,
raw_args.length && callee.type === 'Identifier' && callee.name === 'children'
? b.id('$$props')
: undefined
)
: expression;
const snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
const snippet_args = raw_args.map((arg) => {
return /** @type {import('estree').Expression} */ (context.visit(arg));
@ -1506,10 +1494,6 @@ const template_visitors = {
fn.___snippet = true;
// TODO hoist where possible
context.state.init.push(fn);
if (context.state.options.dev) {
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
}
},
Component(node, context) {
serialize_inline_component(node, b.id(node.name), context);

@ -32,7 +32,7 @@ export const UNINITIALIZED = Symbol();
// Dev-time component properties
export const FILENAME = Symbol('filename');
export const ORIGINAL = Symbol('original');
export const HMR = Symbol('hmr');
/** List of elements that require raw contents and should not have SSR comments put in them */
export const RawTextElements = ['textarea', 'script', 'style', 'title'];

@ -1,5 +1,6 @@
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are).
import type { Getters } from '#shared';
import './ambient.js';
/**
@ -104,6 +105,15 @@ export class SvelteComponent<
$set(props: Partial<Props>): void;
}
declare const brand: unique symbol;
type Brand<B> = { [brand]: B };
type Branded<T, B> = T & Brand<B>;
/**
* Internal implementation details that vary between environments
*/
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;
/**
* Can be used to create strongly typed Svelte components.
*
@ -136,7 +146,8 @@ export interface Component<
* @param props The props passed to the component.
*/
(
internal: unknown,
this: void,
internals: ComponentInternals,
props: Props
): {
/**

@ -1,19 +1,22 @@
/** @import { Source, Effect } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
import { EFFECT_TRANSPARENT } from '../constants.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
* @param {Source<Component>} source
* @param {Component} original
* @param {() => Source<Component>} get_source
*/
export function hmr(source) {
export function hmr(original, get_source) {
/**
* @param {Comment} anchor
* @param {any} props
*/
return function (anchor, props) {
function wrapper(anchor, props) {
let instance = {};
/** @type {Effect} */
@ -22,6 +25,7 @@ export function hmr(source) {
let ran = false;
block(() => {
const source = get_source();
const component = get(source);
if (effect) {
@ -50,5 +54,20 @@ export function hmr(source) {
ran = true;
return instance;
}
// @ts-expect-error
wrapper[FILENAME] = original[FILENAME];
// @ts-expect-error
wrapper[HMR] = {
// When we accept an update, we set the original source to the new component
original,
// The `get_source` parameter reads `wrapper[HMR].source`, but in the `accept`
// function we always replace it with `previous[HMR].source`, which in practice
// means we only ever update the original
source: source(original)
};
return wrapper;
}

@ -1,7 +1,6 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import {
@ -11,6 +10,8 @@ import {
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@ -53,7 +54,7 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
return add_snippet_symbol((/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
@ -62,7 +63,7 @@ export function wrap_snippet(component, fn) {
} finally {
set_dev_current_component_function(previous_component_function);
}
});
};
}
/**
@ -75,29 +76,33 @@ export function wrap_snippet(component, fn) {
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol(
(/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
if (hydrating) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild);
anchor.before(element);
}
// @ts-expect-error the types are a lie
return (/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (hydrating) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild);
if (typeof result === 'function') {
teardown(result);
if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) {
w.invalid_raw_snippet_render();
}
anchor.before(element);
}
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (typeof result === 'function') {
teardown(result);
}
);
};
}

@ -1,5 +1,5 @@
/** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HEAD_EFFECT } from '../../constants.js';
@ -36,14 +36,22 @@ export function head(render_fn) {
}
while (
head_anchor.nodeType !== 8 ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START
head_anchor !== null &&
(head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
) {
head_anchor = /** @type {TemplateNode} */ (head_anchor.nextSibling);
}
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
} else {
// If we can't find an opening hydration marker, skip hydration (this can happen
// if a framework rendered body but not head content)
if (head_anchor === null) {
set_hydrating(false);
} else {
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
}
}
if (!hydrating) {
anchor = document.head.appendChild(empty());
}
@ -51,6 +59,7 @@ export function head(render_fn) {
block(() => render_fn(anchor), HEAD_EFFECT);
} finally {
if (was_hydrating) {
set_hydrating(true);
head_anchor = hydrate_node; // so that next head block starts from the correct node
set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
}

@ -1,4 +1,4 @@
export { FILENAME, ORIGINAL } from '../../constants.js';
export { FILENAME, HMR } from '../../constants.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
export {
@ -164,9 +164,8 @@ export {
export { snapshot } from '../shared/clone.js';
export { noop } from '../shared/utils.js';
export {
validate_component,
invalid_default_snippet,
validate_dynamic_element_tag,
validate_snippet,
validate_store,
validate_void_dynamic_element
} from '../shared/validate.js';

@ -28,7 +28,7 @@ export function store_get(store, store_name, stores) {
entry.store = store ?? null;
if (store == null) {
set(entry.source, undefined);
entry.source.v = undefined; // see synchronous callback comment below
entry.unsubscribe = noop;
} else {
var is_synchronous_callback = true;

@ -24,7 +24,6 @@ import {
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js';
import * as e from './errors.js';
import { validate_component } from '../shared/validate.js';
import { assign_nodes } from './dom/template.js';
/**
@ -79,10 +78,6 @@ export function set_text(text, value) {
* @returns {Exports}
*/
export function mount(component, options) {
if (DEV) {
validate_component(component);
}
const anchor = options.anchor ?? options.target.appendChild(empty());
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => _mount(component, { ...options, anchor }), false);
@ -112,10 +107,6 @@ export function mount(component, options) {
* @returns {Exports}
*/
export function hydrate(component, options) {
if (DEV) {
validate_component(component);
}
options.intro = options.intro ?? false;
const target = options.target;
const was_hydrating = hydrating;

@ -60,6 +60,18 @@ export function hydration_mismatch(location) {
}
}
/**
* The `render` function passed to `createRawSnippet` should return HTML for a single element
*/
export function invalid_raw_snippet_render() {
if (DEV) {
console.warn(`%c[svelte] invalid_raw_snippet_render\n%cThe \`render\` function passed to \`createRawSnippet\` should return HTML for a single element`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("invalid_raw_snippet_render");
}
}
/**
* Tried to unmount a component that was not mounted
*/

@ -1,7 +1,6 @@
/** @import { Snippet } from 'svelte' */
/** @import { Payload } from '#server' */
/** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../shared/validate.js';
/**
* Create a snippet programmatically
@ -13,10 +12,11 @@ import { add_snippet_symbol } from '../../shared/validate.js';
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol((/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
// @ts-expect-error the types are a lie
return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out += fn(...getters)
.render()
.trim();
});
};
}

@ -1,6 +1,6 @@
/** @import { Component, Payload, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, ORIGINAL } from '../../constants.js';
export { FILENAME, HMR } from '../../constants.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
@ -555,10 +555,8 @@ export { push_element, pop_element } from './dev.js';
export { snapshot } from '../shared/clone.js';
export {
add_snippet_symbol,
validate_component,
invalid_default_snippet,
validate_dynamic_element_tag,
validate_snippet,
validate_void_dynamic_element
} from '../shared/validate.js';

@ -79,6 +79,10 @@ function clone(value, cloned, path, paths) {
return copy;
}
if (value instanceof Date) {
return /** @type {Snapshot<T>} */ (structuredClone(value));
}
if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function') {
return clone(
/** @type {T & { toJSON(): any } } */ (value).toJSON(),

@ -3,51 +3,35 @@
import { DEV } from 'esm-env';
/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name
* @returns {never}
*/
export function lifecycle_outside_component(name) {
if (DEV) {
const error = new Error(`lifecycle_outside_component\n\`${name}(...)\` can only be used during component initialisation`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("lifecycle_outside_component");
}
}
/**
* The argument to `{@render ...}` must be a snippet function, not a component or a slot with a `let:` directive or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`
* Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
* @returns {never}
*/
export function render_tag_invalid_argument() {
export function invalid_default_snippet() {
if (DEV) {
const error = new Error(`render_tag_invalid_argument\nThe argument to \`{@render ...}\` must be a snippet function, not a component or a slot with a \`let:\` directive or some other kind of function. If you want to dynamically render one snippet or another, use \`$derived\` and pass its result to \`{@render ...}\``);
const error = new Error(`invalid_default_snippet\nCannot use \`{@render children(...)}\` if the parent component uses \`let:\` directives. Consider using a named snippet instead`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("render_tag_invalid_argument");
throw new Error("invalid_default_snippet");
}
}
/**
* A snippet must be rendered with `{@render ...}`
* `%name%(...)` can only be used during component initialisation
* @param {string} name
* @returns {never}
*/
export function snippet_used_as_component() {
export function lifecycle_outside_component(name) {
if (DEV) {
const error = new Error(`snippet_used_as_component\nA snippet must be rendered with \`{@render ...}\``);
const error = new Error(`lifecycle_outside_component\n\`${name}(...)\` can only be used during component initialisation`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("snippet_used_as_component");
throw new Error("lifecycle_outside_component");
}
}

@ -4,44 +4,7 @@ import { is_void } from '../../constants.js';
import * as w from './warnings.js';
import * as e from './errors.js';
const snippet_symbol = Symbol.for('svelte.snippet');
/**
* @param {any} fn
* @returns {import('svelte').Snippet}
*/
export function add_snippet_symbol(fn) {
fn[snippet_symbol] = true;
return fn;
}
/**
* Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function.
* @param {any} snippet_fn
* @param {Record<string, any> | undefined} $$props Only passed if render tag receives arguments and is for the children prop
*/
export function validate_snippet(snippet_fn, $$props) {
if (
($$props?.$$slots?.default && typeof $$props.$$slots.default !== 'boolean') ||
(snippet_fn && snippet_fn[snippet_symbol] !== true)
) {
e.render_tag_invalid_argument();
}
return snippet_fn;
}
/**
* Validate that the function behind `<Component />` isn't a snippet.
* @param {any} component_fn
*/
export function validate_component(component_fn) {
if (component_fn?.[snippet_symbol] === true) {
e.snippet_used_as_component();
}
return component_fn;
}
export { invalid_default_snippet } from './errors.js';
/**
* @param {() => string} tag_fn

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.192';
export const VERSION = '5.0.0-next.195';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
test(assert, target, snapshot, component, window) {
assert.equal(window.document.querySelectorAll('meta').length, 2);
}
});

@ -0,0 +1 @@
<meta name="description" content="some description"> <meta name="keywords" content="some keywords">

@ -0,0 +1,6 @@
<svelte:head>
<meta name="description" content="some description" />
<meta name="keywords" content="some keywords" />
</svelte:head>
<div>Just a dummy page.</div>

@ -53,13 +53,14 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
});
const override = read(`${cwd}/_override.html`);
const override_head = read(`${cwd}/_override_head.html`);
fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n');
target.innerHTML = override ?? rendered.html;
if (rendered.head) {
fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n');
head.innerHTML = rendered.head;
head.innerHTML = override_head ?? rendered.head;
}
config.before_test?.();

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
compileOptions: {
dev: true
},
test({ assert, target, window }) {
assert.htmlEqual(target.innerHTML, `<input><p>hello</p>`);
const input = target.querySelector('input');
ok(input);
input.value = 'goodbye';
input.dispatchEvent(new window.Event('input'));
flushSync();
assert.htmlEqual(target.innerHTML, `<input><p>goodbye</p>`);
},
warnings: []
});

@ -0,0 +1,6 @@
<script>
let object = { value: 'hello' };
</script>
<input bind:value={object.value} />
<p>{object.value}</p>

@ -0,0 +1,13 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
skip_mode: ['hydrate'],
warnings: [
'The `render` function passed to `createRawSnippet` should return HTML for a single element'
]
});

@ -0,0 +1,11 @@
<script>
import { createRawSnippet } from 'svelte';
const snippet = createRawSnippet(() => ({
render: () => `
<!-- --><div>123</div>
`
}));
</script>
{@render snippet()}

@ -0,0 +1,24 @@
import { flushSync } from 'svelte';
import { test, ok } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
const [btn1, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
btn2.click();
});
assert.deepEqual(logs, ['AA', 'AB']);
flushSync(() => {
btn3.click();
btn1.click();
btn2.click();
});
assert.deepEqual(logs, ['AA', 'AB', 'BA', 'BB']);
}
});

@ -0,0 +1,15 @@
<script>
let hof = $state((name) => () => console.log('A' + name));
const member = $derived({
hof
});
function change() {
hof = (name) => () => console.log('B' + name);
}
</script>
<button onclick={hof('A')}>A</button>
<button onclick={member.hof('B')}>B</button>
<br />
<button onclick={change}>change</button>

@ -1,12 +0,0 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
async test({ assert, target }) {
const div = target.querySelector('div');
assert.htmlEqual(div?.innerHTML || '', '');
},
runtime_error: 'snippet_used_as_component\nA snippet must be rendered with `{@render ...}`'
});

@ -1,14 +0,0 @@
<script>
import { onMount, mount } from 'svelte';
let el;
onMount(() => {
mount(foo, { target: el });
});
</script>
<div bind:this={el}></div>
{#snippet foo()}
shouldnt be rendered
{/snippet}

@ -4,5 +4,5 @@ export default test({
compileOptions: {
dev: true
},
runtime_error: 'render_tag_invalid_argument'
runtime_error: 'invalid_default_snippet'
});

@ -4,5 +4,5 @@ export default test({
compileOptions: {
dev: true
},
error: 'render_tag_invalid_argument'
runtime_error: 'invalid_default_snippet'
});

@ -0,0 +1,5 @@
<script>
let { children: x } = $props();
</script>
{@render x(true)}

@ -0,0 +1,7 @@
<script>
import Inner from './inner.svelte';
</script>
<Inner let:foo>
{foo}
</Inner>

@ -1,7 +0,0 @@
<script>
function not_a_snippet() {
console.log('hello');
}
</script>
{@render not_a_snippet()}

@ -1,8 +0,0 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
error: 'snippet_used_as_component\nA snippet must be rendered with `{@render ...}`'
});

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `true\ntrue\ntrue\ntrue`
});

@ -0,0 +1,16 @@
<script>
let test = $state({
a: new Date()
});
let test2 = $state.snapshot(test);
let test3 = {
a: new Date()
}
let test4 = structuredClone(test3);
</script>
{test.a instanceof Date}
{test2.a instanceof Date}
{test3.a instanceof Date}
{test4.a instanceof Date}

@ -9,10 +9,12 @@
return () => {};
}
};
let store3 = undefined;
// store signal is updated during reading this, which normally errors, but shouldn't for stores
let name = $derived($store1);
let hello = $derived($store2);
let undefined_value = $derived($store3);
</script>
<h1>{hello} {name}</h1>
<h1>{hello} {name} {undefined_value}</h1>

@ -10,16 +10,11 @@ function Hmr($$anchor) {
}
if (import.meta.hot) {
const s = $.source(Hmr);
const filename = Hmr.filename;
const $$original = Hmr;
Hmr = $.hmr(s);
Hmr.filename = filename;
Hmr[$.ORIGINAL] = $$original;
Hmr = $.hmr(Hmr, () => Hmr[$.HMR].source);
import.meta.hot.accept((module) => {
$.set(s, module.default[$.ORIGINAL]);
module.default[$.HMR].source = Hmr[$.HMR].source;
$.set(Hmr[$.HMR].source, module.default[$.HMR].original);
});
}

@ -6,7 +6,8 @@ import {
type ComponentType,
mount,
hydrate,
type Component
type Component,
type ComponentInternals
} from 'svelte';
import { render } from 'svelte/server';
@ -338,5 +339,5 @@ render(functionComponent, {
// but should always pass in tsc (because it will never know about this fact)
import Foo from './doesntexist.svelte';
Foo(null, { a: true });
Foo(null as unknown as ComponentInternals, { a: true });
const f: Foo = new Foo({ target: document.body, props: { a: true } });

@ -101,6 +101,15 @@ declare module 'svelte' {
$set(props: Partial<Props>): void;
}
const brand: unique symbol;
type Brand<B> = { [brand]: B };
type Branded<T, B> = T & Brand<B>;
/**
* Internal implementation details that vary between environments
*/
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;
/**
* Can be used to create strongly typed Svelte components.
*
@ -133,7 +142,8 @@ declare module 'svelte' {
* @param props The props passed to the component.
*/
(
internal: unknown,
this: void,
internals: ComponentInternals,
props: Props
): {
/**
@ -293,6 +303,9 @@ declare module 'svelte' {
: [type: Type, parameter: EventMap[Type], options?: DispatchOptions]
): boolean;
}
type Getters<T> = {
[K in keyof T]: () => T[K];
};
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
* It must be called during the component's initialisation (but doesn't need to live *inside* the component;
@ -457,9 +470,6 @@ declare module 'svelte' {
* https://svelte.dev/docs/svelte#getallcontexts
* */
export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};
export {};
}

@ -18,7 +18,7 @@
<meta name="twitter:title" content="{data.page.title} • Docs • Svelte 5 preview" />
<meta name="twitter:description" content="{data.page.title} • Svelte 5 preview documentation" />
<meta name="Description" content="{data.page.title} • Svelte 5 preview documentation" />
<meta name="description" content="{data.page.title} • Svelte 5 preview documentation" />
</svelte:head>
<div class="text" id="docs-content" use:copy_code_descendants>

@ -556,17 +556,22 @@ let props = $props();
If you're using TypeScript, you can declare the prop types:
<!-- prettier-ignore -->
```ts
type MyProps = any;
// ---cut---
let { a, b, c, ...everythingElse }: MyProps = $props();
interface MyProps {
required: string;
optional?: number;
partOfEverythingElse?: boolean;
};
let { required, optional, ...everythingElse }: MyProps = $props();
```
> In an earlier preview, `$props()` took a type argument. This caused bugs, since in a case like this...
>
> ```ts
> // @errors: 2558
> let { x = 42 } = $props<{ x: string }>();
> let { x = 42 } = $props<{ x?: string }>();
> ```
>
> ...TypeScript [widens the type](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwBIAHGHIgZwB4AVeAXnilQE8A+ACgEoAueagbgBQgiCAzwA3vAAe9eABYATPAC+c4qQqUp03uQwwsqAOaqOnIfCsB6a-AB6AfiA) of `x` to be `string | number`, instead of erroring.

@ -70,7 +70,7 @@
<meta name="twitter:title" content="{data.gist.name} • REPL • Svelte" />
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
<meta name="Description" content="Interactive Svelte playground" />
<meta name="description" content="Interactive Svelte playground" />
</svelte:head>
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}">

@ -10,7 +10,7 @@
<meta name="twitter:title" content="Svelte REPL" />
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
<meta name="Description" content="Interactive Svelte playground" />
<meta name="description" content="Interactive Svelte playground" />
</svelte:head>
<div class="repl-outer">

@ -11,7 +11,7 @@
<meta name="twitter:title" content="Svelte" />
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
<meta name="Description" content="Cybernetically enhanced web apps" />
<meta name="description" content="Cybernetically enhanced web apps" />
</svelte:head>
<h1 class="visually-hidden">Svelte</h1>

@ -13,7 +13,7 @@
<meta name="twitter:title" content="Svelte blog" />
<meta name="twitter:description" content="Articles about Svelte and UI development" />
<meta name="Description" content="Articles about Svelte and UI development" />
<meta name="description" content="Articles about Svelte and UI development" />
</svelte:head>
<h1 class="visually-hidden">Blog</h1>

@ -14,7 +14,7 @@
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={data.post.title} />
<meta name="twitter:description" content={data.post.description} />
<meta name="Description" content={data.post.description} />
<meta name="description" content={data.post.description} />
<meta name="twitter:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />
<meta name="og:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />

@ -19,7 +19,7 @@
<meta name="twitter:title" content="{data.page.title} • Docs • Svelte" />
<meta name="twitter:description" content="{data.page.title} • Svelte documentation" />
<meta name="Description" content="{data.page.title} • Svelte documentation" />
<meta name="description" content="{data.page.title} • Svelte documentation" />
</svelte:head>
<div class="text" id="docs-content" use:copy_code_descendants>

@ -34,7 +34,7 @@
<meta name="twitter:title" content="Svelte examples" />
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
<meta name="Description" content="Interactive example Svelte apps" />
<meta name="description" content="Interactive example Svelte apps" />
</svelte:head>
<h1 class="visually-hidden">Examples</h1>

@ -102,7 +102,7 @@
<meta name="twitter:title" content="Svelte tutorial" />
<meta name="twitter:description" content="{selected.section.title} / {selected.chapter.title}" />
<meta name="Description" content="{selected.section.title} / {selected.chapter.title}" />
<meta name="description" content="{selected.section.title} / {selected.chapter.title}" />
</svelte:head>
<svelte:window bind:innerWidth={width} />

Loading…
Cancel
Save