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

svelte-custom-renderer
paoloricciuti 2 weeks ago
commit b3f3a77e35

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve global CSS in components without scopable elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reduce SSR render result garbage collection

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: resolve the fallback of an each block in the enclosing scope

@ -25,12 +25,12 @@ export const wrapper_bench = {
// Do 3 loops to warm up JIT // Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
render(App); render(App).body;
} }
return await fastest_test(10, () => { return await fastest_test(10, () => {
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
render(App); render(App).body;
} }
}); });
} }

@ -5,10 +5,9 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; import fragment from './state/fragment.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js'; import { create_fragment, disallow_children } from '../nodes.js';
import read_options from './read/options.js'; import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js'; import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js'; import * as state from '../../state.js';
import { is_whitespace } from './utils/whitespace.js'; import { is_whitespace } from './utils/whitespace.js';

@ -9,8 +9,12 @@ import read_style from '../read/style.js';
import { decode_character_references } from '../utils/html.js'; import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js'; import {
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js'; create_attribute,
create_fragment,
ExpressionMetadata,
is_element_node
} from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js'; import { list } from '../../../utils/string.js';

@ -3,11 +3,10 @@
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { ExpressionMetadata } from '../../nodes.js'; import { create_fragment, ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at, parse_statement_at } from '../acorn.js'; import { parse_expression_at, parse_statement_at } from '../acorn.js';
import read_pattern from '../read/context.js'; import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js'; import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { find_matching_bracket, match_bracket } from '../utils/bracket.js'; import { find_matching_bracket, match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y; const regex_whitespace_with_closing_curly_brace = /\s*}/y;

@ -1,16 +0,0 @@
/** @import { AST } from '#compiler' */
/**
* @param {any} transparent
* @returns {AST.Fragment}
*/
export function create_fragment(transparent = false) {
return {
type: 'Fragment',
nodes: [],
metadata: {
transparent,
dynamic: false
}
};
}

