Skip to content

Commit f76a92f

Browse files
authored
Merge branch 'main' into fix-ssr-boundary-in-async
2 parents 0010a26 + 0e709e3 commit f76a92f

File tree

99 files changed

+1454
-416
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+1454
-416
lines changed

.changeset/legal-mangos-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: change title only after any pending work has completed

packages/svelte/CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# svelte
22

3+
## 5.43.2
4+
5+
### Patch Changes
6+
7+
- fix: treat each blocks with async dependencies as uncontrolled ([#17077](https://github.com/sveltejs/svelte/pull/17077))
8+
9+
## 5.43.1
10+
11+
### Patch Changes
12+
13+
- fix: transform `$bindable` after `await` expressions ([#17066](https://github.com/sveltejs/svelte/pull/17066))
14+
15+
## 5.43.0
16+
17+
### Minor Changes
18+
19+
- feat: out-of-order rendering ([#17038](https://github.com/sveltejs/svelte/pull/17038))
20+
21+
### Patch Changes
22+
23+
- fix: settle batch after DOM updates ([#17054](https://github.com/sveltejs/svelte/pull/17054))
24+
25+
## 5.42.3
26+
27+
### Patch Changes
28+
29+
- fix: handle `<svelte:head>` rendered asynchronously ([#17052](https://github.com/sveltejs/svelte/pull/17052))
30+
31+
- fix: don't restore batch in `#await` ([#17051](https://github.com/sveltejs/svelte/pull/17051))
32+
333
## 5.42.2
434

535
### Patch Changes

packages/svelte/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "svelte",
33
"description": "Cybernetically enhanced web apps",
44
"license": "MIT",
5-
"version": "5.42.2",
5+
"version": "5.43.2",
66
"type": "module",
77
"types": "./types/index.d.ts",
88
"engines": {

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { walk } from 'zimmerframe';
66
import { parse } from '../1-parse/acorn.js';
77
import * as e from '../../errors.js';
88
import * as w from '../../warnings.js';
9-
import { extract_identifiers, has_await_expression } from '../../utils/ast.js';
9+
import {
10+
extract_identifiers,
11+
has_await_expression,
12+
object,
13+
unwrap_pattern
14+
} from '../../utils/ast.js';
1015
import * as b from '#compiler/builders';
1116
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
1217
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
@@ -543,7 +548,13 @@ export function analyze_component(root, source, options) {
543548
snippet_renderers: new Map(),
544549
snippets: new Set(),
545550
async_deriveds: new Set(),
546-
pickled_awaits: new Set()
551+
pickled_awaits: new Set(),
552+
instance_body: {
553+
sync: [],
554+
async: [],
555+
declarations: [],
556+
hoisted: []
557+
}
547558
};
548559

549560
if (!runes) {
@@ -676,6 +687,194 @@ export function analyze_component(root, source, options) {
676687
}
677688
}
678689

690+
/**
691+
* @param {ESTree.Node} expression
692+
* @param {Scope} scope
693+
* @param {Set<Binding>} touched
694+
* @param {Set<ESTree.Node>} seen
695+
*/
696+
const touch = (expression, scope, touched, seen = new Set()) => {
697+
if (seen.has(expression)) return;
698+
seen.add(expression);
699+
700+
walk(
701+
expression,
702+
{ scope },
703+
{
704+
ImportDeclaration(node) {},
705+
Identifier(node, context) {
706+
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
707+
if (is_reference(node, parent)) {
708+
const binding = context.state.scope.get(node.name);
709+
if (binding) {
710+
touched.add(binding);
711+
712+
for (const assignment of binding.assignments) {
713+
touch(assignment.value, assignment.scope, touched, seen);
714+
}
715+
}
716+
}
717+
}
718+
}
719+
);
720+
};
721+
722+
/**
723+
* @param {ESTree.Node} node
724+
* @param {Set<ESTree.Node>} seen
725+
* @param {Set<Binding>} reads
726+
* @param {Set<Binding>} writes
727+
*/
728+
const trace_references = (node, reads, writes, seen = new Set()) => {
729+
if (seen.has(node)) return;
730+
seen.add(node);
731+
732+
/**
733+
* @param {ESTree.Pattern} node
734+
* @param {Scope} scope
735+
*/
736+
function update(node, scope) {
737+
for (const pattern of unwrap_pattern(node)) {
738+
const node = object(pattern);
739+
if (!node) return;
740+
741+
const binding = scope.get(node.name);
742+
if (!binding) return;
743+
744+
writes.add(binding);
745+
}
746+
}
747+
748+
walk(
749+
node,
750+
{ scope: instance.scope },
751+
{
752+
_(node, context) {
753+
const scope = scopes.get(node);
754+
if (scope) {
755+
context.next({ scope });
756+
} else {
757+
context.next();
758+
}
759+
},
760+
AssignmentExpression(node, context) {
761+
update(node.left, context.state.scope);
762+
},
763+
UpdateExpression(node, context) {
764+
update(
765+
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
766+
context.state.scope
767+
);
768+
},
769+
CallExpression(node, context) {
770+
// for now, assume everything touched by the callee ends up mutating the object
771+
// TODO optimise this better
772+
773+
// special case — no need to peek inside effects as they only run once async work has completed
774+
const rune = get_rune(node, context.state.scope);
775+
if (rune === '$effect') return;
776+
777+
/** @type {Set<Binding>} */
778+
const touched = new Set();
779+
touch(node, context.state.scope, touched);
780+
781+
for (const b of touched) {
782+
writes.add(b);
783+
}
784+
},
785+
// don't look inside functions until they are called
786+
ArrowFunctionExpression(_, context) {},
787+
FunctionDeclaration(_, context) {},
788+
FunctionExpression(_, context) {},
789+
Identifier(node, context) {
790+
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
791+
if (is_reference(node, parent)) {
792+
const binding = context.state.scope.get(node.name);
793+
if (binding) {
794+
reads.add(binding);
795+
}
796+
}
797+
}
798+
}
799+
);
800+
};
801+
802+
let awaited = false;
803+
804+
// TODO this should probably be attached to the scope?
805+
var promises = b.id('$$promises');
806+
807+
/**
808+
* @param {ESTree.Identifier} id
809+
* @param {ESTree.Expression} blocker
810+
*/
811+
function push_declaration(id, blocker) {
812+
analysis.instance_body.declarations.push(id);
813+
814+
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
815+
binding.blocker = blocker;
816+
}
817+
818+
for (let node of instance.ast.body) {
819+
if (node.type === 'ImportDeclaration') {
820+
analysis.instance_body.hoisted.push(node);
821+
continue;
822+
}
823+
824+
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
825+
// these can't exist inside `<script>` but TypeScript doesn't know that
826+
continue;
827+
}
828+
829+
if (node.type === 'ExportNamedDeclaration') {
830+
if (node.declaration) {
831+
node = node.declaration;
832+
} else {
833+
continue;
834+
}
835+
}
836+
837+
const has_await = has_await_expression(node);
838+
awaited ||= has_await;
839+
840+
if (awaited && node.type !== 'FunctionDeclaration') {
841+
/** @type {Set<Binding>} */
842+
const reads = new Set(); // TODO we're not actually using this yet
843+
844+
/** @type {Set<Binding>} */
845+
const writes = new Set();
846+
847+
trace_references(node, reads, writes);
848+
849+
const blocker = b.member(promises, b.literal(analysis.instance_body.async.length), true);
850+
851+
for (const binding of writes) {
852+
binding.blocker = blocker;
853+
}
854+
855+
if (node.type === 'VariableDeclaration') {
856+
for (const declarator of node.declarations) {
857+
for (const id of extract_identifiers(declarator.id)) {
858+
push_declaration(id, blocker);
859+
}
860+
861+
// one declarator per declaration, makes things simpler
862+
analysis.instance_body.async.push({
863+
node: declarator,
864+
has_await
865+
});
866+
}
867+
} else if (node.type === 'ClassDeclaration') {
868+
push_declaration(node.id, blocker);
869+
analysis.instance_body.async.push({ node, has_await });
870+
} else {
871+
analysis.instance_body.async.push({ node, has_await });
872+
}
873+
} else {
874+
analysis.instance_body.sync.push(node);
875+
}
876+
}
877+
679878
if (analysis.runes) {
680879
const props_refs = module.scope.references.get('$$props');
681880
if (props_refs) {

packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,13 @@ import * as e from '../../../errors.js';
1010
export function AwaitExpression(node, context) {
1111
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
1212

13-
// preserve context for
14-
// a) top-level await and
15-
// b) awaits that precede other expressions in template or `$derived(...)`
13+
// preserve context for awaits that precede other expressions in template or `$derived(...)`
1614
if (
17-
tla ||
18-
(is_reactive_expression(
15+
is_reactive_expression(
1916
context.path,
2017
context.state.derived_function_depth === context.state.function_depth
2118
) &&
22-
!is_last_evaluated_expression(context.path, node))
19+
!is_last_evaluated_expression(context.path, node)
2320
) {
2421
context.state.analysis.pickled_awaits.add(node);
2522
}
@@ -145,6 +142,9 @@ function is_last_evaluated_expression(path, node) {
145142
if (node !== parent.expressions.at(-1)) return false;
146143
break;
147144

145+
case 'VariableDeclarator':
146+
return true;
147+
148148
default:
149149
return false;
150150
}

packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export function BindDirective(node, context) {
172172
}
173173

174174
const binding = context.state.scope.get(left.name);
175+
node.metadata.binding = binding;
175176

176177
if (assignee.type === 'Identifier') {
177178
// reassignment

packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,13 @@ export function SnippetBlock(node, context) {
8181
function can_hoist_snippet(scope, scopes, visited = new Set()) {
8282
for (const [reference] of scope.references) {
8383
const binding = scope.get(reference);
84+
if (!binding) continue;
8485

85-
if (!binding || binding.scope.function_depth === 0) {
86+
if (binding.blocker) {
87+
return false;
88+
}
89+
90+
if (binding.scope.function_depth === 0) {
8691
continue;
8792
}
8893

0 commit comments

Comments
 (0)