Merge branch 'main' into elliott/variadic-snippets

pull/10320/head
S. Elliott Johnson 2 years ago committed by GitHub
commit 959abf6d6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
A transition's parameters are now evaluated when the transition is initialized.

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add `$state.frozen` rune

@ -0,0 +1,5 @@
---
'svelte': patch
---
Fix issue with assigning prop values as defaults of other props

@ -0,0 +1,5 @@
---
'svelte': patch
---
Fix interopability between backticks and templates

@ -13,6 +13,7 @@
"beige-flies-wash",
"beige-rabbits-shave",
"brave-walls-destroy",
"bright-peas-juggle",
"brown-spoons-boil",
"chatty-cups-drop",
"chatty-taxis-juggle",
@ -26,6 +27,7 @@
"curly-lizards-dream",
"dirty-garlics-design",
"dirty-tips-add",
"dry-clocks-grow",
"dull-mangos-wave",
"early-ads-tie",
"eight-steaks-shout",
@ -56,6 +58,7 @@
"hungry-dots-fry",
"hungry-tips-unite",
"itchy-beans-melt",
"itchy-kings-deliver",
"itchy-lions-wash",
"khaki-mails-draw",
"khaki-moose-arrive",
@ -69,6 +72,7 @@
"lazy-spiders-think",
"lemon-geese-drum",
"light-pens-watch",
"long-buckets-lay",
"long-crews-return",
"lovely-carpets-lick",
"lovely-items-turn",
@ -111,6 +115,7 @@
"slow-chefs-dream",
"small-papayas-laugh",
"smart-parents-swim",
"smart-zebras-pay",
"soft-clocks-remember",
"soft-geese-learn",
"sour-forks-stare",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: support hydrating around `<noscript>`