@ -145,6 +145,19 @@ export function prune(stylesheet, elements) {
}, },
ComplexSelector(node) { ComplexSelector(node) {
const selectors = get_relative_selectors(node); const selectors = get_relative_selectors(node);
const rule = /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule);
const first = selectors[0]?.selectors[0];
const is_icss_export =
selectors.length === 1 &&
selectors[0].selectors.length === 1 &&
first?.type === 'PseudoClassSelector' &&
first.name === 'export' &&
first.args === null;
// Global and ICSS export rules do not depend on an element in this component
if (every_is_global(selectors, 0, selectors.length, rule) || is_icss_export) {
node.metadata.used = true;
}
for (const element of elements) { for (const element of elements) {
seen.clear(); seen.clear();
@ -153,12 +166,7 @@ export function prune(stylesheet, elements) {
// Elements rendered through <svelte:head> are not style-scopable. // Elements rendered through <svelte:head> are not style-scopable.
// Prevent css hash injection (class="s-...") on tags like <meta>, <link>, <script>. // Prevent css hash injection (class="s-...") on tags like <meta>, <link>, <script>.
!is_inside_svelte_head(element) && !is_inside_svelte_head(element) &&
apply_selector( apply_selector(selectors, rule, element, BACKWARD)
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element,
BACKWARD
)
) { ) {
node.metadata.used = true; node.metadata.used = true;
} }

@ -14,7 +14,7 @@ import {
} from '../../utils/ast.js'; } from '../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.js'; import { create_attribute, is_custom_element_node } from '../nodes.js';
import { analyze_css } from './css/css-analyze.js'; import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js'; import { prune } from './css/css-prune.js';

@ -9,7 +9,7 @@ import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js'; import { binding_properties } from '../../bindings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js'; import fuzzymatch from '../../../utils/fuzzymatch.js';
import { is_content_editable_binding, is_svg } from '../../../../utils.js'; import { is_content_editable_binding, is_svg } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';
import { custom_renderer } from '../../../state.js'; import { custom_renderer } from '../../../state.js';

@ -1,7 +1,7 @@
/** @import { ClassDeclaration } from 'estree' */ /** @import { ClassDeclaration } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { validate_identifier_name } from './shared/utils.js'; import { validate_identifier_name } from '../../scope.js';
/** /**
* @param {ClassDeclaration} node * @param {ClassDeclaration} node

@ -1,7 +1,7 @@
/** @import { FunctionDeclaration } from 'estree' */ /** @import { FunctionDeclaration } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { visit_function } from './shared/function.js'; import { visit_function } from './shared/function.js';
import { validate_identifier_name } from './shared/utils.js'; import { validate_identifier_name } from '../../scope.js';
/** /**
* @param {FunctionDeclaration} node * @param {FunctionDeclaration} node

@ -1,12 +1,11 @@
/** @import { Expression, Identifier } from 'estree' */ /** @import { Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js'; import { should_proxy, get_rune } from '../../scope.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js'; import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js'; import { is_component_node } from '../../nodes.js';
/** /**

@ -3,7 +3,7 @@
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { custom_renderer } from '../../../state.js'; import { custom_renderer } from '../../../state.js';
import { is_event_attribute } from '../../../utils/ast.js'; import { is_event_attribute } from '../../../utils/ast.js';
import { disallow_children } from './shared/special-element.js'; import { disallow_children } from '../../nodes.js';
import { check_global_event_reference } from './shared/utils.js'; import { check_global_event_reference } from './shared/utils.js';
/** /**

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js'; import { disallow_children } from '../../nodes.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { custom_renderer } from '../../../state.js'; import { custom_renderer } from '../../../state.js';
import { is_event_attribute } from '../../../utils/ast.js'; import { is_event_attribute } from '../../../utils/ast.js';

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js'; import { disallow_children } from '../../nodes.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { custom_renderer } from '../../../state.js'; import { custom_renderer } from '../../../state.js';
import { is_event_attribute } from '../../../utils/ast.js'; import { is_event_attribute } from '../../../utils/ast.js';

@ -1,8 +1,8 @@
/** @import { Expression, Identifier, Literal, VariableDeclarator } from 'estree' */ /** @import { Expression, Identifier, Literal, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */ /** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { get_rune } from '../../scope.js'; import { get_rune, validate_identifier_name } from '../../scope.js';
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js'; import { ensure_no_module_import_conflict } from './shared/utils.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js'; import { extract_paths } from '../../../utils/ast.js';

@ -45,7 +45,7 @@ import {
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { list } from '../../../../../utils/string.js'; import { list } from '../../../../../utils/string.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import fuzzymatch from '../../../../1-parse/utils/fuzzymatch.js'; import fuzzymatch from '../../../../../utils/fuzzymatch.js';
import { is_content_editable_binding } from '../../../../../../utils.js'; import { is_content_editable_binding } from '../../../../../../utils.js';
import * as w from '../../../../../warnings.js'; import * as w from '../../../../../warnings.js';

@ -1,16 +0,0 @@
/** @import { AST } from '#compiler' */
import * as e from '../../../../errors.js';
/**
* @param {AST.SvelteBody | AST.SvelteDocument | AST.SvelteOptionsRaw | AST.SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}

@ -246,44 +246,6 @@ export function is_pure(node, context) {
return false; return false;
} }
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}
/** /**
* Checks that the exported name is not a derived or reassigned state variable. * Checks that the exported name is not a derived or reassigned state variable.
* @param {Node} node * @param {Node} node

@ -127,44 +127,6 @@ export function is_prop_source(binding, state) {
); );
} }
/**
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
return true;
}
/** /**
* An async thunk. If an `await` inside restores the reaction context via `$.save`, * An async thunk. If an `await` inside restores the reaction context via `$.save`,
* the body exits through `$.unsave` so the context cannot leak into foreign microtasks * the body exits through `$.unsave` so the context cannot leak into foreign microtasks

@ -9,10 +9,10 @@ import {
is_expression_async is_expression_async
} from '../../../../utils/ast.js'; } from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js'; import { dev, locate_node } from '../../../../state.js';
import { build_getter, should_proxy } from '../utils.js'; import { build_getter } from '../utils.js';
import { should_proxy, get_rune } from '../../../scope.js';
import { visit_assignment_expression } from '../../shared/assignments.js'; import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js'; import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js'; import { get_name } from '../../../nodes.js';
/** /**

@ -2,8 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js'; import { get_rune, should_proxy } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { get_inspect_args } from '../../utils.js'; import { get_inspect_args } from '../../utils.js';
/** /**

@ -5,14 +5,8 @@ import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths, save } from '../../../../utils/ast.js'; import { extract_paths, save } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js'; import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune, should_proxy } from '../../../scope.js';
import { import { async_thunk, get_prop_source, is_prop_source, is_state_source } from '../utils.js';
async_thunk,
get_prop_source,
is_prop_source,
is_state_source,
should_proxy
} from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
/** /**

@ -9,7 +9,7 @@ import {
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js'; import { extract_identifiers } from '../../utils/ast.js';
import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js'; import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { set_scope } from '../scope.js'; import { set_scope } from '../scope.js';

@ -1,6 +1,7 @@
/** @import { Expression, PrivateIdentifier, SourceLocation } from 'estree' */ /** @import { Expression, PrivateIdentifier, SourceLocation } from 'estree' */
/** @import { AST, Binding } from '#compiler' */ /** @import { AST, Binding } from '#compiler' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import * as e from '../errors.js';
/** /**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children * All nodes that can appear elsewhere than the top level, have attributes and can contain children
@ -45,6 +46,35 @@ export function is_custom_element_node(node) {
); );
} }
/**
* @param {boolean} transparent
* @returns {AST.Fragment}
*/
export function create_fragment(transparent = false) {
return {
type: 'Fragment',
nodes: [],
metadata: {
transparent,
dynamic: false
}
};
}
/**
* @param {AST.SvelteBody | AST.SvelteDocument | AST.SvelteOptionsRaw | AST.SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}
/** /**
* @param {string} name * @param {string} name
* @param {SourceLocation | null} name_loc * @param {SourceLocation | null} name_loc

@ -14,7 +14,6 @@ import {
} from '../utils/ast.js'; } from '../utils/ast.js';
import { is_reserved, is_rune } from '../../utils.js'; import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js'; import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
const UNKNOWN = Symbol('unknown'); const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */ /** Includes `BigInt` */
@ -1233,6 +1232,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
EachBlock(node, { state, visit }) { EachBlock(node, { state, visit }) {
visit(node.expression); visit(node.expression);
if (node.fallback) visit(node.fallback);
// context and children are a new scope // context and children are a new scope
const scope = state.scope.child(); const scope = state.scope.child();
@ -1278,7 +1278,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
for (const child of node.body.nodes) { for (const child of node.body.nodes) {
visit(child, { scope }); visit(child, { scope });
} }
if (node.fallback) visit(node.fallback, { scope });
node.metadata = { node.metadata = {
expression: new ExpressionMetadata(), expression: new ExpressionMetadata(),
@ -1469,3 +1468,79 @@ function get_global_keypath(node, scope) {
return n.name + joined; return n.name + joined;
} }
/**
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
return true;
}
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}

@ -1,5 +1,5 @@
import { IGNORABLE_RUNTIME_WARNINGS } from '../../constants.js'; import { IGNORABLE_RUNTIME_WARNINGS } from '../../constants.js';
import fuzzymatch from '../phases/1-parse/utils/fuzzymatch.js'; import fuzzymatch from './fuzzymatch.js';
import * as w from '../warnings.js'; import * as w from '../warnings.js';
const regex_svelte_ignore = /^\s*svelte-ignore\s/; const regex_svelte_ignore = /^\s*svelte-ignore\s/;

@ -1,10 +1,10 @@
/** @import { ValidatedCompileOptions } from '#compiler' */ /** @import { ValidatedCompileOptions } from '#compiler' */
/** @import { Processed } from '../preprocess/public.js' */ /** @import { Processed } from '../preprocess/public.js' */
/** @import { SourceMap } from 'magic-string' */ /** @import { SourceMap } from 'magic-string' */
/** @import { Source } from '../preprocess/private.js' */ /** @import { Source } from '../preprocess/private.js' */
/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@jridgewell/remapping' */ /** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@jridgewell/remapping' */
import remapping from '@jridgewell/remapping'; import remapping from '@jridgewell/remapping';
import { push_array } from './push_array.js';
/** /**
* @param {string} s * @param {string} s
@ -12,6 +12,18 @@ import { push_array } from './push_array.js';
function last_line_length(s) { function last_line_length(s) {
return s.length - s.lastIndexOf('\n') - 1; return s.length - s.lastIndexOf('\n') - 1;
} }
/**
* `array.push(...items)` overflows the stack for large `items`.
* @template T
* @param {T[]} array
* @param {T[]} items
*/
function push_array(array, items) {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}
// mutate map in-place // mutate map in-place
/** /**

@ -1,13 +0,0 @@
/**
* Pushes all `items` into `array` using `push`, therefore mutating the array.
* We do this for memory and perf reasons, and because `array.push(...items)` would
* run into a "max call stack size exceeded" error with too many items (~65k).
* @template T
* @param {T[]} array
* @param {T[]} items
*/
export function push_array(array, items) {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}

@ -23,6 +23,87 @@ import { escape_html } from '../../escaping.js';
* @typedef {string | Renderer} RendererItem * @typedef {string | Renderer} RendererItem
*/ */
class RenderResult {
/** @type {() => AccumulatedContent} */
#render;
/** @type {() => Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }>} */
#render_async;
/** @type {AccumulatedContent | undefined} */
#sync;
/** @type {{ script: '' }} */
#hashes = { script: '' };
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }> | undefined} */
#promise;
/**
* @param {() => AccumulatedContent} render
* @param {() => Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }>} render_async
*/
constructor(render, render_async) {
this.#render = render;
this.#render_async = render_async;
}
#get() {
return (this.#sync ??= this.#render());
}
get html() {
return this.#get().body;
}
get head() {
return this.#get().head;
}
get body() {
return this.#get().body;
}
get hashes() {
return this.#hashes;
}
/**
* This is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param {(value: SyncRenderOutput) => TResult1} onfulfilled
* @param {(reason: unknown) => TResult2} onrejected
*/
then(onfulfilled, onrejected) {
if (!async_mode_flag) {
const result = this.#get();
const user_result = onfulfilled({
head: result.head,
body: result.body,
html: result.body,
hashes: { script: [] }
});
return Promise.resolve(user_result);
}
this.#promise ??= this.#render_async().then((result) => {
Object.defineProperty(result, 'html', {
// eslint-disable-next-line getter-return
get: () => {
e.html_deprecated();
}
});
return result;
});
return this.#promise.then(
(result) => onfulfilled(/** @type {SyncRenderOutput} */ (result)),
onrejected
);
}
}
/** /**
* Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents * Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents
* work that may or may not have completed. A renderer can be {@link collect}ed to aggregate the * work that may or may not have completed. A renderer can be {@link collect}ed to aggregate the
@ -534,73 +615,17 @@ export class Renderer {
* @returns {RenderOutput} * @returns {RenderOutput}
*/ */
static render(component, options = {}) { static render(component, options = {}) {
/** @type {AccumulatedContent | undefined} */ return /** @type {RenderOutput} */ (
let sync; /** @type {unknown} */ (
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }> | undefined} */ new RenderResult(
let async; () => Renderer.#render(component, options),
() =>
const result = /** @type {RenderOutput} */ ({}); init_render_context().then(() =>
// making these properties non-enumerable so that console.logging
// doesn't trigger a sync render
Object.defineProperties(result, {
html: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
head: {
get: () => {
return (sync ??= Renderer.#render(component, options)).head;
}
},
body: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
hashes: {
value: {
script: ''
}
},
then: {
value:
/**
* this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param { (value: SyncRenderOutput) => TResult1 } onfulfilled
* @param { (reason: unknown) => TResult2 } onrejected
*/
(onfulfilled, onrejected) => {
if (!async_mode_flag) {
const result = (sync ??= Renderer.#render(component, options));
const user_result = onfulfilled({
head: result.head,
body: result.body,
html: result.body,
hashes: { script: [] }
});
return Promise.resolve(user_result);
}
async ??= init_render_context().then(() =>
with_render_context(() => Renderer.#render_async(component, options)) with_render_context(() => Renderer.#render_async(component, options))
); )
return async.then((result) => { )
Object.defineProperty(result, 'html', { )
// eslint-disable-next-line getter-return );
get: () => {
e.html_deprecated();
}
});
return onfulfilled(/** @type {SyncRenderOutput} */ (result));
}, onrejected);
}
}
});
return result;
} }
/** /**

@ -4,6 +4,16 @@ import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js'; import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
import { getAbortSignal } from './abort-signal.js'; import { getAbortSignal } from './abort-signal.js';
test('render result properties are inherited', () => {
const result = Renderer.render((() => {}) as unknown as Component);
expect(Object.hasOwn(result, 'head')).toBe(false);
expect(Object.hasOwn(result, 'body')).toBe(false);
expect(Object.hasOwn(result, 'html')).toBe(false);
expect(Object.hasOwn(result, 'hashes')).toBe(false);
expect(Object.hasOwn(result, 'then')).toBe(false);
});
test('collects synchronous body content by default', () => { test('collects synchronous body content by default', () => {
const component = (renderer: Renderer) => { const component = (renderer: Renderer) => {
renderer.push('a'); renderer.push('a');

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
warnings: []
});

@ -0,0 +1,2 @@
:export { foo: red; }
:is(td, th) { color: red; }

@ -0,0 +1,6 @@
<svelte:head><meta name="x" content="y" /></svelte:head>
<style>
:export { foo: red; }
:is(:global(td), :global(th)) { color: red; }
</style>

@ -0,0 +1,14 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '<button>outer</button>',
test({ assert, target }) {
const button = target.querySelector('button');
flushSync(() => {
button?.click();
});
assert.htmlEqual(target.innerHTML, '<button>changed</button>');
}
});

@ -0,0 +1,10 @@
<script>
let items = $state([]);
let item = $state('outer');
</script>
{#each items as item}
<p>{item}</p>
{:else}
<button onclick={() => (item = 'changed')}>{item}</button>
{/each}
Loading…
Cancel
Save