Merge remote-tracking branch 'origin/main' into svelte-custom-renderer

pull/18058/head
paoloricciuti 6 months ago
commit f35e12cf0e

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: discard batches made obsolete by commit

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: defer batch resolution until earlier intersecting batches have committed

@ -1,5 +1,31 @@
# svelte # svelte
## 5.54.0
### Minor Changes
- feat: allow `css`, `runes`, `customElement` compiler options to be functions ([#17951](https://github.com/sveltejs/svelte/pull/17951))
### Patch Changes
- fix: reinstate reactivity loss tracking ([#17801](https://github.com/sveltejs/svelte/pull/17801))
## 5.53.13
### Patch Changes
- fix: ensure `$inspect` after top level await doesn't break builds ([#17943](https://github.com/sveltejs/svelte/pull/17943))
- fix: resume inert effects when they come from offscreen ([#17942](https://github.com/sveltejs/svelte/pull/17942))
- fix: don't eagerly access not-yet-initialized functions in template ([#17938](https://github.com/sveltejs/svelte/pull/17938))
- fix: discard batches made obsolete by commit ([#17934](https://github.com/sveltejs/svelte/pull/17934))
- fix: ensure "is standalone child" is correctly reset ([#17944](https://github.com/sveltejs/svelte/pull/17944))
- fix: remove nodes in boundary when work is pending and HMR is active ([#17932](https://github.com/sveltejs/svelte/pull/17932))
## 5.53.12 ## 5.53.12
### Patch Changes ### Patch Changes

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

@ -23,6 +23,7 @@ export { print } from './print/index.js';
export function compile(source, options) { export function compile(source, options) {
source = remove_bom(source); source = remove_bom(source);
state.reset({ warning: options.warningFilter, filename: options.filename }); state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, ''); const validated = validate_component_options(options, '');
let parsed = _parse(source); let parsed = _parse(source);
@ -33,7 +34,9 @@ export function compile(source, options) {
const combined_options = { const combined_options = {
...validated, ...validated,
...parsed_options, ...parsed_options,
customElementOptions customElementOptions,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : validated.css,
runes: 'runes' in parsed_options ? () => parsed_options.runes : validated.runes
}; };
if (parsed.metadata.ts) { if (parsed.metadata.ts) {

@ -146,6 +146,8 @@ export function migrate(source, { filename, use_ts } = {}) {
...parsed_options, ...parsed_options,
customElementOptions, customElementOptions,
filename: filename ?? UNKNOWN_FILENAME, filename: filename ?? UNKNOWN_FILENAME,
css: 'css' in parsed_options ? () => parsed_options.css ?? 'external' : () => 'external',
runes: 'runes' in parsed_options ? () => parsed_options.runes : () => undefined,
experimental: { experimental: {
async: true async: true
} }

@ -345,6 +345,8 @@ export function analyze_component(root, source, options) {
let synthetic_stores_legacy_check = []; let synthetic_stores_legacy_check = [];
const runes_option = options.runes?.({ filename: options.filename });
// create synthetic bindings for store subscriptions // create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) { for (const [name, references] of module.scope.references) {
if (name[0] !== '$' || RESERVED.includes(name)) continue; if (name[0] !== '$' || RESERVED.includes(name)) continue;
@ -359,7 +361,7 @@ export function analyze_component(root, source, options) {
// If we're not in legacy mode through the compiler option, assume the user // If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store. // is referencing a rune and not a global store.
if ( if (
options.runes === false || runes_option === false ||
!is_rune(name) || !is_rune(name) ||
(declaration !== null && (declaration !== null &&
// const state = $state(0) is valid // const state = $state(0) is valid
@ -395,7 +397,7 @@ export function analyze_component(root, source, options) {
e.store_invalid_scoped_subscription(is_nested_store_subscription_node); e.store_invalid_scoped_subscription(is_nested_store_subscription_node);
} }
if (options.runes !== false) { if (runes_option !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) { if (declaration === null && /[a-z]/.test(store_name[0])) {
e.global_reference_invalid(references[0].node, name); e.global_reference_invalid(references[0].node, name);
} else if (declaration !== null && is_rune(name)) { } else if (declaration !== null && is_rune(name)) {
@ -447,7 +449,7 @@ export function analyze_component(root, source, options) {
const component_name = get_component_name(options.filename); const component_name = get_component_name(options.filename);
const runes = const runes =
options.runes ?? runes_option ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune)); (has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
if (!runes) { if (!runes) {
@ -463,7 +465,10 @@ export function analyze_component(root, source, options) {
} }
} }
const is_custom_element = !!options.customElementOptions || options.customElement; const custom_element_from_option = options.customElement({ filename: options.filename });
const css = options.css({ filename: options.filename });
const custom_element = options.customElementOptions ?? custom_element_from_option;
const is_custom_element = !!options.customElementOptions || custom_element_from_option;
const name = module.scope.generate(options.name ?? component_name); const name = module.scope.generate(options.name ?? component_name);
@ -491,7 +496,7 @@ export function analyze_component(root, source, options) {
maybe_runes: maybe_runes:
!runes && !runes &&
// if they explicitly disabled runes, use the legacy behavior // if they explicitly disabled runes, use the legacy behavior
options.runes !== false && runes_option !== false &&
![...module.scope.references.keys()].some((name) => ![...module.scope.references.keys()].some((name) =>
['$$props', '$$restProps'].includes(name) ['$$props', '$$restProps'].includes(name)
) && ) &&
@ -523,8 +528,8 @@ export function analyze_component(root, source, options) {
needs_props: false, needs_props: false,
event_directive_node: null, event_directive_node: null,
uses_event_attributes: false, uses_event_attributes: false,
custom_element: is_custom_element, custom_element,
inject_styles: options.css === 'injected' || is_custom_element, inject_styles: css === 'injected' || is_custom_element,
accessors: accessors:
is_custom_element || is_custom_element ||
(runes ? false : !!options.accessors) || (runes ? false : !!options.accessors) ||
@ -680,7 +685,7 @@ export function analyze_component(root, source, options) {
w.options_deprecated_accessors(attribute); w.options_deprecated_accessors(attribute);
} }
if (attribute.name === 'customElement' && !options.customElement) { if (attribute.name === 'customElement' && !custom_element_from_option) {
w.options_missing_custom_element(attribute); w.options_missing_custom_element(attribute);
} }

@ -619,7 +619,7 @@ export function client_component(analysis, options) {
); );
} }
const ce = options.customElementOptions ?? options.customElement; const ce = analysis.custom_element;
if (ce) { if (ce) {
const ce_props = typeof ce === 'boolean' ? {} : ce.props || {}; const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};

@ -63,6 +63,7 @@ export function Fragment(node, context) {
/** @type {ComponentClientTransformState} */ /** @type {ComponentClientTransformState} */
const state = { const state = {
...context.state, ...context.state,
is_standalone,
init: [], init: [],
snippets: [], snippets: [],
consts: [], consts: [],
@ -128,7 +129,7 @@ export function Fragment(node, context) {
// no need to create a template, we can just use the existing block's anchor // no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { process_children(trimmed, () => b.id('$$anchor'), false, {
...context, ...context,
state: { ...state, is_standalone } state
}); });
} else { } else {
/** @type {(is_text: boolean) => Expression} */ /** @type {(is_text: boolean) => Expression} */

@ -82,12 +82,16 @@ export class Memoizer {
async_values() { async_values() {
if (this.#async.length === 0) return; if (this.#async.length === 0) return;
return b.array(this.#async.map((memo) => b.thunk(memo.expression, true))); // use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#async.map((memo) => b.arrow([], memo.expression, true)));
} }
sync_values() { sync_values() {
if (this.#sync.length === 0) return; if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression))); // use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#sync.map((memo) => b.arrow([], memo.expression)));
} }
} }

@ -65,7 +65,7 @@ export function render_stylesheet(source, analysis, options) {
merge_with_preprocessor_map(css, options, css.map.sources[0]); merge_with_preprocessor_map(css, options, css.map.sources[0]);
if (dev && options.css === 'injected' && css.code) { if (dev && analysis.inject_styles && css.code) {
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`; css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
} }

@ -300,7 +300,7 @@ export function server_component(analysis, options) {
const body = [...state.hoisted, ...module.body]; const body = [...state.hoisted, ...module.body];
if (analysis.css.ast !== null && options.css === 'injected' && !options.customElement) { if (analysis.css.ast !== null && analysis.inject_styles && !analysis.custom_element) {
const hash = b.literal(analysis.css.hash); const hash = b.literal(analysis.css.hash);
const code = b.literal(render_stylesheet(analysis.source, analysis, options).code); const code = b.literal(render_stylesheet(analysis.source, analysis, options).code);

@ -95,7 +95,8 @@ export function transform_body(instance_body, runner, transform) {
); );
if (expression.type === 'EmptyStatement') { if (expression.type === 'EmptyStatement') {
return null; // Keep indices stable for async sequencing while avoiding array holes in run([...]).
return b.thunk(b.void0, false);
} }
return expression.type === 'AwaitExpression' return expression.type === 'AwaitExpression'

@ -73,9 +73,11 @@ export interface CompileOptions extends ModuleCompileOptions {
/** /**
* If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component. * If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component.
* *
* You can also pass a function that receives `{ filename }` and returns a boolean.
*
* @default false * @default false
*/ */
customElement?: boolean; customElement?: boolean | ((options: { filename: string }) => boolean);
/** /**
* If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`. * If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
* *
@ -101,8 +103,10 @@ export interface CompileOptions extends ModuleCompileOptions {
* - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root. * - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root.
* - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. * - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files.
* This is always `'injected'` when compiling with `customElement` mode. * This is always `'injected'` when compiling with `customElement` mode.
*
* You can also pass a function that receives `{ filename }` and returns either `'injected'` or `'external'`.
*/ */
css?: 'injected' | 'external'; css?: 'injected' | 'external' | ((options: { filename: string }) => 'injected' | 'external');
/** /**
* A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS. * A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS.
* It defaults to returning `svelte-${hash(filename ?? css)}`. * It defaults to returning `svelte-${hash(filename ?? css)}`.
@ -146,7 +150,7 @@ export interface CompileOptions extends ModuleCompileOptions {
* which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead. * which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead.
* @default undefined * @default undefined
*/ */
runes?: boolean | undefined; runes?: boolean | undefined | ((options: { filename: string }) => boolean | undefined);
/** /**
* If `true`, exposes the Svelte major version in the browser by adding it to a `Set` stored in the global `window.__svelte.v`. * If `true`, exposes the Svelte major version in the browser by adding it to a `Set` stored in the global `window.__svelte.v`.
* *
@ -252,18 +256,22 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
Required<CompileOptions>, Required<CompileOptions>,
| keyof ModuleCompileOptions | keyof ModuleCompileOptions
| 'name' | 'name'
| 'customElement'
| 'compatibility' | 'compatibility'
| 'outputFilename' | 'outputFilename'
| 'cssOutputFilename' | 'cssOutputFilename'
| 'sourcemap' | 'sourcemap'
| 'css'
| 'runes' | 'runes'
> & { > & {
name: CompileOptions['name']; name: CompileOptions['name'];
customElement: (options: { filename: string }) => boolean;
outputFilename: CompileOptions['outputFilename']; outputFilename: CompileOptions['outputFilename'];
cssOutputFilename: CompileOptions['cssOutputFilename']; cssOutputFilename: CompileOptions['cssOutputFilename'];
sourcemap: CompileOptions['sourcemap']; sourcemap: CompileOptions['sourcemap'];
compatibility: Required<Required<CompileOptions>['compatibility']>; compatibility: Required<Required<CompileOptions>['compatibility']>;
runes: CompileOptions['runes']; css: (options: { filename: string }) => 'injected' | 'external';
runes: (options: { filename: string }) => boolean | undefined;
customElementOptions: AST.SvelteOptions['customElement']; customElementOptions: AST.SvelteOptions['customElement'];
hmr: CompileOptions['hmr']; hmr: CompileOptions['hmr'];
}; };

@ -36,6 +36,13 @@ export function assignment_pattern(left, right) {
* @returns {ESTree.ArrowFunctionExpression} * @returns {ESTree.ArrowFunctionExpression}
*/ */
export function arrow(params, body, async = false) { export function arrow(params, body, async = false) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (async && body.type === 'AwaitExpression') {
if (!has_await_expression(body.argument)) {
return arrow(params, body.argument);
}
}
return { return {
type: 'ArrowFunctionExpression', type: 'ArrowFunctionExpression',
params, params,
@ -462,13 +469,6 @@ export function thunk(expression, async = false) {
* @returns {ESTree.Expression} * @returns {ESTree.Expression}
*/ */
export function unthunk(expression) { export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
if (!has_await_expression(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}
if ( if (
expression.async === false && expression.async === false &&
expression.body.type === 'CallExpression' && expression.body.type === 'CallExpression' &&

@ -51,7 +51,10 @@ const common_options = {
const component_options = { const component_options = {
accessors: deprecate(w.options_deprecated_accessors, boolean(false)), accessors: deprecate(w.options_deprecated_accessors, boolean(false)),
css: validator('external', (input) => { /** @type {Validator<'injected' | 'external' | ((options: { filename: string }) => 'injected' | 'external'), (options: { filename: string }) => 'injected' | 'external'>} */
css: parametric(
/** @type {(options: { filename: string }) => 'injected' | 'external'} */ (() => 'external'),
(input) => {
if (input === true || input === false) { if (input === true || input === false) {
throw_error( throw_error(
'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true' 'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true'
@ -67,8 +70,9 @@ const component_options = {
throw_error(`css should be either "external" (default, recommended) or "injected"`); throw_error(`css should be either "external" (default, recommended) or "injected"`);
} }
return input; return /** @type {'external' | 'injected'} */ (input);
}), }
),
cssHash: fun(({ css, filename, hash }) => { cssHash: fun(({ css, filename, hash }) => {
return `svelte-${hash(filename === '(unknown)' ? css : filename ?? css)}`; return `svelte-${hash(filename === '(unknown)' ? css : filename ?? css)}`;
@ -77,7 +81,17 @@ const component_options = {
// TODO this is a sourcemap option, would be good to put under a sourcemap namespace // TODO this is a sourcemap option, would be good to put under a sourcemap namespace
cssOutputFilename: string(undefined), cssOutputFilename: string(undefined),
customElement: boolean(false), /** @type {Validator<boolean | ((options: { filename: string }) => boolean), (options: { filename: string }) => boolean>} */
customElement: parametric(
/** @type {(options: { filename: string }) => boolean} */ (() => false),
(input, keypath) => {
if (typeof input !== 'boolean') {
throw_error(`${keypath} should be true or false`);
}
return /** @type {boolean} */ (input);
}
),
discloseVersion: boolean(true), discloseVersion: boolean(true),
@ -109,7 +123,8 @@ const component_options = {
preserveWhitespace: boolean(false), preserveWhitespace: boolean(false),
runes: boolean(undefined), /** @type {Validator<boolean | undefined | (() => boolean | undefined), () => boolean | undefined>} */
runes: parametric(() => /** @type {boolean | undefined} */ (undefined)),
hmr: boolean(false), hmr: boolean(false),
@ -320,6 +335,28 @@ function fun(fallback) {
}); });
} }
/**
* @template {(...args: any[]) => any} F
* @param {F} fallback
* @param {(value: unknown, keypath: string) => ReturnType<F>} [normalize]
* @returns {Validator}
*/
function parametric(fallback, normalize = (value) => /** @type {ReturnType<F>} */ (value)) {
return validator(fallback, (input, keypath) => {
if (typeof input === 'function') {
/** @type {(...args: Parameters<F>) => ReturnType<F>} */
const normalized = (...args) => normalize(input(...args), keypath);
return /** @type {F} */ (/** @type {unknown} */ (normalized));
}
/** @type {(...args: Parameters<F>) => ReturnType<F>} */
const normalized = (..._args) => normalize(input, keypath);
return /** @type {F} */ (/** @type {unknown} */ (normalized));
});
}
/** @param {string} msg */ /** @param {string} msg */
function throw_error(msg) { function throw_error(msg) {
e.options_invalid_value(null, msg); e.options_invalid_value(null, msg);

@ -6,6 +6,8 @@ import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set, source } from '../reactivity/sources.js'; import { set, source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js'; import { set_should_intro } from '../render.js';
import { get } from '../runtime.js'; import { get } from '../runtime.js';
import { assign_nodes } from '../dom/template.js';
import { create_comment } from '../dom/operations.js';
/** /**
* @template {(anchor: Comment, props: any) => any} Component * @template {(anchor: Comment, props: any) => any} Component
@ -27,6 +29,13 @@ export function hmr(fn) {
let ran = false; let ran = false;
// Surround the wrapped effects with comments and assign the nodes
// on the wrapping effects so the parent can properly do DOM operations.
let start = create_comment();
let end = create_comment();
anchor.before(start);
block(() => { block(() => {
if (component === (component = get(current))) { if (component === (component = get(current))) {
return; return;
@ -61,6 +70,10 @@ export function hmr(fn) {
anchor = hydrate_node; anchor = hydrate_node;
} }
anchor.before(end);
assign_nodes(start, end);
return instance; return instance;
} }

@ -480,6 +480,14 @@ function reconcile(state, array, anchor, flags, get_key) {
} }
} }
if ((effect.f & INERT) !== 0) {
resume_effect(effect);
if (is_animated) {
effect.nodes?.a?.unfix();
(to_animate ??= new Set()).delete(effect);
}
}
if ((effect.f & EFFECT_OFFSCREEN) !== 0) { if ((effect.f & EFFECT_OFFSCREEN) !== 0) {
effect.f ^= EFFECT_OFFSCREEN; effect.f ^= EFFECT_OFFSCREEN;
@ -508,14 +516,6 @@ function reconcile(state, array, anchor, flags, get_key) {
} }
} }
if ((effect.f & INERT) !== 0) {
resume_effect(effect);
if (is_animated) {
effect.nodes?.a?.unfix();
(to_animate ??= new Set()).delete(effect);
}
}
if (effect !== current) { if (effect !== current) {
if (seen !== undefined && seen.has(effect)) { if (seen !== undefined && seen.has(effect)) {
if (matched.length < stashed.length) { if (matched.length < stashed.length) {

@ -19,10 +19,10 @@ import {
import { Batch, current_batch } from './batch.js'; import { Batch, current_batch } from './batch.js';
import { import {
async_derived, async_derived,
current_async_effect, reactivity_loss_tracker,
derived, derived,
derived_safe_equal, derived_safe_equal,
set_from_async_derived set_reactivity_loss_tracker
} from './deriveds.js'; } from './deriveds.js';
import { aborted } from './effects.js'; import { aborted } from './effects.js';
@ -131,7 +131,7 @@ export function capture() {
} }
if (DEV) { if (DEV) {
set_from_async_derived(null); set_reactivity_loss_tracker(null);
set_dev_stack(previous_dev_stack); set_dev_stack(previous_dev_stack);
} }
}; };
@ -163,11 +163,11 @@ export async function save(promise) {
* @returns {Promise<() => T>} * @returns {Promise<() => T>}
*/ */
export async function track_reactivity_loss(promise) { export async function track_reactivity_loss(promise) {
var previous_async_effect = current_async_effect; var previous_async_effect = reactivity_loss_tracker;
var value = await promise; var value = await promise;
return () => { return () => {
set_from_async_derived(previous_async_effect); set_reactivity_loss_tracker(previous_async_effect);
return value; return value;
}; };
} }
@ -224,7 +224,7 @@ export function unset_context(deactivate_batch = true) {
if (deactivate_batch) current_batch?.deactivate(); if (deactivate_batch) current_batch?.deactivate();
if (DEV) { if (DEV) {
set_from_async_derived(null); set_reactivity_loss_tracker(null);
set_dev_stack(null); set_dev_stack(null);
} }
} }
@ -307,15 +307,16 @@ export function wait(blockers) {
* @returns {(skip?: boolean) => void} * @returns {(skip?: boolean) => void}
*/ */
export function increment_pending() { export function increment_pending() {
var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b); var effect = /** @type {Effect} */ (active_effect);
var boundary = /** @type {Boundary} */ (effect.b);
var batch = /** @type {Batch} */ (current_batch); var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered(); var blocking = boundary.is_rendered();
boundary.update_pending_count(1, batch); boundary.update_pending_count(1, batch);
batch.increment(blocking); batch.increment(blocking, effect);
return (skip = false) => { return (skip = false) => {
boundary.update_pending_count(-1, batch); boundary.update_pending_count(-1, batch);
batch.decrement(blocking, skip); batch.decrement(blocking, effect, skip);
}; };
} }

@ -90,20 +90,20 @@ var source_stacks = DEV ? new Set() : null;
let uid = 1; let uid = 1;
export class Batch { export class Batch {
// for debugging. TODO remove once async is stable
id = uid++; id = uid++;
/** /**
* The current values of any sources that are updated in this batch * The current values of any signals that are updated in this batch.
* Tuple format: [value, is_derived] (note: is_derived is false for deriveds, too, if they were overridden via assignment)
* They keys of this map are identical to `this.#previous` * They keys of this map are identical to `this.#previous`
* @type {Map<Source, any>} * @type {Map<Value, [any, boolean]>}
*/ */
current = new Map(); current = new Map();
/** /**
* The values of any sources that are updated in this batch _before_ those updates took place. * The values of any signals (sources and deriveds) that are updated in this batch _before_ those updates took place.
* They keys of this map are identical to `this.#current` * They keys of this map are identical to `this.#current`
* @type {Map<Source, any>} * @type {Map<Value, any>}
*/ */
previous = new Map(); previous = new Map();
@ -121,14 +121,16 @@ export class Batch {
#discard_callbacks = new Set(); #discard_callbacks = new Set();
/** /**
* The number of async effects that are currently in flight * Async effects that are currently in flight
* @type {Map<Effect, number>}
*/ */
#pending = 0; #pending = new Map();
/** /**
* The number of async effects that are currently in flight, _not_ inside a pending boundary * Async effects that are currently in flight, _not_ inside a pending boundary
* @type {Map<Effect, number>}
*/ */
#blocking_pending = 0; #blocking_pending = new Map();
/** /**
* A deferred that resolves when the batch is committed, used with `settled()` * A deferred that resolves when the batch is committed, used with `settled()`
@ -168,8 +170,35 @@ export class Batch {
#decrement_queued = false; #decrement_queued = false;
/** @type {Set<Batch>} */
#blockers = new Set();
#is_deferred() { #is_deferred() {
return this.is_fork || this.#blocking_pending > 0; return this.is_fork || this.#blocking_pending.size > 0;
}
#is_blocked() {
for (const batch of this.#blockers) {
for (const effect of batch.#blocking_pending.keys()) {
var skipped = false;
var e = effect;
while (e.parent !== null) {
if (this.#skipped_branches.has(e)) {
skipped = true;
break;
}
e = e.parent;
}
if (!skipped) {
return true;
}
}
}
return false;
} }
/** /**
@ -264,7 +293,7 @@ export class Batch {
collected_effects = null; collected_effects = null;
legacy_updates = null; legacy_updates = null;
if (this.#is_deferred()) { if (this.#is_deferred() || this.#is_blocked()) {
this.#defer_effects(render_effects); this.#defer_effects(render_effects);
this.#defer_effects(effects); this.#defer_effects(effects);
@ -272,7 +301,7 @@ export class Batch {
reset_branch(e, t); reset_branch(e, t);
} }
} else { } else {
if (this.#pending === 0) { if (this.#pending.size === 0) {
batches.delete(this); batches.delete(this);
} }
@ -383,17 +412,18 @@ export class Batch {
/** /**
* Associate a change to a given source with the current * Associate a change to a given source with the current
* batch, noting its previous and current values * batch, noting its previous and current values
* @param {Source} source * @param {Value} source
* @param {any} old_value * @param {any} old_value
* @param {boolean} [is_derived]
*/ */
capture(source, old_value) { capture(source, old_value, is_derived = false) {
if (old_value !== UNINITIALIZED && !this.previous.has(source)) { if (old_value !== UNINITIALIZED && !this.previous.has(source)) {
this.previous.set(source, old_value); this.previous.set(source, old_value);
} }
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) { if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, source.v); this.current.set(source, [source.v, is_derived]);
batch_values?.set(source, source.v); batch_values?.set(source, source.v);
} }
} }
@ -453,11 +483,13 @@ export class Batch {
/** @type {Source[]} */ /** @type {Source[]} */
var sources = []; var sources = [];
for (const [source, value] of this.current) { for (const [source, [value, is_derived]] of this.current) {
if (batch.current.has(source)) { if (batch.current.has(source)) {
if (is_earlier && value !== batch.current.get(source)) { var batch_value = /** @type {[any, boolean]} */ (batch.current.get(source))[0]; // faster than destructuring
if (is_earlier && value !== batch_value) {
// bring the value up to date // bring the value up to date
batch.current.set(source, value); batch.current.set(source, [value, is_derived]);
} else { } else {
// same value or later batch has more recent value, // same value or later batch has more recent value,
// no need to re-run these effects // no need to re-run these effects
@ -507,24 +539,56 @@ export class Batch {
batch.deactivate(); batch.deactivate();
} }
} }
for (const batch of batches) {
if (batch.#blockers.has(this)) {
batch.#blockers.delete(this);
if (batch.#blockers.size === 0 && !batch.#is_deferred()) {
batch.activate();
batch.#process();
}
}
}
} }
/** /**
*
* @param {boolean} blocking * @param {boolean} blocking
* @param {Effect} effect
*/ */
increment(blocking) { increment(blocking, effect) {
this.#pending += 1; let pending_count = this.#pending.get(effect) ?? 0;
if (blocking) this.#blocking_pending += 1; this.#pending.set(effect, pending_count + 1);
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
this.#blocking_pending.set(effect, blocking_pending_count + 1);
}
} }
/** /**
* @param {boolean} blocking * @param {boolean} blocking
* @param {Effect} effect
* @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction) * @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction)
*/ */
decrement(blocking, skip) { decrement(blocking, effect, skip) {
this.#pending -= 1; let pending_count = this.#pending.get(effect) ?? 0;
if (blocking) this.#blocking_pending -= 1;
if (pending_count === 1) {
this.#pending.delete(effect);
} else {
this.#pending.set(effect, pending_count - 1);
}
if (blocking) {
let blocking_pending_count = this.#blocking_pending.get(effect) ?? 0;
if (blocking_pending_count === 1) {
this.#blocking_pending.delete(effect);
} else {
this.#blocking_pending.set(effect, blocking_pending_count - 1);
}
}
if (this.#decrement_queued || skip) return; if (this.#decrement_queued || skip) return;
this.#decrement_queued = true; this.#decrement_queued = true;
@ -597,12 +661,33 @@ export class Batch {
// if there are multiple batches, we are 'time travelling' — // if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch... // we need to override values with the ones in this batch...
batch_values = new Map(this.current); batch_values = new Map();
for (const [source, [value]] of this.current) {
batch_values.set(source, value);
}
// ...and undo changes belonging to other batches // ...and undo changes belonging to other batches unless they block this one
for (const batch of batches) { for (const batch of batches) {
if (batch === this || batch.is_fork) continue; if (batch === this || batch.is_fork) continue;
// A batch is blocked on an earlier batch if it overlaps with the earlier batch's changes but is not a superset
var intersects = false;
var differs = false;
if (batch.id < this.id) {
for (const [source, [, is_derived]] of batch.current) {
// Derived values don't partake in the blocking mechanism, because a derived could
// be triggered in one batch already but not the other one yet, causing a false-positive
if (is_derived) continue;
intersects ||= this.current.has(source);
differs ||= !this.current.has(source);
}
}
if (intersects && differs) {
this.#blockers.add(batch);
} else {
for (const [source, previous] of batch.previous) { for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) { if (!batch_values.has(source)) {
batch_values.set(source, previous); batch_values.set(source, previous);
@ -610,6 +695,7 @@ export class Batch {
} }
} }
} }
}
/** /**
* *
@ -1065,7 +1151,7 @@ export function fork(fn) {
batch.is_fork = false; batch.is_fork = false;
// apply changes and update write versions so deriveds see the change // apply changes and update write versions so deriveds see the change
for (var [source, value] of batch.current) { for (var [source, [value]] of batch.current) {
source.v = value; source.v = value;
source.wv = increment_write_version(); source.wv = increment_write_version();
} }

@ -45,12 +45,16 @@ import { increment_pending, unset_context } from './async.js';
import { deferred, includes, noop } from '../../shared/utils.js'; import { deferred, includes, noop } from '../../shared/utils.js';
import { set_signal_status, update_derived_status } from './status.js'; import { set_signal_status, update_derived_status } from './status.js';
/** @type {Effect | null} */ /**
export let current_async_effect = null; * This allows us to track 'reactivity loss' that occurs when signals
* are read after a non-context-restoring `await`. Dev-only
* @type {{ effect: Effect, warned: boolean } | null}
*/
export let reactivity_loss_tracker = null;
/** @param {Effect | null} v */ /** @param {{ effect: Effect, warned: boolean } | null} v */
export function set_from_async_derived(v) { export function set_reactivity_loss_tracker(v) {
current_async_effect = v; reactivity_loss_tracker = v;
} }
export const recent_async_deriveds = new Set(); export const recent_async_deriveds = new Set();
@ -124,7 +128,12 @@ export function async_derived(fn, label, location) {
var deferreds = new Map(); var deferreds = new Map();
async_effect(() => { async_effect(() => {
if (DEV) current_async_effect = active_effect; if (DEV) {
reactivity_loss_tracker = {
effect: /** @type {Effect} */ (active_effect),
warned: false
};
}
var effect = /** @type {Effect} */ (active_effect); var effect = /** @type {Effect} */ (active_effect);
@ -142,7 +151,9 @@ export function async_derived(fn, label, location) {
unset_context(); unset_context();
} }
if (DEV) current_async_effect = null; if (DEV) {
reactivity_loss_tracker = null;
}
var batch = /** @type {Batch} */ (current_batch); var batch = /** @type {Batch} */ (current_batch);
@ -174,7 +185,9 @@ export function async_derived(fn, label, location) {
* @param {unknown} error * @param {unknown} error
*/ */
const handler = (value, error = undefined) => { const handler = (value, error = undefined) => {
if (DEV) current_async_effect = null; if (DEV) {
reactivity_loss_tracker = null;
}
if (decrement_pending) { if (decrement_pending) {
// don't trigger an update if we're only here because // don't trigger an update if we're only here because
@ -383,7 +396,7 @@ export function update_derived(derived) {
// change, `derived.equals` may incorrectly return `true` // change, `derived.equals` may incorrectly return `true`
if (!current_batch?.is_fork || derived.deps === null) { if (!current_batch?.is_fork || derived.deps === null) {
derived.v = value; derived.v = value;
current_batch?.capture(derived, old_value); current_batch?.capture(derived, old_value, true);
// deriveds without dependencies should never be recomputed // deriveds without dependencies should never be recomputed
if (derived.deps === null) { if (derived.deps === null) {

@ -27,7 +27,7 @@ import {
} from './constants.js'; } from './constants.js';
import { old_values } from './reactivity/sources.js'; import { old_values } from './reactivity/sources.js';
import { import {
destroy_derived_effects, reactivity_loss_tracker,
execute_derived, execute_derived,
freeze_derived_effects, freeze_derived_effects,
recent_async_deriveds, recent_async_deriveds,
@ -58,6 +58,7 @@ import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js'; import { captured_signals } from './legacy.js';
import { without_reactive_context } from './dom/elements/bindings/shared.js'; import { without_reactive_context } from './dom/elements/bindings/shared.js';
import { set_signal_status, update_derived_status } from './reactivity/status.js'; import { set_signal_status, update_derived_status } from './reactivity/status.js';
import * as w from './warnings.js';
let is_updating_effect = false; let is_updating_effect = false;
@ -568,19 +569,20 @@ export function get(signal) {
} }
if (DEV) { if (DEV) {
// TODO reinstate this, but make it actually work if (
// if (current_async_effect) { !untracking &&
// var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; reactivity_loss_tracker &&
// var was_read = current_async_effect.deps?.includes(signal); !reactivity_loss_tracker.warned &&
(reactivity_loss_tracker.effect.f & REACTION_IS_UPDATING) === 0
) {
reactivity_loss_tracker.warned = true;
// if (!tracking && !untracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label));
// w.await_reactivity_loss(/** @type {string} */ (signal.label));
// var trace = get_error('traced at'); var trace = get_error('traced at');
// // eslint-disable-next-line no-console // eslint-disable-next-line no-console
// if (trace) console.warn(trace); if (trace) console.warn(trace);
// } }
// }
recent_async_deriveds.delete(signal); recent_async_deriveds.delete(signal);
@ -595,7 +597,7 @@ export function get(signal) {
if (signal.trace) { if (signal.trace) {
signal.trace(); signal.trace();
} else { } else {
var trace = get_error('traced at'); trace = get_error('traced at');
if (trace) { if (trace) {
var entry = tracing_expressions.entries.get(signal); var entry = tracing_expressions.entries.get(signal);

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

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a, b, resolve] = target.querySelectorAll('button');
a.click();
await tick();
b.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>a</button>
<button>b</button>
<button>resolve</button>
hi
1
`
);
}
});

@ -0,0 +1,21 @@
<script>
let a = $state(0);
let b = $state(0);
let a_b = $derived(a * b);
const queued = [];
function push(value) {
if (!value) return value;
return new Promise(resolve => {
queued.push(() => resolve(value));
});
}
</script>
<button onclick={() => (a++)}>a</button>
<button onclick={() => (b++)}>b</button>
<button onclick={() => (queued.shift()?.())}>resolve</button>
<!-- a_b called in a block effect before being called in an async effect -->
{#if a_b}hi{/if}
{await push(a_b)}

@ -0,0 +1,32 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const spam = /** @type {HTMLButtonElement} */ (target.querySelector('button.spam'));
const resolve = /** @type {HTMLButtonElement} */ (target.querySelector('button.resolve'));
resolve.click();
await tick();
for (let i = 0; i < 5; i += 1) {
spam.click();
await tick();
}
for (let i = 0; i < 5; i += 1) {
resolve.click();
await tick();
}
assert.equal(target.querySelectorAll('div').length, 1);
assert.htmlEqual(
target.innerHTML,
`
<button class="spam">Spam</button>
<button class="resolve">Resolve</button>
<div>5</div>
`
);
}
});

@ -0,0 +1,28 @@
<script>
let value = $state({ id: '0' });
const resolvers = [];
function wait() {
const promise = Promise.withResolvers();
resolvers.push(promise.resolve);
return promise.promise;
}
function spam() {
value.id = `${Number(value.id) + 1}`;
}
</script>
<button class="spam" onclick={spam}>Spam</button>
<button class="resolve" onclick={() => resolvers.shift()?.()}>Resolve</button>
<svelte:boundary>
{#each [value.id] as s (s)}
{await wait()}
<div>{s}</div>
{/each}
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

@ -1,102 +0,0 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true,
async test({ assert, target }) {
const [add, shift] = target.querySelectorAll('button');
add.click();
await tick();
add.click();
await tick();
add.click();
await tick();
// TODO pending count / number of pushes is off
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=6 values.length=1 values=[1]</p>
<div>not keyed:
<div>1</div>
</div>
<div>keyed:
<div>1</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=4 values.length=2 values=[1,2]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=2 values.length=3 values=[1,2,3]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<p>pending=0 values.length=4 values=[1,2,3,4]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
`
);
}
});

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a, b] = target.querySelectorAll('button');
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 0</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
a.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 2</button><button>b 0</button><p>hello</p>`);
// if we don't skip over the never-resolving promise in the `else` block, we will never update
b.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>a 3</button><button>b 1</button><p>hello</p>`);
}
});

@ -0,0 +1,14 @@
<script>
let a = $state(0);
let b = $state(0);
let show = $state(true);
</script>
<button onclick={() => (a++, show = !show)}>a {a}</button>
<button onclick={() => (b++, show = !show)}>b {b}</button>
{#if show}
<p>hello</p>
{:else}
{await new Promise(() => {})}
{/if}

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
ssrHtml: 'works',
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'works');
}
});

@ -0,0 +1,7 @@
<script lang="ts">
const test = async () => "test";
await test();
$inspect("inspect after await shouldnt break builds");
</script>
works

@ -0,0 +1,9 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'aaa 1');
}
});

@ -0,0 +1,10 @@
<script>
let name = $derived(await new Promise((a) => a('aaa')));
function use() {
return () => 1;
}
const aa = use();
</script>
{name}
{aa()}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [a_b, b, resolve] = target.querySelectorAll('button');
a_b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
b.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 0_0</button> <button>b 0</button> <button>resolve</button> 0'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>a_b 1_2</button> <button>b 2</button> <button>resolve</button> 1'
);
}
});

@ -0,0 +1,17 @@
<script>
let a = $state(0);
let b = $state(0);
let deferreds = [];
function push(value) {
if (!value) return value;
return new Promise(resolve => {
deferreds.push(() => resolve(value));
});
}
</script>
<button onclick={() => {a++;b++}}>a_b {a}_{b}</button>
<button onclick={() => (b++)}>b {b}</button>
<button onclick={() => (deferreds.shift()?.())}>resolve</button>
{await push(a)}

@ -0,0 +1,205 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [add, shift, pop] = target.querySelectorAll('button');
add.click();
await tick();
add.click();
await tick();
add.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=6 values.length=1 values=[1]</p>
<div>not keyed:
<div>1</div>
</div>
<div>keyed:
<div>1</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=4 values.length=2 values=[1,2]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=2 values.length=3 values=[1,2,3]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
</div>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=0 values.length=4 values=[1,2,3,4]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
`
);
add.click();
await tick();
add.click();
await tick();
add.click();
await tick();
add.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=8 values.length=4 values=[1,2,3,4]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
`
);
// pop should have no effect until earlier promises have also resolved
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=6 values.length=4 values=[1,2,3,4]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
`
);
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>add</button>
<button>shift</button>
<button>pop</button>
<p>pending=0 values.length=8 values=[1,2,3,4,5,6,7,8]</p>
<div>not keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
</div>
<div>keyed:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
</div>
`
);
}
});

@ -11,18 +11,14 @@
return p.promise; return p.promise;
} }
function shift() {
const fn = queue.shift();
if (fn) fn();
}
function addValue() { function addValue() {
values.push(values.length+1); values.push(values.length+1);
} }
</script> </script>
<button onclick={addValue}>add</button> <button onclick={addValue}>add</button>
<button onclick={shift}>shift</button> <button onclick={() => queue.shift()?.()}>shift</button>
<button onclick={() => queue.pop()?.()}>pop</button>
<p> <p>
pending={$effect.pending()} pending={$effect.pending()}

@ -1,10 +1,8 @@
import { tick } from 'svelte'; import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
export default test({ export default test({
// TODO reinstate
skip: true,
compileOptions: { compileOptions: {
dev: true dev: true
}, },
@ -15,13 +13,10 @@ export default test({
await tick(); await tick();
assert.htmlEqual(target.innerHTML, '<button>a</button><button>b</button><h1>3</h1>'); assert.htmlEqual(target.innerHTML, '<button>a</button><button>b</button><h1>3</h1>');
assert.equal( assert.deepEqual(normalise_trace_logs(warnings), [
warnings[0], {
'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' log: 'Detected reactivity loss when reading `values.length`. This happens when state is read in an async function after an earlier `await`'
); }
]);
assert.equal(warnings[1].name, 'traced at');
assert.equal(warnings.length, 2);
} }
}); });

@ -2,9 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
// TODO reinstate this
skip: true,
compileOptions: { compileOptions: {
dev: true dev: true
}, },

@ -0,0 +1,5 @@
<script>
let { src } = $props();
</script>
<img {src} />

@ -0,0 +1,7 @@
<script>
let { children } = $props();
</script>
<a href="/">
{@render children()}
</a>

@ -0,0 +1,14 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(
target.innerHTML,
`<a href="/"><div>card</div> <img src="https://svelte.dev" /></a>`
);
}
});

@ -0,0 +1,11 @@
<script lang="ts">
import Image from "./Image.svelte";
import Link from "./Link.svelte";
let url = $derived(await 'https://svelte.dev');
</script>
<Link>
<div>card</div>
<Image src={url} />
</Link>

@ -6,7 +6,7 @@ var root = $.from_html(`<p> </p>`);
export default function Async_top_level_inspect_server($$anchor) { export default function Async_top_level_inspect_server($$anchor) {
var data; var data;
var $$promises = $.run([async () => data = await Promise.resolve(42),,]); var $$promises = $.run([async () => data = await Promise.resolve(42), () => void 0]);
var p = root(); var p = root();
var text = $.child(p, true); var text = $.child(p, true);

@ -3,7 +3,7 @@ import * as $ from 'svelte/internal/server';
export default function Async_top_level_inspect_server($$renderer) { export default function Async_top_level_inspect_server($$renderer) {
var data; var data;
var $$promises = $$renderer.run([async () => data = await Promise.resolve(42),,]); var $$promises = $$renderer.run([async () => data = await Promise.resolve(42), () => void 0]);
$$renderer.push(`<p>`); $$renderer.push(`<p>`);
$$renderer.async([$$promises[1]], ($$renderer) => $$renderer.push(() => $.escape(data))); $$renderer.async([$$promises[1]], ($$renderer) => $$renderer.push(() => $.escape(data)));

@ -32,7 +32,7 @@ export default function Main($$anchor) {
$.set_attribute(div_1, 'foobar', $0); $.set_attribute(div_1, 'foobar', $0);
$.set_attribute(svg_1, 'viewBox', $1); $.set_attribute(svg_1, 'viewBox', $1);
}, },
[y, y] [() => y(), () => y()]
); );
$.append($$anchor, fragment); $.append($$anchor, fragment);

@ -19,6 +19,6 @@ export default function Text_nodes_deriveds($$anchor) {
var text = $.child(p); var text = $.child(p);
$.reset(p); $.reset(p);
$.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [text1, text2]); $.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [() => text1(), () => text2()]);
$.append($$anchor, p); $.append($$anchor, p);
} }

@ -1030,9 +1030,11 @@ declare module 'svelte/compiler' {
/** /**
* If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component. * If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component.
* *
* You can also pass a function that receives `{ filename }` and returns a boolean.
*
* @default false * @default false
*/ */
customElement?: boolean; customElement?: boolean | ((options: { filename: string }) => boolean);
/** /**
* If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`. * If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
* *
@ -1058,8 +1060,10 @@ declare module 'svelte/compiler' {
* - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root. * - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root.
* - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. * - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files.
* This is always `'injected'` when compiling with `customElement` mode. * This is always `'injected'` when compiling with `customElement` mode.
*
* You can also pass a function that receives `{ filename }` and returns either `'injected'` or `'external'`.
*/ */
css?: 'injected' | 'external'; css?: 'injected' | 'external' | ((options: { filename: string }) => 'injected' | 'external');
/** /**
* A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS. * A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS.
* It defaults to returning `svelte-${hash(filename ?? css)}`. * It defaults to returning `svelte-${hash(filename ?? css)}`.
@ -1103,7 +1107,7 @@ declare module 'svelte/compiler' {
* which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead. * which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead.
* @default undefined * @default undefined
*/ */
runes?: boolean | undefined; runes?: boolean | undefined | ((options: { filename: string }) => boolean | undefined);
/** /**
* If `true`, exposes the Svelte major version in the browser by adding it to a `Set` stored in the global `window.__svelte.v`. * If `true`, exposes the Svelte major version in the browser by adding it to a `Set` stored in the global `window.__svelte.v`.
* *
@ -3017,9 +3021,11 @@ declare module 'svelte/types/compiler/interfaces' {
/** /**
* If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component. * If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component.
* *
* You can also pass a function that receives `{ filename }` and returns a boolean.
*
* @default false * @default false
*/ */
customElement?: boolean; customElement?: boolean | ((options: { filename: string }) => boolean);
/** /**
* If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`. * If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
* *
@ -3045,8 +3051,10 @@ declare module 'svelte/types/compiler/interfaces' {
* - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root. * - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root.
* - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. * - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files.
* This is always `'injected'` when compiling with `customElement` mode. * This is always `'injected'` when compiling with `customElement` mode.
*
* You can also pass a function that receives `{ filename }` and returns either `'injected'` or `'external'`.
*/ */
css?: 'injected' | 'external'; css?: 'injected' | 'external' | ((options: { filename: string }) => 'injected' | 'external');
/** /**
* A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS. * A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS.
* It defaults to returning `svelte-${hash(filename ?? css)}`. * It defaults to returning `svelte-${hash(filename ?? css)}`.
@ -3090,7 +3098,7 @@ declare module 'svelte/types/compiler/interfaces' {
* which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead. * which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead.
* @default undefined * @default undefined
*/ */
runes?: boolean | undefined; runes?: boolean | undefined | ((options: { filename: string }) => boolean | undefined);
/** /**
* If `true`, exposes the Svelte major version in the browser by adding it to a `Set` stored in the global `window.__svelte.v`. * If `true`, exposes the Svelte major version in the browser by adding it to a `Set` stored in the global `window.__svelte.v`.
* *

Loading…
Cancel
Save