pull/12335/head
Rich Harris 2 years ago
commit bdf894607f

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: correct start of `{:else if}` and `{:else}`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reorder reactive statements during migration

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: play transitions on `mount` by default

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `<select>` `<option value>` behavior consistent

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: stricter control flow syntax validation in runes mode

@ -78,6 +78,7 @@
"cold-cheetahs-judge", "cold-cheetahs-judge",
"cold-lamps-accept", "cold-lamps-accept",
"cold-masks-learn", "cold-masks-learn",
"cold-teachers-turn",
"cool-actors-tan", "cool-actors-tan",
"cool-ants-leave", "cool-ants-leave",
"cool-comics-marry", "cool-comics-marry",
@ -133,6 +134,7 @@
"eighty-lizards-notice", "eighty-lizards-notice",
"eleven-avocados-walk", "eleven-avocados-walk",
"eleven-beers-yell", "eleven-beers-yell",
"eleven-cows-judge",
"eleven-cycles-applaud", "eleven-cycles-applaud",
"eleven-hounds-pump", "eleven-hounds-pump",
"empty-bags-heal", "empty-bags-heal",
@ -157,6 +159,7 @@
"fast-donkeys-pay", "fast-donkeys-pay",
"fast-penguins-matter", "fast-penguins-matter",
"fast-weeks-clean", "fast-weeks-clean",
"few-cheetahs-taste",
"few-clouds-shop", "few-clouds-shop",
"few-mugs-fail", "few-mugs-fail",
"few-teachers-know", "few-teachers-know",
@ -242,6 +245,7 @@
"heavy-ducks-leave", "heavy-ducks-leave",
"heavy-ears-rule", "heavy-ears-rule",
"hip-balloons-begin", "hip-balloons-begin",
"hip-garlics-tap",
"hip-pumpkins-boil", "hip-pumpkins-boil",
"honest-buses-add", "honest-buses-add",
"honest-dragons-turn", "honest-dragons-turn",
@ -302,6 +306,7 @@
"lemon-trees-act", "lemon-trees-act",
"light-badgers-glow", "light-badgers-glow",
"light-days-clean", "light-days-clean",
"light-hounds-carry",
"light-humans-hang", "light-humans-hang",
"light-penguins-invent", "light-penguins-invent",
"light-pens-watch", "light-pens-watch",
@ -340,6 +345,7 @@
"mighty-cooks-scream", "mighty-cooks-scream",
"mighty-files-hammer", "mighty-files-hammer",
"mighty-frogs-obey", "mighty-frogs-obey",
"mighty-paws-smash",
"mighty-shoes-nail", "mighty-shoes-nail",
"modern-apricots-promise", "modern-apricots-promise",
"modern-fishes-double", "modern-fishes-double",
@ -398,6 +404,7 @@
"pink-bikes-agree", "pink-bikes-agree",
"pink-goats-promise", "pink-goats-promise",
"pink-mayflies-tie", "pink-mayflies-tie",
"plenty-clouds-reply",
"plenty-elephants-fry", "plenty-elephants-fry",
"plenty-starfishes-dress", "plenty-starfishes-dress",
"plenty-zoos-fix", "plenty-zoos-fix",
@ -498,6 +505,7 @@
"silver-sheep-knock", "silver-sheep-knock",
"six-bears-trade", "six-bears-trade",
"six-boats-shave", "six-boats-shave",
"six-chicken-kneel",
"six-gorillas-obey", "six-gorillas-obey",
"sixty-items-crash", "sixty-items-crash",
"sixty-numbers-hope", "sixty-numbers-hope",
@ -517,6 +525,7 @@
"slow-plums-chew", "slow-plums-chew",
"slow-wombats-reply", "slow-wombats-reply",
"small-apples-eat", "small-apples-eat",
"small-owls-remain",
"small-papayas-laugh", "small-papayas-laugh",
"small-sheep-type", "small-sheep-type",
"small-spiders-fail", "small-spiders-fail",
@ -527,6 +536,7 @@
"smart-spiders-fetch", "smart-spiders-fetch",
"smart-turkeys-tell", "smart-turkeys-tell",
"smart-zebras-pay", "smart-zebras-pay",
"smart-zoos-vanish",
"smooth-kids-protect", "smooth-kids-protect",
"smooth-pens-protect", "smooth-pens-protect",
"smooth-rings-rush", "smooth-rings-rush",
@ -621,6 +631,7 @@
"tidy-chefs-taste", "tidy-chefs-taste",
"tidy-deers-hope", "tidy-deers-hope",
"tidy-fans-relax", "tidy-fans-relax",
"tidy-lizards-happen",
"tidy-starfishes-allow", "tidy-starfishes-allow",
"tiny-kings-whisper", "tiny-kings-whisper",
"tiny-meals-deliver", "tiny-meals-deliver",
@ -658,9 +669,11 @@
"wet-wombats-repeat", "wet-wombats-repeat",
"wicked-clouds-exercise", "wicked-clouds-exercise",
"wicked-doors-train", "wicked-doors-train",
"wicked-emus-drive",
"wicked-hairs-cheer", "wicked-hairs-cheer",
"wicked-wasps-allow", "wicked-wasps-allow",
"wicked-ways-reply", "wicked-ways-reply",
"wild-cows-chew",
"wild-foxes-wonder", "wild-foxes-wonder",
"wild-moose-compare", "wild-moose-compare",
"wise-apples-care", "wise-apples-care",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: resolve legacy component props equality for mutations

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: align warning and error objects, add frame property

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reconnected deep derived signals to graph

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `$state` component exports settable

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure `$effect.root` is ignored on the server

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: enable local transitions on `svelte:element`

