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

svelte-custom-renderer
paoloricciuti 7 days ago
commit 17e37a51bc

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: speed up parser interactions with Acorn or avoid them where possible

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: serialize input default values during server rendering

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: avoid regex matching in parser where possible

@ -231,14 +231,6 @@ Rest element properties of `$props()` such as `%property%` are readonly
The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
```
### set_context_after_init
```
`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
```
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### snippet_renderer_mismatch
```

@ -80,6 +80,14 @@ Context was not set in the current component or any of its ancestors
The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
### set_context_after_init
```
`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
```
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### snippet_without_render_tag
```

@ -175,12 +175,6 @@ This can happen if you render a hydratable on the client that was not rendered o
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
## set_context_after_init
> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## snippet_renderer_mismatch
> A snippet created in a component with a custom renderer cannot be rendered by a different renderer

@ -66,6 +66,12 @@ Certain lifecycle methods can only be used during component initialisation. To f
The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
## set_context_after_init
> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## snippet_without_render_tag
> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.

@ -178,17 +178,17 @@
"web-features": "^3.29.0"
},
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.10",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"@jridgewell/remapping": "^2.3.5",
"@jridgewell/sourcemap-codec": "^1.6.0",
"@sveltejs/acorn-typescript": "^1.0.13",
"@types/estree": "^1.0.9",
"acorn": "^8.18.0",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.8.1",
"devalue": "^5.9.2",
"esm-env": "^1.2.1",
"esrap": "^2.2.12",
"esrap": "^2.3.6",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -5,6 +5,7 @@ import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
import { locator } from '../../state.js';
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
@ -87,7 +88,8 @@ export function parse_expression_at(parser, source, index) {
sourceType: 'module',
ecmaVersion: 16,
locations: true,
preserveParens: true
preserveParens: true,
startLocation: start_location(parser, index)
});
add_comments(ast);
@ -112,7 +114,13 @@ export function parse_statement_at(parser, source, index) {
try {
// This is like parseExpressionAt but for statements
const p = new acorn(
{ onComment, sourceType: 'module', ecmaVersion: 16, locations: true },
{
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
startLocation: start_location(parser, index)
},
source,
index
);
@ -128,6 +136,32 @@ export function parse_statement_at(parser, source, index) {
}
}
const regex_non_lf_line_break = /\r(?!\n)|[\u2028\u2029]/;
let last_template = '';
let lf_only = true;
/**
* Without `startLocation`, acorn counts the lines before `index` on every call
* @param {Parser} parser
* @param {number} index
*/
function start_location(parser, index) {
return has_lf_line_breaks_only(parser) ? locator(index) : undefined;
}
/**
* acorn breaks lines on bare `\r`, `\u2028` and `\u2029`, which the locator doesn't
* @param {Parser} parser
*/
export function has_lf_line_breaks_only(parser) {
if (parser.template !== last_template) {
last_template = parser.template;
lf_only = !regex_non_lf_line_break.test(last_template);
}
return lf_only;
}
const regex_position_indicator = / \(\d+:\d+\)$/;
/**

@ -10,25 +10,7 @@ import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
/** @param {number} cc */
function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}
import { is_whitespace } from './utils/whitespace.js';
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
@ -278,8 +260,27 @@ export class Parser {
};
}
/** @param {string} delimiter */
read_until(delimiter) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
}
const start = this.index;
const index = this.template.indexOf(delimiter, start);
if (index !== -1) {
this.index = index;
return this.template.slice(start, this.index);
}
this.index = this.template.length;
return this.template.slice(start);
}
/** @param {RegExp} pattern */
read_until(pattern) {
read_until_regex(pattern) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
@ -302,6 +303,7 @@ export class Parser {
e.expected_whitespace(this.index);
}
this.index++;
this.allow_whitespace();
}

@ -2,7 +2,6 @@
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
/**
@ -33,24 +32,10 @@ export default function read_pattern(parser) {
i = match_bracket(parser, start);
parser.index = i;
const pattern_string = parser.template.slice(start, i);
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
// acorn never reads before `start`, so the template itself can serve as the prefix
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
parse_expression_at(parser, parser.template.slice(0, i) + ' = 1', start)
);
expression = expression.left;
@ -80,7 +65,7 @@ function read_type_annotation(parser) {
const insert = '_ as ';
let a = parser.index - insert.length;
const template =
parser.template.slice(0, a).replace(/[^\n]/g, ' ') +
parser.template.slice(0, a) +
insert +
// If this is a type annotation for a function parameter, Acorn-TS will treat subsequent
// parameters as part of a sequence expression instead, and will then error on optional

@ -1,9 +1,13 @@
/** @import { Expression } from 'estree' */
/** @import { Expression, Identifier } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at, remove_parens } from '../acorn.js';
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import { has_lf_line_breaks_only, parse_expression_at, remove_parens } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
import { is_reserved } from '../../../../utils.js';
import { locator } from '../../../state.js';
/**
* @param {Parser} parser
@ -33,6 +37,9 @@ export function get_loose_identifier(parser, opening_token) {
* @returns {Expression}
*/
export default function read_expression(parser, opening_token, disallow_loose) {
const simple = read_simple_expression(parser);
if (simple) return simple;
try {
const node = parse_expression_at(parser, parser.template, parser.index);
@ -57,3 +64,104 @@ export default function read_expression(parser, opening_token, disallow_loose) {
throw err;
}
}
/**
* Most template expressions are an identifier or a `a.b.c` member chain followed by `}`.
* Those are built directly for better parse performance, with the same shape acorn would produce; anything else goes to acorn
* @param {Parser} parser
* @returns {Expression | null}
*/
function read_simple_expression(parser) {
if (!has_lf_line_breaks_only(parser)) return null;
const template = parser.template;
const index = parser.index;
parser.allow_whitespace();
const start = parser.index;
let end = read_word(template, start);
if (end === -1 || is_reserved(template.slice(start, end))) {
parser.index = index;
return null;
}
/** @type {Expression} */
let node = identifier(template, start, end);
while (template[end] === '.') {
const property_end = read_word(template, end + 1);
if (property_end === -1) {
parser.index = index;
return null;
}
node = {
type: 'MemberExpression',
start,
end: property_end,
loc: { start: position(start), end: position(property_end) },
object: node,
property: identifier(template, end + 1, property_end),
computed: false,
optional: false
};
end = property_end;
}
parser.index = end;
parser.allow_whitespace();
if (!parser.match('}')) {
parser.index = index;
return null;
}
parser.index = end;
return node;
}
/**
* @param {string} template
* @param {number} start
* @returns {number} the end of the identifier starting at `start`, or -1
*/
function read_word(template, start) {
if (start >= template.length) return -1;
const code = /** @type {number} */ (template.codePointAt(start));
if (!isIdentifierStart(code, true)) return -1;
let end = start + (code <= 0xffff ? 1 : 2);
while (end < template.length) {
const code = /** @type {number} */ (template.codePointAt(end));
if (!isIdentifierChar(code, true)) break;
end += code <= 0xffff ? 1 : 2;
}
return end;
}
/**
* @param {string} template
* @param {number} start
* @param {number} end
* @returns {Identifier}
*/
function identifier(template, start, end) {
return {
type: 'Identifier',
start,
end,
loc: { start: position(start), end: position(end) },
name: template.slice(start, end)
};
}
/** @param {number} index */
function position(index) {
const { line, column } = locator(index);
return { line, column };
}

@ -22,7 +22,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
*/
export function read_script(parser, start, attributes) {
const script_start = parser.index;
const data = parser.read_until(regex_closing_script_tag);
const data = parser.read_until_regex(regex_closing_script_tag);
if (parser.index >= parser.template.length) {
e.element_unclosed(parser.template.length, 'script');
}

@ -16,8 +16,6 @@ const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_UNICODE_SEQUENCE = /\\[0-9a-fA-F]{1,6}(\r\n|\s)?/y;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
@ -478,7 +476,7 @@ function read_block_item(parser) {
function read_declaration(parser) {
const start = parser.index;
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
const property = parser.read_until_regex(REGEX_WHITESPACE_OR_COLON);
parser.allow_whitespace();
parser.eat(':');
let index = parser.index;
@ -661,7 +659,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) {
}
if (parser.eat('<!--')) {
parser.read_until(REGEX_HTML_COMMENT_CLOSE);
parser.read_until('-->');
parser.eat('-->', true);
}
@ -676,7 +674,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) {
function read_comment(parser) {
const start = parser.index;
parser.eat('/*', true);
const value = parser.read_until(REGEX_COMMENT_CLOSE);
const value = parser.read_until('*/');
parser.eat('*/', true);
const end = parser.index;

@ -15,13 +15,10 @@ import { get_attribute_expression, is_expression_attribute } from '../../../util
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { locator } from '../../../state.js';
import * as b from '#compiler/builders';
import { is_whitespace } from '../utils/whitespace.js';
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
const regex_closing_comment = /-->/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /["']/y;
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
const regex_doctype_name = /^![a-zA-Z]+$/;
@ -67,7 +64,7 @@ export default function element(parser) {
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(regex_closing_comment);
const data = parser.read_until('-->');
parser.eat('-->', true);
parser.append({
@ -81,7 +78,7 @@ export default function element(parser) {
}
if (parser.eat('/')) {
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
const name = read_tag_name(parser);
parser.allow_whitespace();
parser.eat('>', true);
@ -137,7 +134,7 @@ export default function element(parser) {
return;
}
const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag);
const tag = read_tag(parser);
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
@ -475,7 +472,7 @@ function parent_is_shadowroot_template(stack) {
function read_static_attribute(parser) {
const start = parser.index;
const tag = read_tag(parser, regex_token_ending_character);
const tag = read_tag(parser, true);
if (!tag.name) return null;
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
@ -607,7 +604,7 @@ function read_attribute(parser) {
}
}
const tag = read_tag(parser, regex_token_ending_character);
const tag = read_tag(parser, true);
if (!tag.name) return null;
@ -731,7 +728,7 @@ function read_comment(parser) {
const start = parser.index;
if (parser.eat('//')) {
const value = parser.read_until(/\n/);
const value = parser.read_until('\n');
const end = parser.index;
return {
@ -747,7 +744,7 @@ function read_comment(parser) {
}
if (parser.eat('/*')) {
const value = parser.read_until(/\*\//);
const value = parser.read_until('*/');
parser.eat('*/');
const end = parser.index;
@ -847,25 +844,21 @@ function read_attribute_value(parser) {
* @returns {any[]}
*/
function read_sequence(parser, done, location) {
/** @type {AST.Text} */
let current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
/** @type {Array<AST.Text | AST.ExpressionTag>} */
const chunks = [];
let chunk_start = parser.index;
/** @param {number} end */
function flush(end) {
if (end > current_chunk.start) {
current_chunk.raw = parser.template.slice(current_chunk.start, end);
current_chunk.data = decode_character_references(current_chunk.raw, true);
current_chunk.end = end;
chunks.push(current_chunk);
if (end > chunk_start) {
const raw = parser.template.slice(chunk_start, end);
chunks.push({
start: chunk_start,
end,
type: 'Text',
raw,
data: decode_character_references(raw, true)
});
}
}
@ -879,12 +872,14 @@ function read_sequence(parser, done, location) {
if (parser.match('#')) {
const index = parser.index - 1;
parser.eat('#');
const name = parser.read_until(/[^a-z]/);
// const name = parser.read_until_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.block_invalid_placement(index, name, location);
} else if (parser.match('@')) {
const index = parser.index - 1;
parser.eat('@');
const name = parser.read_until(/[^a-z]/);
// const name = parser.read_until_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.tag_invalid_placement(index, name, location);
}
@ -907,14 +902,7 @@ function read_sequence(parser, done, location) {
};
chunks.push(chunk);
current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
chunk_start = parser.index;
} else {
parser.index++;
}
@ -929,12 +917,36 @@ function read_sequence(parser, done, location) {
/**
* @param {Parser} parser
* @param {RegExp} regex
* @param {boolean} [attribute]
*/
function read_tag_name(parser, attribute = false) {
const start = parser.index;
if (start >= parser.template.length && !parser.loose) e.unexpected_eof(parser.template.length);
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
if (
is_whitespace(cc) ||
cc === 47 || // /
cc === 62 || // >
(attribute && (cc === 34 || cc === 39 || cc === 61)) // " ' =
) {
break;
}
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}
/**
* @param {Parser} parser
* @param {boolean} [attribute]
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
*/
function read_tag(parser, regex) {
function read_tag(parser, attribute = false) {
const start = parser.index;
const name = parser.read_until(regex);
const name = read_tag_name(parser, attribute);
const end = parser.index;
return {
@ -948,3 +960,15 @@ function read_tag(parser, regex) {
}
};
}
/** @param {Parser} parser */
function read_lowercase_name(parser) {
const start = parser.index;
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
// a-z
if (cc < 97 || cc > 122) break;
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}

@ -488,12 +488,13 @@ function open(parser) {
parser.eat(')', true);
}
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index);
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(parser, prelude + `${params} => {}`, params_start)
parse_expression_at(
parser,
parser.template.slice(0, parser.index) + ' => {}',
params_start
)
)
: { params: [] };

@ -38,6 +38,8 @@ const entity_pattern_attr_value = get_entity_pattern(true);
* @param {boolean} is_attribute_value
*/
export function decode_character_references(html, is_attribute_value) {
if (html.indexOf('&') === -1) return html; // fast path
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
return html.replace(
entity_pattern,

@ -0,0 +1,18 @@
/** @param {number} cc */
export function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}

@ -80,6 +80,14 @@ export function build_element_attributes(node, context, transform) {
) {
events_to_capture.add(attribute.name);
}
} else if (
node.type === 'RegularElement' &&
node.name === 'input' &&
(attribute.name === 'defaultValue' || attribute.name === 'defaultChecked')
) {
attributes.push(attribute);
// deopt to spread at runtime, where we can handle interaction of value/defaultValue etc
has_spread = true;
// the defaultValue/defaultChecked properties don't exist as attributes
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') {

@ -33,10 +33,14 @@ export let component_name = '<unknown>';
export let source;
/**
* The source code split into lines (set by `set_source`)
* @type {string[]}
* The source code split into lines, initialized when a diagnostic needs a code frame
* @type {string[] | undefined}
*/
export let source_lines = [];
let source_lines;
export function get_source_lines() {
return (source_lines ??= source.split('\n'));
}
/**
* True if compiling with `dev: true`
@ -61,7 +65,7 @@ export let locator;
/** @param {string} value */
export function set_source(value) {
source = value;
source_lines = source.split('\n');
source_lines = undefined;
const l = getLocator(source, { offsetLine: 1 });
@ -151,7 +155,7 @@ export function reset(state) {
custom_renderer = undefined;
component_name = UNKNOWN_FILENAME;
source = '';
source_lines = [];
source_lines = undefined;
filename = (state.filename ?? UNKNOWN_FILENAME).replace(/\\/g, '/');
warning_filter = state.warning ?? (() => true);
warnings = [];

@ -286,7 +286,7 @@ function _extract_paths(paths, inserts, param, expression, update_expression, ha
const props = [];
for (const p of param.properties) {
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
if (p.type === 'Property') {
if (p.key.type === 'Identifier' && !p.computed) {
props.push(b.literal(p.key.name));
} else if (p.key.type === 'Literal') {
@ -548,10 +548,7 @@ export function is_expression_async(expression) {
if (property.type === 'SpreadElement') {
return is_expression_async(property.argument);
} else if (property.type === 'Property') {
return (
(property.key.type !== 'PrivateIdentifier' && is_expression_async(property.key)) ||
is_expression_async(property.value)
);
return is_expression_async(property.key) || is_expression_async(property.value);
}
});
}

@ -15,7 +15,7 @@ function tabs_to_spaces(str) {
* @param {number} column
*/
function get_code_frame(line, column) {
const lines = state.source_lines;
const lines = state.get_source_lines();
const frame_start = Math.max(0, line - 2);
const frame_end = Math.min(line + 3, lines.length);
const digits = String(frame_end + 1).length;

@ -445,22 +445,6 @@ export function rune_outside_svelte(rune) {
}
}
/**
* `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
* @returns {never}
*/
export function set_context_after_init() {
if (DEV) {
const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/set_context_after_init`);
}
}
/**
* A snippet created in a component with a custom renderer cannot be rendered by a different renderer
* @returns {never}

@ -1,6 +1,7 @@
/** @import { SSRContext } from '#server' */
import { DEV } from 'esm-env';
import { create_context, get_or_init_context_map } from '../shared/context.js';
import * as e from './errors.js';
/** @type {SSRContext | null} */
export var ssr_context = null;
@ -40,7 +41,13 @@ export function getContext(key) {
* @returns {T}
*/
export function setContext(key, context) {
get_or_init_context_map(ssr_context, 'setContext').set(key, context);
const context_map = get_or_init_context_map(ssr_context, 'setContext');
if (/** @type {SSRContext} */ (ssr_context).i) {
e.set_context_after_init();
}
context_map.set(key, context);
return context;
}
@ -61,7 +68,7 @@ export function getAllContexts() {
* @param {Function} [fn]
*/
export function push(fn) {
ssr_context = { p: ssr_context, c: null, r: null };
ssr_context = { p: ssr_context, c: null, r: null, i: false };
if (DEV) {
ssr_context.function = fn;

@ -146,8 +146,10 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
const is_html = (flags & ELEMENT_IS_NAMESPACED) === 0;
const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 0;
const is_input = (flags & ELEMENT_IS_INPUT) !== 0;
const names = Object.keys(attrs);
for (name of Object.keys(attrs)) {
outer: for (let i = 0; i < names.length; i++) {
name = names[i];
// omit functions, internal svelte properties and invalid attribute names
if (typeof attrs[name] === 'function') continue;
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
@ -163,8 +165,13 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
if (is_input) {
if (name === 'defaultvalue' || name === 'defaultchecked') {
// value/checked takes precedence over defaultValue/defaultChecked
name = name === 'defaultvalue' ? 'value' : 'checked';
if (attrs[name]) continue;
if (name in attrs) continue;
// We're checking prior entries aswell because "name in attrs" is not enough as the attributes may have different casing
for (let j = 0; j < names.length; j++) {
if (names[j].toLowerCase() === name) continue outer;
}
}
}

@ -162,6 +162,11 @@ export class Renderer {
let promise = Promise.resolve(thunks[0]());
const promises = [promise];
if (context !== null && thunks.length > 1) {
// the remaining thunks run after an `await`, by which point it is too late to set context
context.i = true;
}
for (const fn of thunks.slice(1)) {
promise = promise.then(() => {
const previous_context = ssr_context;
@ -209,7 +214,8 @@ export class Renderer {
...ssr_context,
p: parent,
c: null,
r: child
r: child,
i: ssr_context?.i ?? false
});
const result = fn(child);
@ -260,7 +266,8 @@ export class Renderer {
...ssr_context,
p: parent_context,
c: null,
r: child
r: child,
i: ssr_context?.i ?? false
});
try {
@ -859,7 +866,7 @@ export class Renderer {
try {
/** @type {SSRContext} */
const context = { p: null, c: options.context ?? null, r: renderer };
const context = { p: null, c: options.context ?? null, r: renderer, i: false };
set_ssr_context(context);
renderer.push(BLOCK_OPEN);

@ -9,6 +9,8 @@ export interface SSRContext {
c: null | Map<unknown, unknown>;
/** renderer */
r: null | Renderer;
/** True if initialized, i.e. an `await` was reached */
i: boolean;
/** dev mode only: the current component function */
function?: any;
/** dev mode only: the current element */

@ -101,6 +101,22 @@ export function missing_context() {
}
}
/**
* `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
* @returns {never}
*/
export function set_context_after_init() {
if (DEV) {
const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/set_context_after_init`);
}
}
/**
* Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
* @returns {never}

@ -1,2 +1,8 @@
/** Anything except a function */
export type NotFunction<T> = T extends Function ? never : T;
declare global {
// @ts-ignore devalue has it in its types, but it's not part of the standard lib at the version our runtime is.
// We're not actually doing anything with it so we silence the error this ay
type Float16Array = any;
}

@ -82,11 +82,11 @@
"loc": {
"start": {
"line": 5,
"column": 19
"column": 18
},
"end": {
"line": 5,
"column": 40
"column": 39
}
},
"elements": [
@ -97,11 +97,11 @@
"loc": {
"start": {
"line": 5,
"column": 20
"column": 19
},
"end": {
"line": 5,
"column": 23
"column": 22
}
},
"name": "key"
@ -113,11 +113,11 @@
"loc": {
"start": {
"line": 5,
"column": 25
"column": 24
},
"end": {
"line": 5,
"column": 30
"column": 29
}
},
"name": "value"
@ -129,11 +129,11 @@
"loc": {
"start": {
"line": 5,
"column": 32
"column": 31
},
"end": {
"line": 5,
"column": 39
"column": 38
}
},
"argument": {
@ -143,11 +143,11 @@
"loc": {
"start": {
"line": 5,
"column": 35
"column": 34
},
"end": {
"line": 5,
"column": 39
"column": 38
}
},
"name": "rest"

@ -170,11 +170,11 @@
"loc": {
"start": {
"line": 3,
"column": 13
"column": 12
},
"end": {
"line": 3,
"column": 24
"column": 23
}
},
"properties": [
@ -185,11 +185,11 @@
"loc": {
"start": {
"line": 3,
"column": 15
"column": 14
},
"end": {
"line": 3,
"column": 22
"column": 21
}
},
"method": false,
@ -202,11 +202,11 @@
"loc": {
"start": {
"line": 3,
"column": 15
"column": 14
},
"end": {
"line": 3,
"column": 16
"column": 15
}
},
"name": "y"
@ -218,11 +218,11 @@
"loc": {
"start": {
"line": 3,
"column": 15
"column": 14
},
"end": {
"line": 3,
"column": 22
"column": 21
}
},
"left": {
@ -232,11 +232,11 @@
"loc": {
"start": {
"line": 3,
"column": 15
"column": 14
},
"end": {
"line": 3,
"column": 16
"column": 15
}
},
"name": "y"
@ -248,11 +248,11 @@
"loc": {
"start": {
"line": 3,
"column": 19
"column": 18
},
"end": {
"line": 3,
"column": 22
"column": 21
}
},
"value": "{",
@ -302,11 +302,11 @@
"loc": {
"start": {
"line": 5,
"column": 13
"column": 12
},
"end": {
"line": 5,
"column": 24
"column": 23
}
},
"properties": [
@ -317,11 +317,11 @@
"loc": {
"start": {
"line": 5,
"column": 15
"column": 14
},
"end": {
"line": 5,
"column": 22
"column": 21
}
},
"method": false,
@ -334,11 +334,11 @@
"loc": {
"start": {
"line": 5,
"column": 15
"column": 14
},
"end": {
"line": 5,
"column": 16
"column": 15
}
},
"name": "y"
@ -350,11 +350,11 @@
"loc": {
"start": {
"line": 5,
"column": 15
"column": 14
},
"end": {
"line": 5,
"column": 22
"column": 21
}
},
"left": {
@ -364,11 +364,11 @@
"loc": {
"start": {
"line": 5,
"column": 15
"column": 14
},
"end": {
"line": 5,
"column": 16
"column": 15
}
},
"name": "y"
@ -380,11 +380,11 @@
"loc": {
"start": {
"line": 5,
"column": 19
"column": 18
},
"end": {
"line": 5,
"column": 22
"column": 21
}
},
"value": "]",
@ -434,11 +434,11 @@
"loc": {
"start": {
"line": 7,
"column": 13
"column": 12
},
"end": {
"line": 7,
"column": 29
"column": 28
}
},
"properties": [
@ -449,11 +449,11 @@
"loc": {
"start": {
"line": 7,
"column": 15
"column": 14
},
"end": {
"line": 7,
"column": 27
"column": 26
}
},
"method": false,
@ -466,11 +466,11 @@
"loc": {
"start": {
"line": 7,
"column": 15
"column": 14
},
"end": {
"line": 7,
"column": 16
"column": 15
}
},
"name": "y"
@ -482,11 +482,11 @@
"loc": {
"start": {
"line": 7,
"column": 15
"column": 14
},
"end": {
"line": 7,
"column": 27
"column": 26
}
},
"left": {
@ -496,11 +496,11 @@
"loc": {
"start": {
"line": 7,
"column": 15
"column": 14
},
"end": {
"line": 7,
"column": 16
"column": 15
}
},
"name": "y"
@ -512,11 +512,11 @@
"loc": {
"start": {
"line": 7,
"column": 19
"column": 18
},
"end": {
"line": 7,
"column": 27
"column": 26
}
},
"expressions": [
@ -527,11 +527,11 @@
"loc": {
"start": {
"line": 7,
"column": 22
"column": 21
},
"end": {
"line": 7,
"column": 25
"column": 24
}
},
"expressions": [],
@ -543,11 +543,11 @@
"loc": {
"start": {
"line": 7,
"column": 23
"column": 22
},
"end": {
"line": 7,
"column": 24
"column": 23
}
},
"value": {
@ -567,11 +567,11 @@
"loc": {
"start": {
"line": 7,
"column": 20
"column": 19
},
"end": {
"line": 7,
"column": 20
"column": 19
}
},
"value": {
@ -587,11 +587,11 @@
"loc": {
"start": {
"line": 7,
"column": 26
"column": 25
},
"end": {
"line": 7,
"column": 26
"column": 25
}
},
"value": {
@ -646,11 +646,11 @@
"loc": {
"start": {
"line": 9,
"column": 13
"column": 12
},
"end": {
"line": 9,
"column": 32
"column": 31
}
},
"properties": [
@ -661,11 +661,11 @@
"loc": {
"start": {
"line": 9,
"column": 15
"column": 14
},
"end": {
"line": 9,
"column": 30
"column": 29
}
},
"method": false,
@ -678,11 +678,11 @@
"loc": {
"start": {
"line": 9,
"column": 15
"column": 14
},
"end": {
"line": 9,
"column": 16
"column": 15
}
},
"name": "y"
@ -694,11 +694,11 @@
"loc": {
"start": {
"line": 9,
"column": 15
"column": 14
},
"end": {
"line": 9,
"column": 30
"column": 29
}
},
"left": {
@ -708,11 +708,11 @@
"loc": {
"start": {
"line": 9,
"column": 15
"column": 14
},
"end": {
"line": 9,
"column": 16
"column": 15
}
},
"name": "y"
@ -724,11 +724,11 @@
"loc": {
"start": {
"line": 9,
"column": 19
"column": 18
},
"end": {
"line": 9,
"column": 30
"column": 29
}
},
"expressions": [
@ -739,11 +739,11 @@
"loc": {
"start": {
"line": 9,
"column": 22
"column": 21
},
"end": {
"line": 9,
"column": 28
"column": 27
}
},
"expressions": [],
@ -755,11 +755,11 @@
"loc": {
"start": {
"line": 9,
"column": 23
"column": 22
},
"end": {
"line": 9,
"column": 27
"column": 26
}
},
"value": {
@ -779,11 +779,11 @@
"loc": {
"start": {
"line": 9,
"column": 20
"column": 19
},
"end": {
"line": 9,
"column": 20
"column": 19
}
},
"value": {
@ -799,11 +799,11 @@
"loc": {
"start": {
"line": 9,
"column": 29
"column": 28
},
"end": {
"line": 9,
"column": 29
"column": 28
}
},
"value": {
@ -822,5 +822,6 @@
}
]
},
"options": null
"options": null,
"comments": []
}

@ -495,11 +495,11 @@
"loc": {
"start": {
"line": 5,
"column": 18
"column": 17
},
"end": {
"line": 5,
"column": 57
"column": 56
}
},
"properties": [
@ -510,11 +510,11 @@
"loc": {
"start": {
"line": 5,
"column": 20
"column": 19
},
"end": {
"line": 5,
"column": 42
"column": 41
}
},
"method": false,
@ -527,11 +527,11 @@
"loc": {
"start": {
"line": 5,
"column": 20
"column": 19
},
"end": {
"line": 5,
"column": 24
"column": 23
}
},
"name": "name"
@ -543,11 +543,11 @@
"loc": {
"start": {
"line": 5,
"column": 20
"column": 19
},
"end": {
"line": 5,
"column": 42
"column": 41
}
},
"left": {
@ -557,11 +557,11 @@
"loc": {
"start": {
"line": 5,
"column": 20
"column": 19
},
"end": {
"line": 5,
"column": 24
"column": 23
}
},
"name": "name"
@ -573,11 +573,11 @@
"loc": {
"start": {
"line": 5,
"column": 27
"column": 26
},
"end": {
"line": 5,
"column": 42
"column": 41
}
},
"expressions": [
@ -588,11 +588,11 @@
"loc": {
"start": {
"line": 5,
"column": 35
"column": 34
},
"end": {
"line": 5,
"column": 40
"column": 39
}
},
"value": "Doe",
@ -607,11 +607,11 @@
"loc": {
"start": {
"line": 5,
"column": 28
"column": 27
},
"end": {
"line": 5,
"column": 33
"column": 32
}
},
"value": {
@ -627,11 +627,11 @@
"loc": {
"start": {
"line": 5,
"column": 41
"column": 40
},
"end": {
"line": 5,
"column": 41
"column": 40
}
},
"value": {
@ -652,11 +652,11 @@
"loc": {
"start": {
"line": 5,
"column": 44
"column": 43
},
"end": {
"line": 5,
"column": 55
"column": 54
}
},
"method": false,
@ -669,11 +669,11 @@
"loc": {
"start": {
"line": 5,
"column": 44
"column": 43
},
"end": {
"line": 5,
"column": 48
"column": 47
}
},
"name": "cool"
@ -685,11 +685,11 @@
"loc": {
"start": {
"line": 5,
"column": 44
"column": 43
},
"end": {
"line": 5,
"column": 55
"column": 54
}
},
"left": {
@ -699,11 +699,11 @@
"loc": {
"start": {
"line": 5,
"column": 44
"column": 43
},
"end": {
"line": 5,
"column": 48
"column": 47
}
},
"name": "cool"
@ -715,11 +715,11 @@
"loc": {
"start": {
"line": 5,
"column": 51
"column": 50
},
"end": {
"line": 5,
"column": 55
"column": 54
}
},
"value": true,
@ -906,11 +906,11 @@
"loc": {
"start": {
"line": 9,
"column": 18
"column": 17
},
"end": {
"line": 9,
"column": 79
"column": 78
}
},
"properties": [
@ -921,11 +921,11 @@
"loc": {
"start": {
"line": 9,
"column": 20
"column": 19
},
"end": {
"line": 9,
"column": 64
"column": 63
}
},
"method": false,
@ -938,11 +938,11 @@
"loc": {
"start": {
"line": 9,
"column": 20
"column": 19
},
"end": {
"line": 9,
"column": 24
"column": 23
}
},
"name": "name"
@ -954,11 +954,11 @@
"loc": {
"start": {
"line": 9,
"column": 20
"column": 19
},
"end": {
"line": 9,
"column": 64
"column": 63
}
},
"left": {
@ -968,11 +968,11 @@
"loc": {
"start": {
"line": 9,
"column": 20
"column": 19
},
"end": {
"line": 9,
"column": 24
"column": 23
}
},
"name": "name"
@ -984,11 +984,11 @@
"loc": {
"start": {
"line": 9,
"column": 27
"column": 26
},
"end": {
"line": 9,
"column": 64
"column": 63
}
},
"callee": {
@ -998,11 +998,11 @@
"loc": {
"start": {
"line": 9,
"column": 28
"column": 27
},
"end": {
"line": 9,
"column": 61
"column": 60
}
},
"id": null,
@ -1017,11 +1017,11 @@
"loc": {
"start": {
"line": 9,
"column": 34
"column": 33
},
"end": {
"line": 9,
"column": 61
"column": 60
}
},
"body": [
@ -1032,11 +1032,11 @@
"loc": {
"start": {
"line": 9,
"column": 36
"column": 35
},
"end": {
"line": 9,
"column": 59
"column": 58
}
},
"argument": {
@ -1046,11 +1046,11 @@
"loc": {
"start": {
"line": 9,
"column": 43
"column": 42
},
"end": {
"line": 9,
"column": 58
"column": 57
}
},
"expressions": [
@ -1061,11 +1061,11 @@
"loc": {
"start": {
"line": 9,
"column": 51
"column": 50
},
"end": {
"line": 9,
"column": 56
"column": 55
}
},
"value": "Doe",
@ -1080,11 +1080,11 @@
"loc": {
"start": {
"line": 9,
"column": 44
"column": 43
},
"end": {
"line": 9,
"column": 49
"column": 48
}
},
"value": {
@ -1100,11 +1100,11 @@
"loc": {
"start": {
"line": 9,
"column": 57
"column": 56
},
"end": {
"line": 9,
"column": 57
"column": 56
}
},
"value": {
@ -1132,11 +1132,11 @@
"loc": {
"start": {
"line": 9,
"column": 66
"column": 65
},
"end": {
"line": 9,
"column": 77
"column": 76
}
},
"method": false,
@ -1149,11 +1149,11 @@
"loc": {
"start": {
"line": 9,
"column": 66
"column": 65
},
"end": {
"line": 9,
"column": 70
"column": 69
}
},
"name": "cool"
@ -1165,11 +1165,11 @@
"loc": {
"start": {
"line": 9,
"column": 66
"column": 65
},
"end": {
"line": 9,
"column": 77
"column": 76
}
},
"left": {
@ -1179,11 +1179,11 @@
"loc": {
"start": {
"line": 9,
"column": 66
"column": 65
},
"end": {
"line": 9,
"column": 70
"column": 69
}
},
"name": "cool"
@ -1195,11 +1195,11 @@
"loc": {
"start": {
"line": 9,
"column": 73
"column": 72
},
"end": {
"line": 9,
"column": 77
"column": 76
}
},
"value": true,
@ -1213,5 +1213,6 @@
}
]
},
"options": null
"options": null,
"comments": []
}

@ -133,11 +133,11 @@
"loc": {
"start": {
"line": 5,
"column": 15
"column": 14
},
"end": {
"line": 5,
"column": 39
"column": 38
}
},
"elements": [
@ -148,11 +148,11 @@
"loc": {
"start": {
"line": 5,
"column": 16
"column": 15
},
"end": {
"line": 5,
"column": 19
"column": 18
}
},
"name": "key"
@ -164,11 +164,11 @@
"loc": {
"start": {
"line": 5,
"column": 21
"column": 20
},
"end": {
"line": 5,
"column": 38
"column": 37
}
},
"left": {
@ -178,11 +178,11 @@
"loc": {
"start": {
"line": 5,
"column": 21
"column": 20
},
"end": {
"line": 5,
"column": 26
"column": 25
}
},
"name": "value"
@ -194,11 +194,11 @@
"loc": {
"start": {
"line": 5,
"column": 29
"column": 28
},
"end": {
"line": 5,
"column": 38
"column": 37
}
},
"value": "default",
@ -211,6 +211,7 @@
]
},
"options": null,
"comments": [],
"instance": {
"type": "Script",
"start": 0,

@ -0,0 +1,9 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, logs }) {
flushSync(() => target.querySelector('button')?.click());
assert.deepEqual(logs, ['setup: two', 'cleanup: two']);
}
});

@ -0,0 +1,28 @@
<script>
let start = $state(false);
let enabled = $state(false);
let advance = $state(false);
let value = $state('one');
$effect(() => {
if (start) {
value = 'two';
enabled = true;
}
});
$effect(() => {
if (enabled) {
console.log(`setup: ${value}`);
advance = true;
return () => console.log(`cleanup: ${value}`);
}
});
$effect(() => {
if (advance) enabled = false;
});
</script>
<button onclick={() => (start = true)}>start</button>

@ -1,7 +1,6 @@
import { test } from '../../test';
export default test({
skip: true, // TODO it appears there might be an actual bug here; the promise isn't ever actually awaited in spite of being awaited in the component
mode: ['async'],
error: 'lifecycle_outside_component'
error: 'set_context_after_init'
});

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,6 @@
<input value="hello" />
<input type="checkbox" checked />
<input value="spread" checked />
<input value="" />
<input value="" />
<input value="" />

@ -0,0 +1,10 @@
<script>
const props = { defaultValue: 'spread', defaultChecked: true };
</script>
<input defaultValue="hello" />
<input type="checkbox" defaultChecked />
<input {...props} />
<input {...props} value="" checked={false} />
<input value="" checked={false} {...props} />
<input {...props} {...{ VALUE: '', CHECKED: false }} />

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save