Merge branch 'main' into async-derived-coordinate-batches

async-derived-coordinate-batches
Simon Holthausen 5 months ago
commit 73035296f4

@ -1,5 +1,15 @@
# 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

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

@ -23,6 +23,7 @@ export { print } from './print/index.js';
export function compile(source, options) {
source = remove_bom(source);
state.reset({ warning: options.warningFilter, filename: options.filename });
const validated = validate_component_options(options, '');
let parsed = _parse(source);
@ -33,7 +34,9 @@ export function compile(source, options) {
const combined_options = {
...validated,
...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) {

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

@ -345,6 +345,8 @@ export function analyze_component(root, source, options) {
let synthetic_stores_legacy_check = [];
const runes_option = options.runes?.({ filename: options.filename });
// create synthetic bindings for store subscriptions
for (const [name, references] of module.scope.references) {
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
// is referencing a rune and not a global store.
if (
options.runes === false ||
runes_option === false ||
!is_rune(name) ||
(declaration !== null &&
// 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);
}
if (options.runes !== false) {
if (runes_option !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) {
e.global_reference_invalid(references[0].node, 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 runes =
options.runes ??
runes_option ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
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);
@ -491,7 +496,7 @@ export function analyze_component(root, source, options) {
maybe_runes:
!runes &&
// if they explicitly disabled runes, use the legacy behavior
options.runes !== false &&
runes_option !== false &&
![...module.scope.references.keys()].some((name) =>
['$$props', '$$restProps'].includes(name)
) &&
@ -523,8 +528,8 @@ export function analyze_component(root, source, options) {
needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: is_custom_element,
inject_styles: options.css === 'injected' || is_custom_element,
custom_element,
inject_styles: css === 'injected' || is_custom_element,
accessors:
is_custom_element ||
(runes ? false : !!options.accessors) ||
@ -680,7 +685,7 @@ export function analyze_component(root, source, options) {
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);
}

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

@ -65,7 +65,7 @@ export function render_stylesheet(source, analysis, options) {
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()} */`;
}

@ -300,7 +300,7 @@ export function server_component(analysis, options) {
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 code = b.literal(render_stylesheet(analysis.source, analysis, options).code);

@ -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.
*
* You can also pass a function that receives `{ filename }` and returns a boolean.
*
* @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`.
*
@ -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.
* - `'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.
*
* 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.
* It defaults to returning `svelte-${hash(filename ?? css)}`.
@ -142,7 +146,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.
* @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`.
*
@ -248,18 +252,22 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
Required<CompileOptions>,
| keyof ModuleCompileOptions
| 'name'
| 'customElement'
| 'compatibility'
| 'outputFilename'
| 'cssOutputFilename'
| 'sourcemap'
| 'css'
| 'runes'
> & {
name: CompileOptions['name'];
customElement: (options: { filename: string }) => boolean;
outputFilename: CompileOptions['outputFilename'];
cssOutputFilename: CompileOptions['cssOutputFilename'];
sourcemap: CompileOptions['sourcemap'];
compatibility: Required<Required<CompileOptions>['compatibility']>;
runes: CompileOptions['runes'];
css: (options: { filename: string }) => 'injected' | 'external';
runes: (options: { filename: string }) => boolean | undefined;
customElementOptions: AST.SvelteOptions['customElement'];
hmr: CompileOptions['hmr'];
};

@ -51,24 +51,28 @@ const common_options = {
const component_options = {
accessors: deprecate(w.options_deprecated_accessors, boolean(false)),
css: validator('external', (input) => {
if (input === true || input === false) {
throw_error(
'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true'
);
}
if (input === 'none') {
throw_error(
'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.'
);
}
/** @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) {
throw_error(
'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true'
);
}
if (input === 'none') {
throw_error(
'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.'
);
}
if (input !== 'external' && input !== 'injected') {
throw_error(`css should be either "external" (default, recommended) or "injected"`);
}
if (input !== 'external' && input !== 'injected') {
throw_error(`css should be either "external" (default, recommended) or "injected"`);
}
return input;
}),
return /** @type {'external' | 'injected'} */ (input);
}
),
cssHash: fun(({ css, filename, hash }) => {
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
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),
@ -107,7 +121,8 @@ const component_options = {
preserveWhitespace: boolean(false),
runes: boolean(undefined),
/** @type {Validator<boolean | undefined | (() => boolean | undefined), () => boolean | undefined>} */
runes: parametric(() => /** @type {boolean | undefined} */ (undefined)),
hmr: boolean(false),
@ -318,6 +333,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 */
function throw_error(msg) {
e.options_invalid_value(null, msg);

@ -19,10 +19,10 @@ import {
import { Batch, current_batch } from './batch.js';
import {
async_derived,
current_async_effect,
reactivity_loss_tracker,
derived,
derived_safe_equal,
set_from_async_derived
set_reactivity_loss_tracker
} from './deriveds.js';
import { aborted } from './effects.js';
@ -131,7 +131,7 @@ export function capture() {
}
if (DEV) {
set_from_async_derived(null);
set_reactivity_loss_tracker(null);
set_dev_stack(previous_dev_stack);
}
};
@ -163,11 +163,11 @@ export async function save(promise) {
* @returns {Promise<() => T>}
*/
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;
return () => {
set_from_async_derived(previous_async_effect);
set_reactivity_loss_tracker(previous_async_effect);
return value;
};
}
@ -224,7 +224,7 @@ export function unset_context(deactivate_batch = true) {
if (deactivate_batch) current_batch?.deactivate();
if (DEV) {
set_from_async_derived(null);
set_reactivity_loss_tracker(null);
set_dev_stack(null);
}
}

@ -52,12 +52,16 @@ import { increment_pending, unset_context } from './async.js';
import { deferred, includes, noop } from '../../shared/utils.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 */
export function set_from_async_derived(v) {
current_async_effect = v;
/** @param {{ effect: Effect, warned: boolean } | null} v */
export function set_reactivity_loss_tracker(v) {
reactivity_loss_tracker = v;
}
export const recent_async_deriveds = new Set();
@ -144,7 +148,12 @@ export function async_derived(fn, label, location) {
}
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);
@ -173,7 +182,9 @@ export function async_derived(fn, label, location) {
unset_context();
}
if (DEV) current_async_effect = null;
if (DEV) {
reactivity_loss_tracker = null;
}
if (should_suspend) {
// we only increment the batch's pending state for updates, not creation, otherwise
@ -203,7 +214,9 @@ export function async_derived(fn, label, location) {
* @param {unknown} error
*/
const handler = async (value, error = undefined) => {
if (DEV) current_async_effect = null;
if (DEV) {
reactivity_loss_tracker = null;
}
if (decrement_pending) {
/** @type {Promise<unknown>[]} */

@ -27,6 +27,7 @@ import {
} from './constants.js';
import { old_values } from './reactivity/sources.js';
import {
reactivity_loss_tracker,
execute_derived,
freeze_derived_effects,
recent_async_deriveds,
@ -60,6 +61,7 @@ import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
import { without_reactive_context } from './dom/elements/bindings/shared.js';
import { set_signal_status, update_derived_status } from './reactivity/status.js';
import * as w from './warnings.js';
let is_updating_effect = false;
@ -570,19 +572,20 @@ export function get(signal) {
}
if (DEV) {
// TODO reinstate this, but make it actually work
// if (current_async_effect) {
// var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0;
// var was_read = current_async_effect.deps?.includes(signal);
if (
!untracking &&
reactivity_loss_tracker &&
!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');
// // eslint-disable-next-line no-console
// if (trace) console.warn(trace);
// }
// }
var trace = get_error('traced at');
// eslint-disable-next-line no-console
if (trace) console.warn(trace);
}
recent_async_deriveds.delete(signal);
@ -597,7 +600,7 @@ export function get(signal) {
if (signal.trace) {
signal.trace();
} else {
var trace = get_error('traced at');
trace = get_error('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);

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

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

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

@ -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.
*
* You can also pass a function that receives `{ filename }` and returns a boolean.
*
* @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`.
*
@ -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.
* - `'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.
*
* 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.
* It defaults to returning `svelte-${hash(filename ?? css)}`.
@ -1099,7 +1103,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.
* @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`.
*
@ -3006,9 +3010,11 @@ declare module 'svelte/types/compiler/interfaces' {
/**
* 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
*/
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`.
*
@ -3034,8 +3040,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.
* - `'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.
*
* 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.
* It defaults to returning `svelte-${hash(filename ?? css)}`.
@ -3075,7 +3083,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.
* @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`.
*

Loading…
Cancel
Save