@ -1,5 +1,43 @@
# svelte # svelte
## 5.0.0-next.178
### Patch Changes
- fix: reconnected deep derived signals to graph ([#12350](https://github.com/sveltejs/svelte/pull/12350))
## 5.0.0-next.177
### Patch Changes
- breaking: play transitions on `mount` by default ([#12351](https://github.com/sveltejs/svelte/pull/12351))
- fix: make `<select>` `<option value>` behavior consistent ([#12316](https://github.com/sveltejs/svelte/pull/12316))
- chore: stricter control flow syntax validation in runes mode ([#12342](https://github.com/sveltejs/svelte/pull/12342))
- fix: resolve legacy component props equality for mutations ([#12348](https://github.com/sveltejs/svelte/pull/12348))
- fix: make `$state` component exports settable ([#12345](https://github.com/sveltejs/svelte/pull/12345))
## 5.0.0-next.176
### Patch Changes
- fix: correct start of `{:else if}` and `{:else}` ([#12043](https://github.com/sveltejs/svelte/pull/12043))
- fix: reverse parent/child order in invalid HTML warning ([#12336](https://github.com/sveltejs/svelte/pull/12336))
- fix: reorder reactive statements during migration ([#12329](https://github.com/sveltejs/svelte/pull/12329))
- feat: better `<svelte:element>` SSR output ([#12339](https://github.com/sveltejs/svelte/pull/12339))
- chore: align warning and error objects, add frame property ([#12326](https://github.com/sveltejs/svelte/pull/12326))
- fix: ensure `$effect.root` is ignored on the server ([#12332](https://github.com/sveltejs/svelte/pull/12332))
- fix: enable local transitions on `svelte:element` ([#12346](https://github.com/sveltejs/svelte/pull/12346))
## 5.0.0-next.175 ## 5.0.0-next.175
### Patch Changes ### Patch Changes

@ -88,6 +88,10 @@
> Block was left open > Block was left open
## block_unexpected_character
> Expected a `%character%` character immediately following the opening bracket
## block_unexpected_close ## block_unexpected_close
> Unexpected block closing tag > Unexpected block closing tag

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

@ -1,54 +1,17 @@
/** @import { Location } from 'locate-character' */ import { CompileDiagnostic } from './utils/compile_diagnostic.js';
import * as state from './state.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
export class InternalCompileError extends Error { export class InternalCompileError extends CompileDiagnostic {
name = 'CompileError'; name = 'CompileError';
filename = state.filename;
/** @type {[number, number] | undefined} */
position = undefined;
/** @type {Location | undefined} */
start = undefined;
/** @type {Location | undefined} */
end = undefined;
/** /**
*
* @param {string} code * @param {string} code
* @param {string} message * @param {string} message
* @param {[number, number] | undefined} position * @param {[number, number] | undefined} position
*/ */
constructor(code, message, position) { constructor(code, message, position) {
super(message); super(code, message, position);
this.code = code;
this.position = position;
if (position) {
this.start = state.locator(position[0]);
this.end = state.locator(position[1]);
}
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
} }
} }
@ -65,7 +28,7 @@ function e(node, code, message) {
throw new InternalCompileError( throw new InternalCompileError(
code, code,
message, message,
start !== undefined && end !== undefined ? [start, end] : undefined start !== undefined ? [start, end ?? start] : undefined
); );
} }

@ -1,7 +1,21 @@
import { filename, locator, warnings, ignore_stack, ignore_map } from './state.js'; import { warnings, ignore_stack, ignore_map } from './state.js';
import { CompileDiagnostic } from './utils/compile_diagnostic.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
export class InternalCompileWarning extends CompileDiagnostic {
name = 'CompileWarning';
/**
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(code, message, position);
}
}
/** /**
* @param {null | NodeLike} node * @param {null | NodeLike} node
* @param {string} code * @param {string} code
@ -14,13 +28,13 @@ function w(node, code, message) {
} }
if (stack && stack.at(-1)?.has(code)) return; if (stack && stack.at(-1)?.has(code)) return;
warnings.push({ warnings.push(
new InternalCompileWarning(
code, code,
message, message,
filename, node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined
start: node?.start !== undefined ? locator(node.start) : undefined, )
end: node?.end !== undefined ? locator(node.end) : undefined );
});
} }
export const codes = CODES; export const codes = CODES;

@ -1,50 +1,18 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
/** @import { Location } from 'locate-character' */ import { CompileDiagnostic } from './utils/compile_diagnostic.js';
import * as state from './state.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
export class InternalCompileError extends Error { export class InternalCompileError extends CompileDiagnostic {
name = 'CompileError'; name = 'CompileError';
filename = state.filename;
/** @type {[number, number] | undefined} */
position = undefined;
/** @type {Location | undefined} */
start = undefined;
/** @type {Location | undefined} */
end = undefined;
/** /**
*
* @param {string} code * @param {string} code
* @param {string} message * @param {string} message
* @param {[number, number] | undefined} position * @param {[number, number] | undefined} position
*/ */
constructor(code, message, position) { constructor(code, message, position) {
super(message); super(code, message, position);
this.code = code;
this.position = position;
if (position) {
this.start = state.locator(position[0]);
this.end = state.locator(position[1]);
}
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
} }
} }
@ -58,7 +26,7 @@ function e(node, code, message) {
const start = typeof node === 'number' ? node : node?.start; const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end; const end = typeof node === 'number' ? node : node?.end;
throw new InternalCompileError(code, message, start !== undefined && end !== undefined ? [start, end] : undefined); throw new InternalCompileError(code, message, start !== undefined ? [start, end ?? start] : undefined);
} }
/** /**
@ -762,6 +730,16 @@ export function block_unclosed(node) {
e(node, "block_unclosed", "Block was left open"); e(node, "block_unclosed", "Block was left open");
} }
/**
* Expected a `%character%` character immediately following the opening bracket
* @param {null | number | NodeLike} node
* @param {string} character
* @returns {never}
*/
export function block_unexpected_character(node, character) {
e(node, "block_unexpected_character", `Expected a \`${character}\` character immediately following the opening bracket`);
}
/** /**
* Unexpected block closing tag * Unexpected block closing tag
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -353,11 +353,12 @@ export function convert(source, ast) {
}; };
} }
const start = node.elseif ? node.consequent.nodes[0].start : node.start;
remove_surrounding_whitespace_nodes(node.consequent.nodes); remove_surrounding_whitespace_nodes(node.consequent.nodes);
return { return {
type: 'IfBlock', type: 'IfBlock',
start: node.start, start,
end: node.end, end: node.end,
expression: node.test, expression: node.test,
children: node.consequent.nodes.map( children: node.consequent.nodes.map(

@ -145,6 +145,47 @@ export function migrate(source) {
} }
} }
/**
* If true, then we need to move all reactive statements to the end of the script block,
* in their correct order. Svelte 4 reordered reactive statements, $derived/$effect.pre
* don't have this behavior.
*/
let needs_reordering = false;
for (const [node, { dependencies }] of state.analysis.reactive_statements) {
/** @type {Compiler.Binding[]} */
let ids = [];
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
ids = extract_identifiers(node.body.expression.left)
.map((id) => state.scope.get(id.name))
.filter((id) => !!id);
}
if (
dependencies.some(
(dep) =>
!ids.includes(dep) &&
/** @type {number} */ (dep.node.start) > /** @type {number} */ (node.start)
)
) {
needs_reordering = true;
break;
}
}
if (needs_reordering) {
const nodes = Array.from(state.analysis.reactive_statements.keys());
for (const node of nodes) {
const { start, end } = get_node_range(source, node);
str.appendLeft(end, '\n');
str.move(start, end, /** @type {number} */ (parsed.instance?.content.end));
str.remove(start - (source[start - 2] === '\r' ? 2 : 1), start);
}
}
if (state.needs_run && !added_legacy_import) { if (state.needs_run && !added_legacy_import) {
if (parsed.instance) { if (parsed.instance) {
str.appendRight( str.appendRight(
@ -377,7 +418,7 @@ const instance_script = {
/** @type {number} */ (node.body.expression.start), /** @type {number} */ (node.body.expression.start),
'let ' 'let '
); );
state.str.prependLeft( state.str.prependRight(
/** @type {number} */ (node.body.expression.right.start), /** @type {number} */ (node.body.expression.right.start),
'$derived(' '$derived('
); );
@ -388,14 +429,14 @@ const instance_script = {
');' ');'
); );
} else { } else {
state.str.appendRight(/** @type {number} */ (node.end), ');'); state.str.appendLeft(/** @type {number} */ (node.end), ');');
} }
return; return;
} else { } else {
for (const binding of reassigned_bindings) { for (const binding of reassigned_bindings) {
if (binding && ids.includes(binding.node)) { if (binding && ids.includes(binding.node)) {
// implicitly-declared variable which we need to make explicit // implicitly-declared variable which we need to make explicit
state.str.prependLeft( state.str.prependRight(
/** @type {number} */ (node.start), /** @type {number} */ (node.start),
`let ${binding.node.name}${binding.kind === 'state' ? ' = $state()' : ''};\n${state.indent}` `let ${binding.node.name}${binding.kind === 'state' ? ' = $state()' : ''};\n${state.indent}`
); );
@ -428,7 +469,7 @@ const instance_script = {
[/** @type {number} */ (node.body.end), state.end] [/** @type {number} */ (node.body.end), state.end]
] ]
}); });
state.str.appendRight(/** @type {number} */ (node.end), `\n${state.indent}});`); state.str.appendLeft(/** @type {number} */ (node.end), `\n${state.indent}});`);
} }
} }
}; };
@ -808,6 +849,30 @@ function get_indent(state, ...nodes) {
return indent; return indent;
} }
/**
* Returns start and end of the node. If the start is preceeded with white-space-only before a line break,
* the start will be the start of the line.
* @param {string} source
* @param {Node} node
*/
function get_node_range(source, node) {
let start = /** @type {number} */ (node.start);
let end = /** @type {number} */ (node.end);
let idx = start;
while (source[idx - 1] !== '\n' && source[idx - 1] !== '\r') {
idx--;
if (source[idx] !== ' ' && source[idx] !== '\t') {
idx = start;
break;
}
}
start = idx;
return { start, end };
}
/** /**
* @param {Compiler.OnDirective} last * @param {Compiler.OnDirective} last
* @param {State} state * @param {State} state

@ -39,7 +39,8 @@ export default function tag(parser) {
/** @param {import('../index.js').Parser} parser */ /** @param {import('../index.js').Parser} parser */
function open(parser) { function open(parser) {
const start = parser.index - 2; let start = parser.index - 2;
while (parser.template[start] !== '{') start -= 1;
if (parser.eat('if')) { if (parser.eat('if')) {
parser.require_whitespace(); parser.require_whitespace();
@ -343,9 +344,12 @@ function next(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
let elseif_start = start - 1;
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
/** @type {ReturnType<typeof parser.append<import('#compiler').IfBlock>>} */ /** @type {ReturnType<typeof parser.append<import('#compiler').IfBlock>>} */
const child = parser.append({ const child = parser.append({
start: parser.index, start: elseif_start,
end: -1, end: -1,
type: 'IfBlock', type: 'IfBlock',
elseif: true, elseif: true,

@ -1010,6 +1010,9 @@ const runes_scope_tweaker = {
name: node.local.name, name: node.local.name,
alias: node.exported.name alias: node.exported.name
}); });
const binding = state.scope.get(node.local.name);
if (binding) binding.reassigned = true;
}, },
ExportNamedDeclaration(node, { next, state }) { ExportNamedDeclaration(node, { next, state }) {
if (!node.declaration || state.ast_type !== 'instance') { if (!node.declaration || state.ast_type !== 'instance') {

@ -1090,6 +1090,20 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
} }
} }
/**
* Validates that the opening of a control flow block is `{` immediately followed by the expected character.
* In legacy mode whitespace is allowed inbetween. TODO remove once legacy mode is gone and move this into parser instead.
* @param {{start: number; end: number}} node
* @param {import('./types.js').AnalysisState} state
* @param {string} expected
*/
function validate_opening_tag(node, state, expected) {
if (state.analysis.source[node.start + 1] !== expected) {
// avoid a sea of red and only mark the first few characters
e.block_unexpected_character({ start: node.start, end: node.start + 5 }, expected);
}
}
/** /**
* @param {import('estree').AssignmentExpression | import('estree').UpdateExpression} node * @param {import('estree').AssignmentExpression | import('estree').UpdateExpression} node
* @param {import('estree').Pattern | import('estree').Expression} argument * @param {import('estree').Pattern | import('estree').Expression} argument
@ -1217,6 +1231,8 @@ export const validation_runes = merge(validation, a11y_validators, {
validate_call_expression(node, state.scope, path); validate_call_expression(node, state.scope, path);
}, },
EachBlock(node, { next, state }) { EachBlock(node, { next, state }) {
validate_opening_tag(node, state, '#');
const context = node.context; const context = node.context;
if ( if (
context.type === 'Identifier' && context.type === 'Identifier' &&
@ -1226,6 +1242,51 @@ export const validation_runes = merge(validation, a11y_validators, {
} }
next({ ...state }); next({ ...state });
}, },
IfBlock(node, { state, path }) {
const parent = path.at(-1);
const expected =
path.at(-2)?.type === 'IfBlock' && parent?.type === 'Fragment' && parent.nodes.length === 1
? ':'
: '#';
validate_opening_tag(node, state, expected);
},
AwaitBlock(node, { state }) {
validate_opening_tag(node, state, '#');
if (node.value) {
const start = /** @type {number} */ (node.value.start);
const match = state.analysis.source.substring(start - 10, start).match(/{(\s*):then\s+$/);
if (match && match[1] !== '') {
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
if (node.error) {
const start = /** @type {number} */ (node.error.start);
const match = state.analysis.source.substring(start - 10, start).match(/{(\s*):catch\s+$/);
if (match && match[1] !== '') {
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
},
KeyBlock(node, { state }) {
validate_opening_tag(node, state, '#');
},
SnippetBlock(node, { state }) {
validate_opening_tag(node, state, '#');
},
ConstTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
HtmlTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
DebugTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
RenderTag(node, { state }) {
validate_opening_tag(node, state, '@');
},
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
ensure_no_module_import_conflict(node, state); ensure_no_module_import_conflict(node, state);

@ -198,14 +198,38 @@ export function client_component(source, analysis, options) {
} }
/** @type {Array<ESTree.Property | ESTree.SpreadElement>} */ /** @type {Array<ESTree.Property | ESTree.SpreadElement>} */
const component_returned_object = analysis.exports.map(({ name, alias }) => { const component_returned_object = analysis.exports.flatMap(({ name, alias }) => {
const binding = instance_state.scope.get(name);
const expression = serialize_get_binding(b.id(name), instance_state); const expression = serialize_get_binding(b.id(name), instance_state);
const getter = b.get(alias ?? name, [b.return(expression)]);
if (expression.type === 'Identifier' && !options.dev) { if (expression.type === 'Identifier') {
if (binding?.declaration_kind === 'let' || binding?.declaration_kind === 'var') {
return [
getter,
b.set(alias ?? name, [b.stmt(b.assignment('=', expression, b.id('$$value')))])
];
} else if (!options.dev) {
return b.init(alias ?? name, expression); return b.init(alias ?? name, expression);
} }
}
if (binding?.kind === 'state' || binding?.kind === 'frozen_state') {
return [
getter,
b.set(alias ?? name, [
b.stmt(
b.call(
'$.set',
b.id(name),
b.call(binding.kind === 'state' ? '$.proxy' : '$.freeze', b.id('$$value'))
)
)
])
];
}
return b.get(alias ?? name, [b.return(expression)]); return getter;
}); });
const properties = [...analysis.instance.scope.declarations].filter( const properties = [...analysis.instance.scope.declarations].filter(

@ -454,7 +454,6 @@ export function serialize_proxy_reassignment(value, proxy_reference, state) {
? b.call( ? b.call(
'$.proxy', '$.proxy',
value, value,
b.true,
b.null, b.null,
typeof proxy_reference === 'string' typeof proxy_reference === 'string'
? b.id(proxy_reference) ? b.id(proxy_reference)

@ -617,7 +617,16 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
if (is_reactive) { if (is_reactive) {
const id = state.scope.generate(`${node_id.name}_value`); const id = state.scope.generate(`${node_id.name}_value`);
serialize_update_assignment(state, id, undefined, value, update); serialize_update_assignment(
state,
id,
// `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined,
// that means the value should be set to the empty string. To be able to do that when the value is
// initially undefined, we need to set a value that is guaranteed to be different.
element === 'option' ? b.object([]) : undefined,
value,
update
);
return true; return true;
} else { } else {
state.init.push(update); state.init.push(update);

@ -412,11 +412,8 @@ const global_visitors = {
} }
if (rune === '$effect.root') { if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ ( // ignore $effect.root() calls, just return a noop which mimics the cleanup function
node.arguments.map((arg) => context.visit(arg)) return b.arrow([], b.block([]));
);
// Just call the function directly
return b.call(args[0]);
} }
if (rune === '$state.snapshot') { if (rune === '$state.snapshot') {

@ -14,6 +14,12 @@ export let warnings = [];
*/ */
export let filename; export let filename;
/**
* The original source code
* @type {string}
*/
export let source;
export let locator = getLocator('', { offsetLine: 1 }); export let locator = getLocator('', { offsetLine: 1 });
/** /**
@ -43,10 +49,11 @@ export function pop_ignore() {
} }
/** /**
* @param {string} source * @param {string} _source
* @param {{ filename?: string, rootDir?: string }} options * @param {{ filename?: string, rootDir?: string }} options
*/ */
export function reset(source, options) { export function reset(_source, options) {
source = _source;
const root_dir = options.rootDir?.replace(/\\/g, '/'); const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename?.replace(/\\/g, '/'); filename = options.filename?.replace(/\\/g, '/');

@ -6,13 +6,12 @@ import type {
Identifier, Identifier,
ImportDeclaration ImportDeclaration
} from 'estree'; } from 'estree';
import type { Location } from 'locate-character';
import type { SourceMap } from 'magic-string'; import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe'; import type { Context } from 'zimmerframe';
import type { Scope } from '../phases/scope.js'; import type { Scope } from '../phases/scope.js';
import type { Css } from './css.js'; import type { Css } from './css.js';
import type { EachBlock, Namespace, SvelteNode, SvelteOptions } from './template.js'; import type { EachBlock, Namespace, SvelteNode, SvelteOptions } from './template.js';
import type { InternalCompileError } from '../errors.js'; import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
/** The return value of `compile` from `svelte/compiler` */ /** The return value of `compile` from `svelte/compiler` */
export interface CompileResult { export interface CompileResult {
@ -51,16 +50,9 @@ export interface CompileResult {
ast: any; ast: any;
} }
export interface Warning { export interface Warning extends ICompileDiagnostic {}
start?: Location;
end?: Location;
// TODO there was pos: number in Svelte 4 - do we want to add it back?
code: string;
message: string;
filename?: string;
}
export interface CompileError extends InternalCompileError {} export interface CompileError extends ICompileDiagnostic {}
export type CssHashGetter = (args: { export type CssHashGetter = (args: {
name: string; name: string;

@ -0,0 +1,105 @@
/** @import { Location } from 'locate-character' */
import * as state from '../state.js';
const regex_tabs = /^\t+/;
/**
* @param {string} str
*/
function tabs_to_spaces(str) {
return str.replace(regex_tabs, (match) => match.split('\t').join(' '));
}
/**
* @param {string} source
* @param {number} line
* @param {number} column
*/
function get_code_frame(source, line, column) {
const lines = source.split('\n');
const frame_start = Math.max(0, line - 2);
const frame_end = Math.min(line + 3, lines.length);
const digits = String(frame_end + 1).length;
return lines
.slice(frame_start, frame_end)
.map((str, i) => {
const is_error_line = frame_start + i === line;
const line_num = String(i + frame_start + 1).padStart(digits, ' ');
if (is_error_line) {
const indicator =
' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^';
return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`;
}
return `${line_num}: ${tabs_to_spaces(str)}`;
})
.join('\n');
}
/**
* @typedef {{
* code: string;
* message: string;
* filename?: string;
* start?: Location;
* end?: Location;
* position?: [number, number];
* frame?: string;
* }} ICompileDiagnostic */
/** @implements {ICompileDiagnostic} */
export class CompileDiagnostic extends Error {
name = 'CompileDiagnostic';
/**
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
if (state.filename) {
this.filename = state.filename;
}
if (position) {
this.position = position;
this.start = state.locator(position[0]);
this.end = state.locator(position[1]);
if (this.start && this.end) {
this.frame = get_code_frame(state.source, this.start.line - 1, this.end.column);
}
}
}
toString() {
let out = `${this.code}: ${this.message}`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `:${this.start.line}:${this.start.column}`;
}
}
if (this.frame) {
out += `\n${this.frame}`;
}
return out;
}
toJSON() {
return {
code: this.code,
message: this.message,
filename: this.filename,
start: this.start,
end: this.end,
position: this.position,
frame: this.frame
};
}
}

@ -1,14 +1,22 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { import { warnings, ignore_stack, ignore_map } from './state.js';
filename, import { CompileDiagnostic } from './utils/compile_diagnostic.js';
locator,
warnings,
ignore_stack,
ignore_map
} from './state.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
export class InternalCompileWarning extends CompileDiagnostic {
name = 'CompileWarning';
/**
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(code, message, position);
}
}
/** /**
* @param {null | NodeLike} node * @param {null | NodeLike} node
* @param {string} code * @param {string} code
@ -22,14 +30,7 @@ function w(node, code, message) {
} }
if (stack && stack.at(-1)?.has(code)) return; if (stack && stack.at(-1)?.has(code)) return;
warnings.push(new InternalCompileWarning(code, message, node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined));
warnings.push({
code,
message,
filename,
start: node?.start !== undefined ? locator(node.start) : undefined,
end: node?.end !== undefined ? locator(node.end) : undefined
});
} }
export const codes = [ export const codes = [

@ -1,9 +1,7 @@
/** @import { Source, Effect } from '#client' */ /** @import { Source, Effect } from '#client' */
import { empty } from '../dom/operations.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js'; import { block, branch, destroy_effect } from '../reactivity/effects.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 { check_target } from './legacy.js';
/** /**
* @template {(anchor: Comment, props: any) => any} Component * @template {(anchor: Comment, props: any) => any} Component
@ -20,6 +18,8 @@ export function hmr(source) {
/** @type {Effect} */ /** @type {Effect} */
let effect; let effect;
let ran = false;
block(() => { block(() => {
const component = get(source); const component = get(source);
@ -30,7 +30,9 @@ export function hmr(source) {
} }
effect = branch(() => { effect = branch(() => {
set_should_intro(false); // when the component is invalidated, replace it without transitions
if (ran) set_should_intro(false);
// preserve getters/setters // preserve getters/setters
Object.defineProperties( Object.defineProperties(
instance, instance,
@ -39,10 +41,13 @@ export function hmr(source) {
new.target ? new component(anchor, props) : component(anchor, props) new.target ? new component(anchor, props) : component(anchor, props)
) )
); );
set_should_intro(true);
if (ran) set_should_intro(true);
}); });
}); });
ran = true;
return instance; return instance;
}; };
} }

@ -0,0 +1,21 @@
import { hydrate_next, hydrating } from '../hydration.js';
/**
* @param {Comment} anchor
* @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn
* @param {Record<string, unknown>} slot_props
* @param {null | ((anchor: Comment) => void)} fallback_fn
*/
export function slot(anchor, slot_fn, slot_props, fallback_fn) {
if (hydrating) {
hydrate_next();
}
if (slot_fn === undefined) {
if (fallback_fn !== null) {
fallback_fn(anchor);
}
} else {
slot_fn(anchor, slot_props);
}
}

@ -20,6 +20,7 @@ import { current_each_item, set_current_each_item } from './each.js';
import { current_component_context, current_effect } from '../../runtime.js'; import { current_component_context, current_effect } from '../../runtime.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { noop } from '../../../shared/utils.js'; import { noop } from '../../../shared/utils.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
/** /**
* @param {Comment | Element} node * @param {Comment | Element} node
@ -157,7 +158,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
// Inert effects are proactively detached from the effect tree. Returning a noop // Inert effects are proactively detached from the effect tree. Returning a noop
// teardown function is an easy way to ensure that this is not discarded // teardown function is an easy way to ensure that this is not discarded
return noop; return noop;
}); }, EFFECT_TRANSPARENT);
if (was_hydrating) { if (was_hydrating) {
set_hydrating(true); set_hydrating(true);

@ -155,6 +155,7 @@ export function set_custom_element_data(node, prop, value) {
export function set_attributes(element, prev, next, lowercase_attributes, css_hash) { export function set_attributes(element, prev, next, lowercase_attributes, css_hash) {
var has_hash = css_hash.length !== 0; var has_hash = css_hash.length !== 0;
var current = prev || {}; var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
for (var key in prev) { for (var key in prev) {
if (!(key in next)) { if (!(key in next)) {
@ -178,6 +179,26 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
for (const key in next) { for (const key in next) {
// let instead of var because referenced in a closure // let instead of var because referenced in a closure
let value = next[key]; let value = next[key];
// Up here because we want to do this for the initial value, too, even if it's undefined,
// and this wouldn't be reached in case of undefined because of the equality check below
if (is_option_element && key === 'value' && value == null) {
// The <option> element is a special case because removing the value attribute means
// the value is set to the text content of the option element, and setting the value
// to null or undefined means the value is set to the string "null" or "undefined".
// To align with how we handle this case in non-spread-scenarios, this logic is needed.
// There's a super-edge-case bug here that is left in in favor of smaller code size:
// Because of the "set missing props to null" logic above, we can't differentiate
// between a missing value and an explicitly set value of null or undefined. That means
// that once set, the value attribute of an <option> element can't be removed. This is
// a very rare edge case, and removing the attribute altogether isn't possible either
// for the <option value={undefined}> case, so we're not losing any functionality here.
// @ts-ignore
element.value = element.__value = '';
current[key] = value;
continue;
}
var prev_value = current[key]; var prev_value = current[key];
if (value === prev_value) continue; if (value === prev_value) continue;

@ -15,6 +15,7 @@ export { key_block as key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js'; export { css_props } from './dom/blocks/css-props.js';
export { index, each } from './dom/blocks/each.js'; export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js'; export { html } from './dom/blocks/html.js';
export { slot } from './dom/blocks/slot.js';
export { snippet, wrap_snippet } from './dom/blocks/snippet.js'; export { snippet, wrap_snippet } from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js'; export { component } from './dom/blocks/svelte-component.js';
export { element } from './dom/blocks/svelte-element.js'; export { element } from './dom/blocks/svelte-element.js';
@ -119,7 +120,7 @@ export {
update_pre_store, update_pre_store,
update_store update_store
} from './reactivity/store.js'; } from './reactivity/store.js';
export { append_styles, sanitize_slots, set_text, slot } from './render.js'; export { append_styles, sanitize_slots, set_text } from './render.js';
export { export {
get, get,
invalidate_inner_signals, invalidate_inner_signals,

@ -11,7 +11,7 @@ import {
object_prototype object_prototype
} from './utils.js'; } from './utils.js';
import { check_ownership, widen_ownership } from './dev/ownership.js'; import { check_ownership, widen_ownership } from './dev/ownership.js';
import { mutable_source, source, set } from './reactivity/sources.js'; import { source, set } from './reactivity/sources.js';
import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js'; import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js';
import { UNINITIALIZED } from '../../constants.js'; import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js'; import * as e from './errors.js';
@ -19,12 +19,11 @@ import * as e from './errors.js';
/** /**
* @template T * @template T
* @param {T} value * @param {T} value
* @param {boolean} [immutable]
* @param {import('#client').ProxyMetadata | null} [parent] * @param {import('#client').ProxyMetadata | null} [parent]
* @param {import('#client').Source<T>} [prev] dev mode only * @param {import('#client').Source<T>} [prev] dev mode only
* @returns {import('#client').ProxyStateObject<T> | T} * @returns {import('#client').ProxyStateObject<T> | T}
*/ */
export function proxy(value, immutable = true, parent = null, prev) { export function proxy(value, parent = null, prev) {
if ( if (
typeof value === 'object' && typeof value === 'object' &&
value != null && value != null &&
@ -59,7 +58,6 @@ export function proxy(value, immutable = true, parent = null, prev) {
s: new Map(), s: new Map(),
v: source(0), v: source(0),
a: is_array(value), a: is_array(value),
i: immutable,
p: proxy, p: proxy,
t: value t: value
}), }),
@ -169,7 +167,7 @@ const state_proxy_handler = {
const metadata = target[STATE_SYMBOL]; const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop); const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(descriptor.value, metadata.i, metadata)); if (s !== undefined) set(s, proxy(descriptor.value, metadata));
} }
return Reflect.defineProperty(target, prop, descriptor); return Reflect.defineProperty(target, prop, descriptor);
@ -215,7 +213,7 @@ const state_proxy_handler = {
// create a source, but only if it's an own property and not a prototype property // create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) { if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) {
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata)); s = source(proxy(target[prop], metadata));
metadata.s.set(prop, s); metadata.s.set(prop, s);
} }
@ -256,9 +254,7 @@ const state_proxy_handler = {
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable)) (current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) { ) {
if (s === undefined) { if (s === undefined) {
s = (metadata.i ? source : mutable_source)( s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED);
has ? proxy(target[prop], metadata.i, metadata) : UNINITIALIZED
);
metadata.s.set(prop, s); metadata.s.set(prop, s);
} }
const value = get(s); const value = get(s);
@ -283,7 +279,7 @@ const state_proxy_handler = {
s = metadata.s.get(prop); s = metadata.s.get(prop);
} }
if (s !== undefined) { if (s !== undefined) {
set(s, proxy(value, metadata.i, metadata)); set(s, proxy(value, metadata));
} }
const is_array = metadata.a; const is_array = metadata.a;
const not_has = !(prop in target); const not_has = !(prop in target);

@ -31,8 +31,8 @@ export const root_event_handles = new Set();
/** /**
* This is normally true block effects should run their intro transitions * This is normally true block effects should run their intro transitions
* but is false during hydration and mounting (unless `options.intro` is `true`) * but is false during hydration (unless `options.intro` is `true`) and
* and when creating the children of a `<svelte:element>` that just changed tag * when creating the children of a `<svelte:element>` that just changed tag
*/ */
export let should_intro = true; export let should_intro = true;
@ -57,27 +57,8 @@ export function set_text(text, value) {
} }
/** /**
* @param {Comment} anchor * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component.
* @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn * Transitions will play during the initial render unless the `intro` option is set to `false`.
* @param {Record<string, unknown>} slot_props
* @param {null | ((anchor: Comment) => void)} fallback_fn
*/
export function slot(anchor, slot_fn, slot_props, fallback_fn) {
if (hydrating) {
hydrate_next();
}
if (slot_fn === undefined) {
if (fallback_fn !== null) {
fallback_fn(anchor);
}
} else {
slot_fn(anchor, slot_props);
}
}
/**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component
* *
* @template {Record<string, any>} Props * @template {Record<string, any>} Props
* @template {Record<string, any>} Exports * @template {Record<string, any>} Exports
@ -137,6 +118,7 @@ export function hydrate(component, options) {
validate_component(component); validate_component(component);
} }
options.intro = options.intro ?? false;
const target = options.target; const target = options.target;
const was_hydrating = hydrating; const was_hydrating = hydrating;
@ -213,7 +195,7 @@ export function hydrate(component, options) {
* }} options * }} options
* @returns {Exports} * @returns {Exports}
*/ */
function _mount(Component, { target, anchor, props = {}, events, context, intro = false }) { function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations(); init_operations();
const registered_events = new Set(); const registered_events = new Set();

@ -171,8 +171,17 @@ export function check_dirtiness(reaction) {
if (dependencies !== null) { if (dependencies !== null) {
var is_unowned = (flags & UNOWNED) !== 0; var is_unowned = (flags & UNOWNED) !== 0;
var i;
if ((flags & DISCONNECTED) !== 0) {
for (i = 0; i < dependencies.length; i++) {
(dependencies[i].reactions ??= []).push(reaction);
}
for (var i = 0; i < dependencies.length; i++) { reaction.f ^= DISCONNECTED;
}
for (i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i]; var dependency = dependencies[i];
if (check_dirtiness(/** @type {import('#client').Derived} */ (dependency))) { if (check_dirtiness(/** @type {import('#client').Derived} */ (dependency))) {
@ -771,25 +780,6 @@ export function get(signal) {
if (check_dirtiness(derived)) { if (check_dirtiness(derived)) {
update_derived(derived); update_derived(derived);
} }
if ((flags & DISCONNECTED) !== 0) {
// reconnect to the graph
deps = derived.deps;
if (deps !== null) {
for (var i = 0; i < deps.length; i++) {
var dep = deps[i];
var reactions = dep.reactions;
if (reactions === null) {
dep.reactions = [derived];
} else if (!reactions.includes(derived)) {
reactions.push(derived);
}
}
}
derived.f ^= DISCONNECTED;
}
} }
return signal.v; return signal.v;

@ -180,8 +180,6 @@ export interface ProxyMetadata<T = Record<string | symbol, any>> {
v: Source<number>; v: Source<number>;
/** `true` if the proxified object is an array */ /** `true` if the proxified object is an array */
a: boolean; a: boolean;
/** Immutable: Whether to use a source or mutable source under the hood */
i: boolean;
/** The associated proxy */ /** The associated proxy */
p: ProxyStateObject<T>; p: ProxyStateObject<T>;
/** The original target this proxy was created for */ /** The original target this proxy was created for */

@ -1,8 +1,9 @@
/** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */ /** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */
import { proxy } from '../internal/client/proxy.js'; import { mutable_source, get, set } from 'svelte/internal/client';
import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { hydrate, mount, unmount } from '../internal/client/render.js'; import { hydrate, mount, unmount } from '../internal/client/render.js';
import { define_property } from '../internal/client/utils.js'; import { define_property } from '../internal/client/utils.js';
import { safe_not_equal } from '../internal/client/reactivity/equality.js';
/** /**
* Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component. * Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
@ -69,15 +70,43 @@ class Svelte4Component {
* }} options * }} options
*/ */
constructor(options) { constructor(options) {
// Using proxy state here isn't completely mirroring the Svelte 4 behavior, because mutations to a property var sources = new Map();
// cause fine-grained updates to only the places where that property is used, and not the entire property.
// Reactive statements and actions (the things where this matters) are handling this properly regardless, so it should be fine in practise. /**
const props = proxy({ ...(options.props || {}), $$events: {} }, false); * @param {string | symbol} key
* @param {unknown} value
*/
var add_source = (key, value) => {
var s = mutable_source(value);
sources.set(key, s);
return s;
};
// Replicate coarse-grained props through a proxy that has a version source for
// each property, which is increment on updates to the property itself. Do not
// use our $state proxy because that one has fine-grained reactivity.
const props = new Proxy(
{ ...(options.props || {}), $$events: {} },
{
get(target, prop) {
return get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop)));
},
has(target, prop) {
get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop)));
return Reflect.has(target, prop);
},
set(target, prop, value) {
set(sources.get(prop) ?? add_source(prop, value), value);
return Reflect.set(target, prop, value);
}
}
);
this.#instance = (options.hydrate ? hydrate : mount)(options.component, { this.#instance = (options.hydrate ? hydrate : mount)(options.component, {
target: options.target, target: options.target,
props, props,
context: options.context, context: options.context,
intro: options.intro, intro: options.intro ?? false,
recover: options.recover recover: options.recover
}); });
@ -100,6 +129,7 @@ class Svelte4Component {
this.#instance.$set = /** @param {Record<string, any>} next */ (next) => { this.#instance.$set = /** @param {Record<string, any>} next */ (next) => {
Object.assign(props, next); Object.assign(props, next);
}; };
this.#instance.$destroy = () => { this.#instance.$destroy = () => {
unmount(this.#instance); unmount(this.#instance);
}; };

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

@ -10,6 +10,8 @@ import type { CompileOptions, Warning } from '#compiler';
function normalize_warning(warning: Warning) { function normalize_warning(warning: Warning) {
delete warning.filename; delete warning.filename;
delete warning.position;
delete warning.frame;
return warning; return warning;
} }

@ -0,0 +1,7 @@
<script>
$: mobile = width < 640;
$: x = !mobile;
let width = 0;
</script>
{width / mobile / x}

@ -0,0 +1,7 @@
<script>
let width = 0;
let mobile = $derived(width < 640);
let x = $derived(!mobile);
</script>
{width / mobile / x}

@ -0,0 +1,7 @@
<script>
let width = 0;
$: console.log(mobile);
$: mobile = width < 640;
</script>
{width / mobile}

@ -0,0 +1,11 @@
<script>
import { run } from 'svelte/legacy';
let width = 0;
let mobile = $derived(width < 640);
run(() => {
console.log(mobile);
});
</script>
{width / mobile}

@ -0,0 +1,5 @@
{#if foo}
<p>foo</p>
{:else}
<p>not foo</p>
{/if}

@ -0,0 +1,116 @@
{
"css": null,
"js": [],
"start": 0,
"end": 51,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "IfBlock",
"elseif": false,
"start": 0,
"end": 51,
"test": {
"type": "Identifier",
"start": 5,
"end": 8,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 8
}
},
"name": "foo"
},
"consequent": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 9,
"end": 11,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 11,
"end": 21,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 14,
"end": 17,
"raw": "foo",
"data": "foo"
}
],
"transparent": true
}
},
{
"type": "Text",
"start": 21,
"end": 22,
"raw": "\n",
"data": "\n"
}
],
"transparent": false
},
"alternate": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 29,
"end": 31,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 31,
"end": 45,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 34,
"end": 41,
"raw": "not foo",
"data": "not foo"
}
],
"transparent": true
}
},
{
"type": "Text",
"start": 45,
"end": 46,
"raw": "\n",
"data": "\n"
}
],
"transparent": false
}
}
],
"transparent": false
},
"options": null
}

@ -0,0 +1,5 @@
{#if x > 10}
<p>x is greater than 10</p>
{:else if x < 5}
<p>x is less than 5</p>
{/if}

@ -0,0 +1,211 @@
{
"css": null,
"js": [],
"start": 0,
"end": 89,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "IfBlock",
"elseif": false,
"start": 0,
"end": 89,
"test": {
"type": "BinaryExpression",
"start": 5,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 11
}
},
"left": {
"type": "Identifier",
"start": 5,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 6
}
},
"name": "x"
},
"operator": ">",
"right": {
"type": "Literal",
"start": 9,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 11
}
},
"value": 10,
"raw": "10"
}
},
"consequent": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 12,
"end": 14,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 14,
"end": 41,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 17,
"end": 37,
"raw": "x is greater than 10",
"data": "x is greater than 10"
}
],
"transparent": true
}
},
{
"type": "Text",
"start": 41,
"end": 42,
"raw": "\n",
"data": "\n"
}
],
"transparent": false
},
"alternate": {
"type": "Fragment",
"nodes": [
{
"start": 42,
"end": 89,
"type": "IfBlock",
"elseif": true,
"test": {
"type": "BinaryExpression",
"start": 52,
"end": 57,
"loc": {
"start": {
"line": 3,
"column": 10
},
"end": {
"line": 3,
"column": 15
}
},
"left": {
"type": "Identifier",
"start": 52,
"end": 53,
"loc": {
"start": {
"line": 3,
"column": 10
},
"end": {
"line": 3,
"column": 11
}
},
"name": "x"
},
"operator": "<",
"right": {
"type": "Literal",
"start": 56,
"end": 57,
"loc": {
"start": {
"line": 3,
"column": 14
},
"end": {
"line": 3,
"column": 15
}
},
"value": 5,
"raw": "5"
}
},
"consequent": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 58,
"end": 60,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 60,
"end": 83,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 63,
"end": 79,
"raw": "x is less than 5",
"data": "x is less than 5"
}
],
"transparent": true
}
},
{
"type": "Text",
"start": 83,
"end": 84,
"raw": "\n",
"data": "\n"
}
],
"transparent": false
},
"alternate": null
}
],
"transparent": false
}
}
],
"transparent": false
},
"options": null
}

@ -0,0 +1,50 @@
{
"css": null,
"js": [],
"start": 0,
"end": 17,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "IfBlock",
"elseif": false,
"start": 0,
"end": 17,
"test": {
"type": "Identifier",
"start": 5,
"end": 8,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 8
}
},
"name": "foo"
},
"consequent": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 9,
"end": 12,
"raw": "bar",
"data": "bar"
}
],
"transparent": false
},
"alternate": null
}
],
"transparent": false
},
"options": null
}

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
get props() {
return {};
},
html: '',
async test({ assert, component, target }) {
await component.$set({ message: 'goodbye' });
assert.htmlEqual(target.innerHTML, '<p>goodbye</p>');
}
});

@ -0,0 +1,3 @@
{#if 'message' in $$props}
<p>{$$props.message}</p>
{/if}

@ -0,0 +1,24 @@
import { test } from '../../test';
const data = {
message: 'hello'
};
export default test({
get props() {
data.message = 'hello';
return {
data
};
},
html: '<p>hello</p>',
async test({ assert, component, target }) {
data.message = 'goodbye';
await component.$set({ data });
assert.htmlEqual(target.innerHTML, '<p>goodbye</p>');
}
});

@ -0,0 +1,5 @@
<script>
export let data;
</script>
<p>{data.message}</p>

@ -61,7 +61,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
warnings: any[]; warnings: any[];
hydrate: Function; hydrate: Function;
}) => void | Promise<void>; }) => void | Promise<void>;
test_ssr?: (args: { assert: Assert }) => void | Promise<void>; test_ssr?: (args: { logs: any[]; assert: Assert }) => void | Promise<void>;
accessors?: boolean; accessors?: boolean;
immutable?: boolean; immutable?: boolean;
intro?: boolean; intro?: boolean;
@ -285,6 +285,7 @@ async function run_test_variant(
if (config.test_ssr) { if (config.test_ssr) {
await config.test_ssr({ await config.test_ssr({
logs,
// @ts-expect-error // @ts-expect-error
assert: { assert: {
...assert, ...assert,

@ -0,0 +1,21 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target, raf }) {
const btn = target.querySelector('button');
raf.tick(0);
btn?.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>Toggle</button> <div style="opacity: 0;">DIV</div>`
);
raf.tick(100);
assert.htmlEqual(target.innerHTML, `<button>Toggle</button> <div style="">DIV</div>`);
}
});

@ -0,0 +1,12 @@
<script>
import { fade } from 'svelte/transition';
let element = $state('div');
let show = $state(false);
</script>
<button onclick={() => (show = !show)}>Toggle</button>
{#if show}
<svelte:element this={element} transition:fade={{ duration: 100 }}>DIV</svelte:element>
{/if}

@ -0,0 +1,18 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '<button>cleanup</button>',
async test({ assert, target, logs }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.deepEqual(logs, ['effect1', 'effect2']);
},
test_ssr({ assert, logs }) {
assert.deepEqual(logs, []);
}
});

@ -0,0 +1,10 @@
<script>
$effect.root(() => {
console.log('effect1');
});
const cleanup = $effect.root(() => {
console.log('effect2');
});
</script>
<button onclick={cleanup}>cleanup</button>

@ -24,5 +24,8 @@ export default test({
}); });
assert.deepEqual(logs, [0, 1, 'cleanup 1', 'cleanup 2']); assert.deepEqual(logs, [0, 1, 'cleanup 1', 'cleanup 2']);
},
test_ssr({ assert, logs }) {
assert.deepEqual(logs, []);
} }
}); });

@ -5,8 +5,7 @@
function setStore() { function setStore() {
store = writable(0, () => { store = writable(0, () => {
console.log('start'); return () => {};
return () => console.log('stop');
}); });
} }
</script> </script>

@ -4,10 +4,9 @@ import { test } from '../../test';
export default test({ export default test({
test({ assert, target }) { test({ assert, target }) {
assert.htmlEqual(target.innerHTML, `0 0 <button>0 / 0</button>`); assert.htmlEqual(target.innerHTML, `0 0 <button>0 / 0</button>`);
const [btn] = target.querySelectorAll('button'); const btn = target.querySelector('button');
btn?.click(); flushSync(() => btn?.click());
flushSync();
assert.htmlEqual(target.innerHTML, '1 2 <button>1 / 2</button>'); assert.htmlEqual(target.innerHTML, '1 2 <button>1 / 2</button>');
} }
}); });

@ -1,5 +1,5 @@
<script> <script>
import Sub from './sub.svelte' import Sub from './sub.svelte';
let sub = $state(); let sub = $state();
</script> </script>

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

@ -0,0 +1,7 @@
<script>
import Sub from './sub.svelte';
let sub = $state();
</script>
<Sub bind:this={sub} />
<button on:click={() => sub.count++}>{sub?.count} / {sub?.doubled}</button>

@ -0,0 +1,9 @@
<script>
let count = $state(0);
let doubled = $derived(count * 2);
export { count, doubled };
</script>
{count}
{doubled}

@ -0,0 +1,5 @@
<script>
import { fade } from 'svelte/transition';
</script>
<div in:fade|global={{ duration: 100 }}>DIV</div>

@ -0,0 +1,21 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
async test({ assert, target, raf }) {
const [btn1, btn2] = target.querySelectorAll('button');
const div = target.querySelector('div');
ok(div);
btn1.click();
flushSync();
assert.htmlEqual(div.innerHTML, `<div style="opacity: 0;">DIV</div>`);
raf.tick(100);
assert.htmlEqual(div.innerHTML, `<div style="">DIV</div>`);
btn2.click();
flushSync();
assert.htmlEqual(div.innerHTML, `<div>DIV</div>`);
}
});

@ -0,0 +1,21 @@
<script>
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let el;
let instance;
function intro(animate) {
if (instance) unmount(instance);
instance = mount(Component, {
target: el,
intro: animate
});
}
</script>
<div bind:this={el}></div>
<button onclick={() => intro()}>mount with intro transition</button>
<button onclick={() => intro(false)}>mount without intro transition</button>

@ -0,0 +1,32 @@
import { test } from '../../test';
// <option value> is special because falsy values should result in an empty string value attribute
export default test({
mode: ['client'],
test({ assert, target }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
`
);
}
});

@ -0,0 +1,26 @@
<script>
let nonreactive = undefined;
let reactive = $state();
let nonreactive_spread = { value: undefined };
let reactive_spread = $state({ value: undefined });
</script>
<select>
<option value={undefined}>Default</option>
</select>
<select>
<option value={nonreactive}>Default</option>
</select>
<select>
<option value={reactive}>Default</option>
</select>
<select>
<option {...nonreactive_spread}>Default</option>
</select>
<select>
<option {...reactive_spread}>Default</option>
</select>

@ -550,6 +550,40 @@ describe('signals', () => {
}; };
}); });
test('deriveds update upon reconnection #3', () => {
let a = source(false);
let b = source(false);
let c = derived(() => $.get(a) || $.get(b));
let d = derived(() => $.get(c));
let e = derived(() => $.get(d));
return () => {
const log: string[] = [];
let destroy = effect_root(() => {
render_effect(() => {
$.get(e);
log.push('init');
});
});
destroy();
destroy = effect_root(() => {
render_effect(() => {
$.get(e);
log.push('update');
});
});
assert.deepEqual(log, ['init', 'update']);
set(a, true);
flushSync();
assert.deepEqual(log, ['init', 'update', 'update']);
};
});
test('unowned deriveds are not added as reactions', () => { test('unowned deriveds are not added as reactions', () => {
var count = source(0); var count = source(0);

@ -0,0 +1,8 @@
<svelte:options runes={false} />
<!-- prettier-ignore -->
<div>
{ #if true}
<p>hi</p>
{/if}
</div>

@ -0,0 +1,14 @@
[
{
"code": "block_unexpected_character",
"message": "Expected a `#` character immediately following the opening bracket",
"start": {
"line": 5,
"column": 1
},
"end": {
"line": 5,
"column": 6
}
}
]

@ -0,0 +1,8 @@
<svelte:options runes={true} />
<!-- prettier-ignore -->
<div>
{ #if true}
<p>hi</p>
{/if}
</div>

@ -366,7 +366,8 @@ declare module 'svelte' {
/** Anything except a function */ /** Anything except a function */
type NotFunction<T> = T extends Function ? never : T; type NotFunction<T> = T extends Function ? never : T;
/** /**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component.
* Transitions will play during the initial render unless the `intro` option is set to `false`.
* *
* */ * */
export function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: {} extends Props ? { export function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: {} extends Props ? {
@ -562,9 +563,9 @@ declare module 'svelte/animate' {
declare module 'svelte/compiler' { declare module 'svelte/compiler' {
import type { AssignmentExpression, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, ArrayExpression, MemberExpression, ObjectExpression, Pattern, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, FunctionExpression, Node, Program, ChainExpression, SimpleCallExpression } from 'estree'; import type { AssignmentExpression, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, ArrayExpression, MemberExpression, ObjectExpression, Pattern, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, FunctionExpression, Node, Program, ChainExpression, SimpleCallExpression } from 'estree';
import type { Location } from 'locate-character';
import type { SourceMap } from 'magic-string'; import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe'; import type { Context } from 'zimmerframe';
import type { Location } from 'locate-character';
/** /**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component * `compile` converts your `.svelte` source code into a JavaScript module that exports a component
* *
@ -714,16 +715,9 @@ declare module 'svelte/compiler' {
ast: any; ast: any;
} }
export interface Warning { export interface Warning extends ICompileDiagnostic {}
start?: Location;
end?: Location;
// TODO there was pos: number in Svelte 4 - do we want to add it back?
code: string;
message: string;
filename?: string;
}
export interface CompileError extends InternalCompileError {} export interface CompileError extends ICompileDiagnostic {}
type CssHashGetter = (args: { type CssHashGetter = (args: {
name: string; name: string;
@ -1882,18 +1876,15 @@ declare module 'svelte/compiler' {
content: Program; content: Program;
attributes: Attribute[]; attributes: Attribute[];
} }
class InternalCompileError extends Error { type ICompileDiagnostic = {
constructor(code: string, message: string, position: [number, number] | undefined);
filename: string | undefined;
position: [number, number] | undefined;
start: Location | undefined;
end: Location | undefined;
code: string; code: string;
} message: string;
filename?: string;
start?: Location;
end?: Location;
position?: [number, number];
frame?: string;
};
export {}; export {};
} }
@ -2539,14 +2530,7 @@ declare module 'svelte/types/compiler/interfaces' {
export type CompileOptions = CompileOptions_1; export type CompileOptions = CompileOptions_1;
/** @deprecated import this from 'svelte' instead */ /** @deprecated import this from 'svelte' instead */
export type Warning = Warning_1; export type Warning = Warning_1;
interface Warning_1 { interface Warning_1 extends ICompileDiagnostic {}
start?: Location;
end?: Location;
// TODO there was pos: number in Svelte 4 - do we want to add it back?
code: string;
message: string;
filename?: string;
}
type CssHashGetter = (args: { type CssHashGetter = (args: {
name: string; name: string;
@ -2709,6 +2693,15 @@ declare module 'svelte/types/compiler/interfaces' {
* (also see https://github.com/sveltejs/svelte/pull/5652) * (also see https://github.com/sveltejs/svelte/pull/5652)
*/ */
type Namespace = 'html' | 'svg' | 'mathml' | 'foreign'; type Namespace = 'html' | 'svg' | 'mathml' | 'foreign';
type ICompileDiagnostic = {
code: string;
message: string;
filename?: string;
start?: Location;
end?: Location;
position?: [number, number];
frame?: string;
};
export {}; export {};
}declare module '*.svelte' { }declare module '*.svelte' {

@ -281,3 +281,7 @@ In Svelte 4, `<svelte:element this="div">` is valid code. This makes little sens
``` ```
Note that whereas Svelte 4 would treat `<svelte:element this="input">` (for example) identically to `<input>` for the purposes of determining which `bind:` directives could be applied, Svelte 5 does not. Note that whereas Svelte 4 would treat `<svelte:element this="input">` (for example) identically to `<input>` for the purposes of determining which `bind:` directives could be applied, Svelte 5 does not.
### `mount` plays transitions by default
The `mount` function used to render a component tree plays transitions by default unless the `intro` option is set to `false`. This is different from legacy class components which, when manually instantiated, didn't play transitions by default.

Loading…
Cancel
Save