@ -1,5 +1,19 @@
# svelte
## 5.0.0-next.27
### Patch Changes
- fix: evaluate transition parameters when the transition runs ([#9836](https://github.com/sveltejs/svelte/pull/9836))
- feat: add `$state.frozen` rune ([#9851](https://github.com/sveltejs/svelte/pull/9851))
- fix: correctly transform prop fallback values that use other props ([#9985](https://github.com/sveltejs/svelte/pull/9985))
- fix: escape template literal characters in text sequences ([#9973](https://github.com/sveltejs/svelte/pull/9973))
- fix: inject comment in place of `<noscript>` in client output ([#9953](https://github.com/sveltejs/svelte/pull/9953))
## 5.0.0-next.26
### Patch Changes
@ -132,7 +146,7 @@
### Patch Changes
- feat: proxied staet ([#9739](https://github.com/sveltejs/svelte/pull/9739))
- feat: proxied state ([#9739](https://github.com/sveltejs/svelte/pull/9739))
- chore: more validation errors ([#9723](https://github.com/sveltejs/svelte/pull/9723))

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

@ -6,6 +6,7 @@ import { getLocator } from 'locate-character';
import { walk } from 'zimmerframe';
import { validate_component_options, validate_module_options } from './validate-options.js';
import { convert } from './legacy.js';
import { CompileError } from './errors.js';
export { default as preprocess } from './preprocess/index.js';
/**
@ -30,12 +31,8 @@ export function compile(source, options) {
const result = transform_component(analysis, source, combined_options);
return result;
} catch (e) {
if (/** @type {any} */ (e).name === 'CompileError') {
handle_compile_error(
/** @type {import('#compiler').CompileError} */ (e),
options.filename,
source
);
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
}
throw e;
@ -56,12 +53,8 @@ export function compileModule(source, options) {
const analysis = analyze_module(parse_acorn(source, false), validated);
return transform_module(analysis, source, validated);
} catch (e) {
if (/** @type {any} */ (e).name === 'CompileError') {
handle_compile_error(
/** @type {import('#compiler').CompileError} */ (e),
options.filename,
source
);
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
}
throw e;
@ -106,12 +99,8 @@ export function parse(source, options = {}) {
try {
ast = _parse(source);
} catch (e) {
if (/** @type {any} */ (e).name === 'CompileError') {
handle_compile_error(
/** @type {import('#compiler').CompileError} */ (e),
options.filename,
source
);
if (e instanceof CompileError) {
handle_compile_error(e, options.filename, source);
}
throw e;

@ -596,6 +596,7 @@ const legacy_scope_tweaker = {
);
if (
binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
(binding.kind === 'normal' && binding.declaration_kind === 'let')
) {
binding.kind = 'prop';
@ -647,18 +648,19 @@ const legacy_scope_tweaker = {
const runes_scope_js_tweaker = {
VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return;
if (get_rune(node.init, state.scope) === null) return;
const rune = get_rune(node.init, state.scope);
if (rune === null) return;
const callee = node.init.callee;
if (callee.type !== 'Identifier') return;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
const name = callee.name;
if (name !== '$state' && name !== '$derived') return;
if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived') return;
for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind = name === '$state' ? 'state' : 'derived';
binding.kind =
rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived';
}
}
};
@ -676,28 +678,31 @@ const runes_scope_tweaker = {
VariableDeclarator(node, { state }) {
const init = unwrap_ts_expression(node.init);
if (!init || init.type !== 'CallExpression') return;
if (get_rune(init, state.scope) === null) return;
const rune = get_rune(init, state.scope);
if (rune === null) return;
const callee = init.callee;
if (callee.type !== 'Identifier') return;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
const name = callee.name;
if (name !== '$state' && name !== '$derived' && name !== '$props') return;
if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived' && rune !== '$props')
return;
for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind =
name === '$state'
rune === '$state'
? 'state'
: name === '$derived'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived'
? 'derived'
: path.is_rest
? 'rest_prop'
: 'prop';
}
if (name === '$props') {
if (rune === '$props') {
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;
@ -909,7 +914,9 @@ const common_visitors = {
if (
node !== binding.node &&
(binding.kind === 'state' || binding.kind === 'derived') &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth
) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');

@ -349,6 +349,7 @@ export const validation = {
if (
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
@ -661,7 +662,7 @@ function validate_export(node, scope, name) {
error(node, 'invalid-derived-export');
}
if (binding.kind === 'state' && binding.reassigned) {
if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
error(node, 'invalid-state-export');
}
}
@ -835,7 +836,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
is_binding,
// This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
binding.kind !== 'state' && (binding.kind !== 'normal' || !binding.initial)
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
(binding.kind !== 'normal' || !binding.initial)
);
}
}

@ -233,7 +233,9 @@ export function client_component(source, analysis, options) {
'$.bind_prop',
b.id('$$props'),
b.literal(alias ?? name),
binding?.kind === 'state' ? b.call('$.get', b.id(name)) : b.id(name)
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? b.call('$.get', b.id(name))
: b.id(name)
)
);
});
@ -241,7 +243,8 @@ export function client_component(source, analysis, options) {
const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
const is_source =
binding?.kind === 'state' && (!state.analysis.immutable || binding.reassigned);
(binding?.kind === 'state' || binding?.kind === 'frozen_state') &&
(!state.analysis.immutable || binding.reassigned);
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
// Should we for code size reasons make it an init in runes mode and/or non-dev mode?

@ -59,7 +59,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
}
export interface StateField {
kind: 'state' | 'derived';
kind: 'state' | 'frozen_state' | 'derived';
id: PrivateIdentifier;
}

@ -82,7 +82,8 @@ export function serialize_get_binding(node, state) {
}
if (binding.prop_alias) {
return b.member(b.id('$$props'), b.id(binding.prop_alias));
const key = b.key(binding.prop_alias);
return b.member(b.id('$$props'), key, key.type === 'Literal');
}
return b.member(b.id('$$props'), node);
}
@ -92,7 +93,7 @@ export function serialize_get_binding(node, state) {
}
if (
(binding.kind === 'state' &&
((binding.kind === 'state' || binding.kind === 'frozen_state') &&
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
@ -162,40 +163,53 @@ export function serialize_set_binding(node, context, fallback) {
// Handle class private/public state assignment cases
while (left.type === 'MemberExpression') {
if (
left.object.type === 'ThisExpression' &&
left.property.type === 'PrivateIdentifier' &&
context.state.private_state.has(left.property.name)
) {
if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') {
const private_state = context.state.private_state.get(left.property.name);
const value = get_assignment_value(node, context);
if (state.in_constructor) {
// See if we should wrap value in $.proxy
if (context.state.analysis.runes && should_proxy(value)) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right = b.call('$.proxy', value);
return assignment;
if (private_state !== undefined) {
if (state.in_constructor) {
// See if we should wrap value in $.proxy
if (context.state.analysis.runes && should_proxy_or_freeze(value)) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value);
return assignment;
}
}
} else {
return b.call(
'$.set',
left,
context.state.analysis.runes && should_proxy_or_freeze(value)
? private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value)
: value
);
}
} else {
return b.call(
'$.set',
left,
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value
);
}
} else if (
left.object.type === 'ThisExpression' &&
left.property.type === 'Identifier' &&
context.state.public_state.has(left.property.name) &&
state.in_constructor
) {
const public_state = context.state.public_state.get(left.property.name);
const value = get_assignment_value(node, context);
// See if we should wrap value in $.proxy
if (context.state.analysis.runes && should_proxy(value)) {
if (
context.state.analysis.runes &&
public_state !== undefined &&
should_proxy_or_freeze(value)
) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right = b.call('$.proxy', value);
assignment.right =
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value);
return assignment;
}
}
@ -232,6 +246,7 @@ export function serialize_set_binding(node, context, fallback) {
if (
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
@ -249,12 +264,24 @@ export function serialize_set_binding(node, context, fallback) {
return b.call(left, value);
} else if (is_store) {
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
} else {
} else if (binding.kind === 'state') {
return b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value
context.state.analysis.runes && should_proxy_or_freeze(value)
? b.call('$.proxy', value)
: value
);
} else if (binding.kind === 'frozen_state') {
return b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes && should_proxy_or_freeze(value)
? b.call('$.freeze', value)
: value
);
} else {
return b.call('$.set', b.id(left_name), value);
}
} else {
if (is_store) {
@ -492,7 +519,7 @@ export function create_state_declarators(declarator, scope, value) {
}
/** @param {import('estree').Expression} node */
export function should_proxy(node) {
export function should_proxy_or_freeze(node) {
if (
!node ||
node.type === 'Literal' ||

@ -49,6 +49,7 @@ export const global_visitors = {
// use runtime functions for smaller output
if (
binding?.kind === 'state' ||
binding?.kind === 'frozen_state' ||
binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' ||

@ -2,7 +2,7 @@ import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import { create_state_declarators, get_prop_source, should_proxy } from '../utils.js';
import { create_state_declarators, get_prop_source, should_proxy_or_freeze } from '../utils.js';
import { unwrap_ts_expression } from '../../../../utils/ast.js';
/** @type {import('../types.js').ComponentVisitors} */
@ -29,10 +29,11 @@ export const javascript_visitors_runes = {
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope);
if (rune === '$state' || rune === '$derived') {
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
/** @type {import('../types.js').StateField} */
const field = {
kind: rune === '$state' ? 'state' : 'derived',
kind:
rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
@ -84,7 +85,9 @@ export const javascript_visitors_runes = {
value =
field.kind === 'state'
? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init)
? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.proxy', init) : init)
: field.kind === 'frozen_state'
? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.freeze', init) : init)
: b.call('$.derived', b.thunk(init));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
@ -114,6 +117,19 @@ export const javascript_visitors_runes = {
);
}
if (field.kind === 'frozen_state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, b.call('$.freeze', value)))]
)
);
}
if (field.kind === 'derived' && state.options.dev) {
body.push(
b.method(
@ -177,7 +193,7 @@ export const javascript_visitors_runes = {
if (property.value.type === 'AssignmentPattern') {
id = property.value.left;
initial = property.value.right;
initial = /** @type {import('estree').Expression} */ (visit(property.value.right));
}
assert.equal(id.type, 'Identifier');
@ -217,13 +233,24 @@ export const javascript_visitors_runes = {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (should_proxy(value)) {
if (should_proxy_or_freeze(value)) {
value = b.call('$.proxy', value);
}
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
value = b.call('$.source', value);
}
} else if (rune === '$state.frozen') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (should_proxy_or_freeze(value)) {
value = b.call('$.freeze', value);
}
if (binding.reassigned) {
value = b.call('$.source', value);
}
} else {
value = b.call('$.derived', b.thunk(value));
}

@ -33,6 +33,7 @@ import {
} from '../../../../../constants.js';
import { regex_is_valid_identifier } from '../../../patterns.js';
import { javascript_visitors_runes } from './javascript-runes.js';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
@ -1014,8 +1015,7 @@ function create_block(parent, name, nodes, context) {
context.path,
namespace,
context.state.preserve_whitespace,
context.state.options.preserveComments,
false
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0) {
@ -1245,6 +1245,7 @@ function serialize_event_handler(node, { state, visit }) {
if (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
@ -1630,7 +1631,7 @@ function serialize_template_literal(values, visit, state) {
const node = values[i];
if (node.type === 'Text') {
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += node.data;
last.value.raw += sanitize_template_string(node.data);
} else {
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
contains_call_expression = true;
@ -1825,6 +1826,11 @@ export const template_visitors = {
);
},
RegularElement(node, context) {
if (node.name === 'noscript') {
context.state.template.push('<!>');
return;
}
const metadata = context.state.metadata;
const child_metadata = {
...context.state.metadata,
@ -2016,8 +2022,7 @@ export const template_visitors = {
context.path,
child_metadata.namespace,
state.preserve_whitespace,
state.options.preserveComments,
false
state.options.preserveComments
);
for (const node of hoisted) {

@ -26,6 +26,7 @@ import { binding_properties } from '../../bindings.js';
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
import { remove_types } from '../typescript.js';
import { DOMBooleanAttributes } from '../../../../constants.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
/**
* @param {string} value
@ -117,14 +118,6 @@ function serialize_template(template, out = b.id('out')) {
return statements;
}
/**
* @param {string} str
* @returns {string}
*/
function sanitize_template_string(str) {
return str.replace(/(`|\${|\\)/g, '\\$1');
}
/**
* Processes an array of template nodes, joining sibling text/expression nodes and
* recursing into child nodes.
@ -194,7 +187,10 @@ function process_children(nodes, parent, { visit, state }) {
const node = sequence[i];
if (node.type === 'Text' || node.type === 'Comment') {
let last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data);
last.value.raw +=
node.type === 'Comment'
? `<!--${node.data}-->`
: sanitize_template_string(escape_html(node.data));
} else if (node.type === 'Anchor') {
expressions.push(node.id);
quasis.push(b.quasi('', i + 1 === sequence.length));
@ -252,8 +248,7 @@ function create_block(parent, nodes, context, anchor) {
context.path,
namespace,
context.state.preserve_whitespace,
context.state.options.preserveComments,
true
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0 && !anchor) {
@ -446,6 +441,7 @@ function serialize_set_binding(node, context, fallback) {
if (
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
@ -558,7 +554,7 @@ const javascript_visitors_runes = {
if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope);
if (rune === '$state' || rune === '$derived') {
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
return {
...node,
value:
@ -1182,8 +1178,7 @@ const template_visitors = {
inner_context.path,
metadata.namespace,
state.preserve_whitespace,
state.options.preserveComments,
true
state.options.preserveComments
);
for (const node of hoisted) {

@ -83,7 +83,6 @@ export function is_hoistable_function(node) {
* @param {import('#compiler').Namespace} namespace
* @param {boolean} preserve_whitespace
* @param {boolean} preserve_comments
* @param {boolean} preserve_noscript
*/
export function clean_nodes(
parent,
@ -91,8 +90,7 @@ export function clean_nodes(
path,
namespace = 'html',
preserve_whitespace,
preserve_comments,
preserve_noscript
preserve_comments
) {
/** @type {import('#compiler').SvelteNode[]} */
const hoisted = [];
@ -105,10 +103,6 @@ export function clean_nodes(
continue;
}
if (node.type === 'RegularElement' && node.name === 'noscript' && !preserve_noscript) {
continue;
}
if (
node.type === 'ConstTag' ||
node.type === 'DebugTag' ||

@ -72,6 +72,7 @@ export const ElementBindings = [
export const Runes = /** @type {const} */ ([
'$state',
'$state.frozen',
'$props',
'$derived',
'$effect',

@ -710,5 +710,5 @@ export function get_rune(node, scope) {
const binding = scope.get(n.name);
if (binding !== null) return null; // rune name, but references a variable or store
return /** @type {Runes[number] | null} */ (joined);
return /** @type {typeof Runes[number] | null} */ (joined);
}

@ -258,6 +258,7 @@ export interface Binding {
| 'prop'
| 'rest_prop'
| 'state'
| 'frozen_state'
| 'derived'
| 'each'
| 'store_sub'

@ -1,4 +1,5 @@
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
/**
* @param {Array<import('estree').Expression | import('estree').SpreadElement | null>} elements
@ -314,7 +315,7 @@ export function prop_def(key, value, computed = false, is_static = false) {
* @returns {import('estree').TemplateElement}
*/
export function quasi(cooked, tail = false) {
const raw = cooked.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
const raw = sanitize_template_string(cooked);
return { type: 'TemplateElement', value: { raw, cooked }, tail };
}

@ -0,0 +1,7 @@
/**
* @param {string} str
* @returns {string}
*/
export function sanitize_template_string(str) {
return str.replace(/(`|\${|\\)/g, '\\$1');
}

@ -1,4 +1,4 @@
import { define_property } from '../utils.js';
import { define_property, is_frozen } from '../utils.js';
import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
/**
@ -6,8 +6,6 @@ import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
* @typedef {T & { [READONLY_SYMBOL]: Proxy<T> }} StateObject
*/
const is_frozen = Object.isFrozen;
/**
* Expects a value that was wrapped with `proxy` and makes it readonly.
*

@ -1,7 +1,7 @@
import { DEV } from 'esm-env';
import { subscribe_to_store } from '../../store/utils.js';
import { EMPTY_FUNC, run_all } from '../common.js';
import { get_descriptor, get_descriptors, is_array } from './utils.js';
import { get_descriptor, get_descriptors, is_array, is_frozen, object_freeze } from './utils.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
@ -9,7 +9,7 @@ import {
PROPS_IS_UPDATED
} from '../../constants.js';
import { readonly } from './proxy/readonly.js';
import { proxy, unstate } from './proxy/proxy.js';
import { READONLY_SYMBOL, STATE_SYMBOL, proxy, unstate } from './proxy/proxy.js';
export const SOURCE = 1;
export const DERIVED = 1 << 1;
@ -1933,3 +1933,25 @@ export function proxy_rest_array(items) {
}
});
}
/**
* Expects a value that was wrapped with `freeze` and makes it frozen.
* @template {import('./proxy/proxy.js').StateObject} T
* @param {T} value
* @returns {Readonly<Record<string | symbol, any>>}
*/
export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If the object is already proxified, then unstate the value
if (STATE_SYMBOL in value) {
return object_freeze(unstate(value));
}
// If the value is already read-only then just use that
if (DEV && READONLY_SYMBOL in value) {
return value;
}
// Otherwise freeze the object
object_freeze(value);
}
return value;
}

@ -421,7 +421,6 @@ function is_transition_block(block) {
export function bind_transition(dom, get_transition_fn, props_fn, direction, global) {
const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect);
const block = current_block;
const props = props_fn === null ? {} : props_fn();
let can_show_intro_on_mount = true;
let can_apply_lazy_transitions = false;
@ -467,8 +466,9 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
const transition_fn = get_transition_fn();
/** @param {DOMRect} [from] */
const init = (from) =>
untrack(() =>
direction === 'key'
untrack(() => {
const props = props_fn === null ? {} : props_fn();
return direction === 'key'
? /** @type {import('./types.js').AnimateFn<any>} */ (transition_fn)(
dom,
{ from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() },
@ -477,8 +477,8 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
)
: /** @type {import('./types.js').TransitionFn<any>} */ (transition_fn)(dom, props, {
direction
})
);
});
});
transition = create_transition(dom, init, direction, transition_effect);
const is_intro = direction === 'in';

@ -5,6 +5,8 @@ export var array_from = Array.from;
export var object_keys = Object.keys;
export var object_entries = Object.entries;
export var object_assign = Object.assign;
export var is_frozen = Object.isFrozen;
export var object_freeze = Object.freeze;
export var define_property = Object.defineProperty;
export var get_descriptor = Object.getOwnPropertyDescriptor;
export var get_descriptors = Object.getOwnPropertyDescriptors;

@ -38,7 +38,8 @@ export {
inspect,
unwrap,
proxy_rest_array,
thunkspread
thunkspread,
freeze
} from './client/runtime.js';
export * from './client/each.js';

@ -17,6 +17,33 @@ declare module '*.svelte' {
declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined;
declare namespace $state {
/**
* Declares reactive read-only state that is shallowly immutable.
*
* Example:
* ```ts
* <script>
* let items = $state.frozen([0]);
*
* const addItem = () => {
* items = [...items, items.length];
* };
* </script>
*
* <button on:click={addItem}>
* {items.join(', ')}
* </button>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state-raw
*
* @param initial The initial value
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
}
/**
* Declares derived state, i.e. one that depends on other state variables.
* The expression inside `$derived(...)` should be free of side-effects.

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

@ -0,0 +1,2 @@
<!--ssr:0--><noscript>JavaScript is required for this site.</noscript>
<h1>Hello!</h1><p>Count: 0</p><!--ssr:0-->

@ -0,0 +1,9 @@
<script>
import { onMount } from "svelte";
let count = 0;
onMount(() => count++);
</script>
<noscript>JavaScript is required for this site.</noscript>
<h1>Hello!</h1><p>Count: {count}</p>

@ -0,0 +1,44 @@
import { test } from '../../test';
export default test({
html: `
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
<p>6</p>
<h1>Bag'ol stores</h1>
<p>6</p>
<p></p>
<p></p>
<button>Click me!</button>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await button?.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<p>7</p>
<p>8</p>
<p>9</p>
<p>10</p>
<p>11</p>
<p>12</p>
<h1>Bag'ol stores</h1>
<p>12</p>
<p>14</p>
<p>15</p>
<button>Click me!</button>
`
);
}
});

@ -0,0 +1,42 @@
<script>
import { get, writable } from 'svelte/store'
let bagOlStores = writable([1, 2, 3, writable(4), writable(5), writable(6)]);
let firstNonStore, secondNonStore, thirdNonStore, firstStore, secondStore, thirdStore;
([firstNonStore, secondNonStore, thirdNonStore, firstStore, secondStore, thirdStore] = $bagOlStores);
function changeStores() {
$bagOlStores = ([
firstNonStore,
secondNonStore,
thirdNonStore,
firstStore,
$secondStore,
thirdStore
] = [
7,
8,
9,
writable(10),
11,
writable(12),
writable(14),
writable(15)
]);
}
</script>
<p>{firstNonStore}</p>
<p>{secondNonStore}</p>
<p>{thirdNonStore}</p>
<p>{$firstStore}</p>
<p>{$secondStore}</p>
<p>{$thirdStore}</p>
<h1>Bag'ol stores</h1>
<p>{get($bagOlStores[5])}</p>
<p>{get($bagOlStores[6])}</p>
<p>{get($bagOlStores[7])}</p>
<button on:click={changeStores}>Click me!</button>

@ -0,0 +1,44 @@
import { test } from '../../test';
export default test({
html: `
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
<p>6</p>
<h1>Bag'ol stores</h1>
<p>4</p>
<p>5</p>
<p>6</p>
<button>Click me!</button>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await button?.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<p>7</p>
<p>8</p>
<p>9</p>
<p>10</p>
<p>11</p>
<p>12</p>
<h1>Bag'ol stores</h1>
<p>14</p>
<p>13</p>
<p>12</p>
<button>Click me!</button>
`
);
}
});

@ -0,0 +1,48 @@
<script>
import { get, writable } from 'svelte/store'
let bagOlStores = writable({
firstNonStore: 1,
secondNonStore: 2,
thirdNonStore: 3,
firstStore: writable(4),
secondStore: writable(5),
thirdStore: writable(6)
});
let { firstNonStore, secondNonStore, thirdNonStore, firstStore, secondStore, thirdStore } = $bagOlStores;
function changeStores() {
$bagOlStores = ({
thirdStore,
$secondStore,
$firstStore,
firstNonStore,
secondNonStore,
thirdNonStore,
} = {
firstNonStore: 7,
secondNonStore: 8,
thirdNonStore: 9,
$firstStore: 10,
$secondStore: 11,
firstStore: writable(14),
secondStore: writable(13),
thirdStore: writable(12)
});
}
</script>
<p>{firstNonStore}</p>
<p>{secondNonStore}</p>
<p>{thirdNonStore}</p>
<p>{$firstStore}</p>
<p>{$secondStore}</p>
<p>{$thirdStore}</p>
<h1>Bag'ol stores</h1>
<p>{get($bagOlStores.firstStore)}</p>
<p>{get($bagOlStores.secondStore)}</p>
<p>{get($bagOlStores.thirdStore)}</p>
<button on:click={changeStores}>Click me!</button>

@ -0,0 +1,9 @@
<script>
let z = 8;
let { a, b = a, c = b * b, d = z * b + c } = $props();
</script>
<p>{a}</p>
<p>{b}</p>
<p>{c}</p>
<p>{d}</p>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<p>5</p><p>5</p><p>25</p><p>65</p>`
});

@ -0,0 +1,5 @@
<script>
import Test from './Test.svelte'
</script>
<Test a={5} />

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<div>/ $clicks: 0 `tim$es` \\</div><div>$dollars `backticks` pyramid /\\</div>'
});

@ -0,0 +1,6 @@
<div>
/ $clicks: {0} `tim${"e"}s` \
</div>
<div>
$dollars `backticks` pyramid /\
</div>

@ -0,0 +1,22 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
html: `<button>0</button>`,
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
assert.deepEqual(log, ['read only', 'read only']);
}
});

@ -0,0 +1,16 @@
<script>
import { log } from './log.js';
class Counter {
count = $state.frozen({ a: 0 });
}
const counter = new Counter();
</script>
<button on:click={() => {
try {
counter.count.a++
} catch (e) {
log.push('read only')
}
}}>{counter.count.a}</button>

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,8 @@
<script>
class Counter {
count = $state.frozen(0);
}
const counter = new Counter();
</script>
<button on:click={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,22 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
html: `<button>0</button>`,
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
assert.deepEqual(log, ['read only', 'read only']);
}
});

@ -0,0 +1,27 @@
<script>
import { log } from './log.js';
class Counter {
#count = $state.frozen();
constructor(initial_count) {
this.#count = { a: initial_count };
}
get count() {
return this.#count;
}
set count(val) {
this.#count = val;
}
}
const counter = new Counter(0);
</script>
<button on:click={() => {
try {
counter.count.a++
} catch (e) {
log.push('read only')
}
}}>{counter.count.a}</button>

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,19 @@
<script>
class Counter {
#count = $state.frozen(0);
constructor(initial_count) {
this.#count = initial_count;
}
get count() {
return this.#count;
}
set count(val) {
this.#count = val;
}
}
const counter = new Counter(0);
</script>
<button on:click={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,38 @@
import { test } from '../../test';
export default test({
html: `
<button>Update</button>
<p>0</p>
<p>b</p>
<p>true</p>
<p>0</p>
<p>10</p>
<p>12</p>
<p>15</p>
<p>16</p>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await button?.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<button>Update</button>
<p>5</p>
<p>d</p>
<p>false</p>
<p>3</p>
<p>100</p>
<p>120</p>
<p>25</p>
<p>26</p>
`
);
}
});

@ -0,0 +1,23 @@
<script>
let a = $state(0);
let b = $state("b");
let c = $state(true);
let d = $state([]);
let e = $state({ x: 10, y: 12 });
let f = $state({ w: 15, v: 16 });
function change() {
({ d, e, g: [f.w, f.v] } = { d: ([a, b, c] = [5, "d", false]), e: { x: 100, y: 120 }, g: [25, 26] });
}
</script>
<button on:click={change}>Update</button>
<p>{a}</p>
<p>{b}</p>
<p>{c}</p>
<p>{d.length}</p>
<p>{e.x}</p>
<p>{e.y}</p>
<p>{f.w}</p>
<p>{f.v}</p>

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

@ -0,0 +1,11 @@
<script>
let items = $state.frozen([0]);
const addItem = () => {
items = [...items, items.length];
};
</script>
<button on:click={addItem}>
{items.join(', ')}
</button>

@ -0,0 +1,17 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const [b1, b2] = target.querySelectorAll('button');
b1.click();
b2.click();
await Promise.resolve();
assert.deepEqual(log, [0, 1]);
}
});

@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];

@ -0,0 +1,13 @@
<script>
import { log } from './log.js';
let x = $state.frozen(0);
let y = $state.frozen(0);
$effect(() => {
log.push(x);
});
</script>
<button on:click={() => x++}>{x}</button>
<button on:click={() => y++}>{y}</button>

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
test({ assert, component, target }) {
const div = /** @type {HTMLDivElement & { foo?: number }} */ (target.querySelector('div'));
assert.equal(div.foo, undefined);
component.foo = 2;
component.visible = false;
assert.equal(div.foo, 2);
}
});

@ -0,0 +1,12 @@
<script>
const { visible = true, foo = 1 } = $props();
function bar(node, params) {
node.foo = params;
return () => ({});
}
</script>
{#if visible}
<div transition:bar={foo}></div>
{/if}

@ -205,17 +205,23 @@
return {
from: word.from - 1,
options: [
{ label: '$state', type: 'keyword', boost: 10 },
{ label: '$props', type: 'keyword', boost: 9 },
{ label: '$derived', type: 'keyword', boost: 8 },
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 7 }),
{ label: '$state', type: 'keyword', boost: 9 },
{ label: '$props', type: 'keyword', boost: 8 },
{ label: '$derived', type: 'keyword', boost: 7 },
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 6 }),
snip('$effect.pre(() => {\n\t${}\n});', {
label: '$effect.pre',
type: 'keyword',
boost: 6
boost: 5
}),
{ label: '$effect.active', type: 'keyword', boost: 5 },
{ label: '$inspect', type: 'keyword', boost: 4 }
{ label: '$state.frozen', type: 'keyword', boost: 4 },
snip('$effect.root(() => {\n\t${}\n});', {
label: '$effect.root',
type: 'keyword',
boost: 3
}),
{ label: '$effect.active', type: 'keyword', boost: 2 },
{ label: '$inspect', type: 'keyword', boost: 1 }
]
};
}

@ -64,6 +64,35 @@ Objects and arrays [are made reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21
In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component.
## `$state.frozen`
State declared with `$state.frozen` cannot be mutated; it can only be _reassigned_. In other words, rather than assigning to a property of an object, or using an array method like `push`, replace the object or array altogether if you'd like to update it:
```diff
<script>
- let numbers = $state([1, 2, 3]);
+ let numbers = $state.frozen([1, 2, 3]);
</script>
-<button onclick={() => numbers.push(numbers.length + 1)}>
+<button onclick={() => numbers = [...numbers, numbers.length + 1]}>
push
</button>
-<button onclick={() => numbers.pop()}> pop </button>
+<button onclick={() => numbers = numbers.slice(0, -1)}> pop </button>
<p>
{numbers.join(' + ') || 0}
=
{numbers.reduce((a, b) => a + b, 0)}
</p>
```
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that frozen state can _contain_ reactive state (for example, a frozen array of reactive objects).
> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead.
## `$derived`
Derived state is declared with the `$derived` rune:

Loading…
Cancel
Save