Merge branch 'main' into fix/sourcemaps-negative-column-idx

fix/sourcemaps-negative-column-idx
Rich Harris 3 weeks ago committed by GitHub
commit 9484bc643c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: clear batch between runs

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: adjust `loc` property of `Program` nodes created from `<script>` elements

@ -6,6 +6,7 @@ import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_text_attribute } from '../../../utils/ast.js';
import { locator } from '../../../state.js';
const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
@ -39,9 +40,15 @@ export function read_script(parser, start, attributes) {
parser.acorn_error(err);
}
// TODO is this necessary?
ast.start = script_start;
if (ast.loc) {
// Acorn always uses `0` as the start of a `Program`, but for sourcemap purposes
// we need it to be the start of the `<script>` contents
({ line: ast.loc.start.line, column: ast.loc.start.column } = locator(start));
({ line: ast.loc.end.line, column: ast.loc.end.column } = locator(parser.index));
}
/** @type {'default' | 'module'} */
let context = 'default';

@ -21,7 +21,7 @@ import { get_boundary } from './boundary.js';
export function async(node, blockers = [], expressions = [], fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);

@ -2,8 +2,10 @@
import {
BOUNDARY_EFFECT,
COMMENT_NODE,
DIRTY,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT
EFFECT_TRANSPARENT,
MAYBE_DIRTY
} from '#client/constants';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
@ -20,7 +22,8 @@ import {
active_reaction,
get,
set_active_effect,
set_active_reaction
set_active_reaction,
set_signal_status
} from '../../runtime.js';
import {
hydrate_next,
@ -34,11 +37,12 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch } from '../../reactivity/batch.js';
import { Batch, schedule_effect } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
import { create_text } from '../operations.js';
import { defer_effect } from '../../reactivity/utils.js';
/**
* @typedef {{
@ -64,7 +68,7 @@ export class Boundary {
/** @type {Boundary | null} */
parent;
#pending = false;
is_pending = false;
/** @type {TemplateNode} */
#anchor;
@ -101,6 +105,12 @@ export class Boundary {
#is_creating_fallback = false;
/** @type {Set<Effect>} */
#dirty_effects = new Set();
/** @type {Set<Effect>} */
#maybe_dirty_effects = new Set();
/**
* A source containing the number of pending async deriveds/expressions.
* Only created if `$effect.pending()` is used inside the boundary,
@ -134,7 +144,7 @@ export class Boundary {
this.parent = /** @type {Effect} */ (active_effect).b;
this.#pending = !!this.#props.pending;
this.is_pending = !!this.#props.pending;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
@ -164,7 +174,7 @@ export class Boundary {
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.#pending = false;
this.is_pending = false;
}
}
@ -187,7 +197,7 @@ export class Boundary {
// Since server rendered resolved content, we never show pending state
// Even if client-side async operations are still running, the content is already displayed
this.#pending = false;
this.is_pending = false;
}
#hydrate_pending_content() {
@ -212,7 +222,7 @@ export class Boundary {
this.#pending_effect = null;
});
this.#pending = false;
this.is_pending = false;
}
});
}
@ -220,7 +230,7 @@ export class Boundary {
#get_anchor() {
var anchor = this.#anchor;
if (this.#pending) {
if (this.is_pending) {
this.#pending_anchor = create_text();
this.#anchor.before(this.#pending_anchor);
@ -231,11 +241,19 @@ export class Boundary {
}
/**
* Returns `true` if the effect exists inside a boundary whose pending snippet is shown
* Defer an effect inside a pending boundary until the boundary resolves
* @param {Effect} effect
*/
defer_effect(effect) {
defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects);
}
/**
* Returns `false` if the effect exists inside a boundary whose pending snippet is shown
* @returns {boolean}
*/
is_pending() {
return this.#pending || (!!this.parent && this.parent.is_pending());
is_rendered() {
return !this.is_pending && (!this.parent || this.parent.is_rendered());
}
has_pending_snippet() {
@ -298,7 +316,24 @@ export class Boundary {
this.#pending_count += d;
if (this.#pending_count === 0) {
this.#pending = false;
this.is_pending = false;
// any effects that were encountered and deferred during traversal
// should be rescheduled — after the next traversal (which will happen
// immediately, due to the same update that brought us here)
// the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
@ -394,7 +429,7 @@ export class Boundary {
// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
// but it would be really weird to show the parent's boundary on a child reset.
this.#pending = this.has_pending_snippet();
this.is_pending = this.has_pending_snippet();
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
@ -404,7 +439,7 @@ export class Boundary {
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.#pending = false;
this.is_pending = false;
}
};

@ -98,7 +98,6 @@ export {
with_script
} from './dom/template.js';
export {
async_body,
for_await_track_reactivity_loss,
run,
save,

@ -25,7 +25,6 @@ import {
set_from_async_derived
} from './deriveds.js';
import { aborted } from './effects.js';
import { hydrate_next, hydrating, set_hydrate_node, skip_nodes } from '../dom/hydration.js';
/**
* @param {Array<Promise<void>>} blockers
@ -211,51 +210,6 @@ export function unset_context() {
}
}
/**
* @param {TemplateNode} anchor
* @param {(target: TemplateNode) => Promise<void>} fn
*/
export async function async_body(anchor, fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
boundary.update_pending_count(1);
batch.increment(blocking);
var active = /** @type {Effect} */ (active_effect);
var was_hydrating = hydrating;
var next_hydrate_node = undefined;
if (was_hydrating) {
hydrate_next();
next_hydrate_node = skip_nodes(false);
}
try {
var promise = fn(anchor);
} finally {
if (next_hydrate_node) {
set_hydrate_node(next_hydrate_node);
hydrate_next();
}
}
try {
await promise;
} catch (error) {
if (!aborted(active)) {
invoke_error_boundary(error, active);
}
} finally {
boundary.update_pending_count(-1);
batch.decrement(blocking);
unset_context();
}
}
/**
* @param {Array<() => void | Promise<void>>} thunks
*/
@ -264,7 +218,7 @@ export function run(thunks) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
@ -298,17 +252,13 @@ export function run(thunks) {
throw STALE_REACTION;
}
try {
restore();
return fn();
} finally {
// TODO do we need it here as well as below?
unset_context();
}
restore();
return fn();
})
.catch(handle_error)
.finally(() => {
unset_context();
current_batch?.deactivate();
});
promises.push(promise);

@ -1,5 +1,6 @@
/** @import { Fork } from 'svelte' */
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
/** @import { Boundary } from '../dom/blocks/boundary' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
@ -17,7 +18,6 @@ import {
EAGER_EFFECT,
HEAD_EFFECT,
ERROR_VALUE,
WAS_MARKED,
MANAGED_EFFECT
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
@ -37,15 +37,7 @@ import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
/**
* @typedef {{
* parent: EffectTarget | null;
* effect: Effect | null;
* effects: Effect[];
* render_effects: Effect[];
* }} EffectTarget
*/
import { defer_effect } from './utils.js';
/** @type {Set<Batch>} */
const batches = new Set();
@ -161,16 +153,14 @@ export class Batch {
this.apply();
/** @type {EffectTarget} */
var target = {
parent: null,
effect: null,
effects: [],
render_effects: []
};
/** @type {Effect[]} */
var effects = [];
/** @type {Effect[]} */
var render_effects = [];
for (const root of root_effects) {
this.#traverse_effect_tree(root, target);
this.#traverse_effect_tree(root, effects, render_effects);
// Note: #traverse_effect_tree runs block effects eagerly, which can schedule effects,
// which means queued_root_effects now may be filled again.
@ -183,16 +173,16 @@ export class Batch {
}
if (this.is_deferred()) {
this.#defer_effects(target.effects);
this.#defer_effects(target.render_effects);
this.#defer_effects(render_effects);
this.#defer_effects(effects);
} else {
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = this;
current_batch = null;
flush_queued_effects(target.render_effects);
flush_queued_effects(target.effects);
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
@ -206,13 +196,17 @@ export class Batch {
* Traverse the effect tree, executing effects or stashing
* them for later execution as appropriate
* @param {Effect} root
* @param {EffectTarget} target
* @param {Effect[]} effects
* @param {Effect[]} render_effects
*/
#traverse_effect_tree(root, target) {
#traverse_effect_tree(root, effects, render_effects) {
root.f ^= CLEAN;
var effect = root.first;
/** @type {Effect | null} */
var pending_boundary = null;
while (effect !== null) {
var flags = effect.f;
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
@ -220,24 +214,32 @@ export class Batch {
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
if ((effect.f & BOUNDARY_EFFECT) !== 0 && effect.b?.is_pending()) {
target = {
parent: target,
effect,
effects: [],
render_effects: []
};
// Inside a `<svelte:boundary>` with a pending snippet,
// all effects are deferred until the boundary resolves
// (except block/async effects, which run immediately)
if (
async_mode_flag &&
pending_boundary === null &&
(flags & BOUNDARY_EFFECT) !== 0 &&
effect.b?.is_pending
) {
pending_boundary = effect;
}
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if (
pending_boundary !== null &&
(flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0
) {
/** @type {Boundary} */ (pending_boundary.b).defer_effect(effect);
} else if ((flags & EFFECT) !== 0) {
target.effects.push(effect);
effects.push(effect);
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
target.render_effects.push(effect);
render_effects.push(effect);
} else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect);
if ((flags & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect);
update_effect(effect);
}
@ -253,14 +255,8 @@ export class Batch {
effect = effect.next;
while (effect === null && parent !== null) {
if (parent === target.effect) {
// TODO rather than traversing into pending boundaries and deferring the effects,
// could we just attach the effects _to_ the pending boundary and schedule them
// once the boundary is ready?
this.#defer_effects(target.effects);
this.#defer_effects(target.render_effects);
target = /** @type {EffectTarget} */ (target.parent);
if (parent === pending_boundary) {
pending_boundary = null;
}
effect = parent.next;
@ -273,36 +269,8 @@ export class Batch {
* @param {Effect[]} effects
*/
#defer_effects(effects) {
for (const e of effects) {
if ((e.f & DIRTY) !== 0) {
this.#dirty_effects.add(e);
} else if ((e.f & MAYBE_DIRTY) !== 0) {
this.#maybe_dirty_effects.add(e);
}
// Since we're not executing these effects now, we need to clear any WAS_MARKED flags
// so that other batches can correctly reach these effects during their own traversal
this.#clear_marked(e.deps);
// mark as clean so they get scheduled if they depend on pending async state
set_signal_status(e, CLEAN);
}
}
/**
* @param {Value[] | null} deps
*/
#clear_marked(deps) {
if (deps === null) return;
for (const dep of deps) {
if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) {
continue;
}
dep.f ^= WAS_MARKED;
this.#clear_marked(/** @type {Derived} */ (dep).deps);
for (var i = 0; i < effects.length; i += 1) {
defer_effect(effects[i], this.#dirty_effects, this.#maybe_dirty_effects);
}
}
@ -383,14 +351,6 @@ export class Batch {
var previous_batch_values = batch_values;
var is_earlier = true;
/** @type {EffectTarget} */
var dummy_target = {
parent: null,
effect: null,
effects: [],
render_effects: []
};
for (const batch of batches) {
if (batch === this) {
is_earlier = false;
@ -439,10 +399,10 @@ export class Batch {
batch.apply();
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root, dummy_target);
batch.#traverse_effect_tree(root, [], []);
}
// TODO do we need to do anything with `target`? defer block effects?
// TODO do we need to do anything with the dummy effect arrays?
batch.deactivate();
}

@ -150,7 +150,7 @@ export function async_derived(fn, label, location) {
var batch = /** @type {Batch} */ (current_batch);
if (should_suspend) {
var blocking = !boundary.is_pending();
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);

@ -0,0 +1,40 @@
/** @import { Derived, Effect, Value } from '#client' */
import { CLEAN, DERIVED, DIRTY, MAYBE_DIRTY, WAS_MARKED } from '#client/constants';
import { set_signal_status } from '../runtime.js';
/**
* @param {Value[] | null} deps
*/
function clear_marked(deps) {
if (deps === null) return;
for (const dep of deps) {
if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) {
continue;
}
dep.f ^= WAS_MARKED;
clear_marked(/** @type {Derived} */ (dep).deps);
}
}
/**
* @param {Effect} effect
* @param {Set<Effect>} dirty_effects
* @param {Set<Effect>} maybe_dirty_effects
*/
export function defer_effect(effect, dirty_effects, maybe_dirty_effects) {
if ((effect.f & DIRTY) !== 0) {
dirty_effects.add(effect);
} else if ((effect.f & MAYBE_DIRTY) !== 0) {
maybe_dirty_effects.add(effect);
}
// Since we're not executing these effects now, we need to clear any WAS_MARKED flags
// so that other batches can correctly reach these effects during their own traversal
clear_marked(effect.deps);
// mark as clean so they get scheduled if they depend on pending async state
set_signal_status(effect, CLEAN);
}

@ -63,7 +63,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -73,7 +73,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -21,7 +21,7 @@
},
"end": {
"line": 9,
"column": 0
"column": 9
}
},
"body": [

@ -190,7 +190,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -258,7 +258,7 @@
},
"end": {
"line": 2,
"column": 0
"column": 9
}
},
"body": [],

@ -298,7 +298,7 @@
},
"end": {
"line": 31,
"column": 0
"column": 9
}
},
"body": [

@ -73,7 +73,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -73,7 +73,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -37,7 +37,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [],

@ -24,12 +24,12 @@
"end": 77,
"loc": {
"start": {
"line": 1,
"line": 5,
"column": 0
},
"end": {
"line": 7,
"column": 0
"column": 9
}
},
"body": [
@ -84,7 +84,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -73,7 +73,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -72,8 +72,8 @@
"column": 0
},
"end": {
"line": 3,
"column": 0
"line": 8,
"column": 1
}
},
"body": [

@ -272,7 +272,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -34,12 +34,12 @@
"end": 67,
"loc": {
"start": {
"line": 1,
"line": 2,
"column": 0
},
"end": {
"line": 4,
"column": 0
"column": 9
}
},
"body": [

@ -296,7 +296,7 @@
},
"end": {
"line": 2,
"column": 0
"column": 9
}
},
"body": [],

@ -227,7 +227,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -111,12 +111,12 @@
"end": 93,
"loc": {
"start": {
"line": 1,
"line": 3,
"column": 0
},
"end": {
"line": 4,
"column": 0
"column": 9
}
},
"body": [],
@ -182,12 +182,12 @@
"end": 160,
"loc": {
"start": {
"line": 1,
"line": 6,
"column": 0
},
"end": {
"line": 7,
"column": 0
"column": 9
}
},
"body": [],

@ -93,7 +93,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [],

@ -230,7 +230,7 @@
},
"end": {
"line": 1,
"column": 18
"column": 27
}
},
"body": [],

@ -396,7 +396,7 @@
},
"end": {
"line": 3,
"column": 0
"column": 9
}
},
"body": [

@ -0,0 +1,15 @@
<script>
let { x, y, deferred } = $props();
y = await deferred.promise;
</script>
<p>x: {x}</p>
<svelte:boundary>
{#snippet pending()}
<p>Loading...</p>
{/snippet}
<p>y: {y}</p>
</svelte:boundary>

@ -0,0 +1,32 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<button>x</button>
<button>y</button>
<p>loading...</p>
`,
async test({ assert, target }) {
await tick();
const [button1, button2] = target.querySelectorAll('button');
button1.click();
await tick();
button2.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y</button>
<p>x: x2</p>
<p>y: y2</p>
`
);
}
});

@ -0,0 +1,19 @@
<script>
import Child from './Child.svelte';
let x = $state('x1');
let y = $state('y1');
const deferred = Promise.withResolvers();
</script>
<button onclick={() => x = 'x2'}>x</button>
<button onclick={() => deferred.resolve('y2')}>y</button>
<svelte:boundary>
<Child {x} {y} {deferred} />
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save