pull/16724/merge
7nik 21 hours ago committed by GitHub
commit 91f4f98251
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: emit `each_key_duplicate` error in production

@ -337,10 +337,6 @@ export function EachBlock(node, context) {
const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')];
if (dev && node.metadata.keyed) {
statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
}
if (has_await) {
context.state.init.push(
b.stmt(

@ -42,6 +42,9 @@ import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
import { current_batch } from '../../reactivity/batch.js';
import { each_key_duplicate } from '../../errors.js';
import { validate_each_keys } from '../../validate.js';
import { invoke_error_boundary } from '../../error-handling.js';
/**
* The row of a keyed each block that is currently updating. We track this
@ -201,6 +204,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
was_empty = length === 0;
// skip if #each block isn't keyed
if (DEV && get_key !== index) {
validate_each_keys(array, get_key);
}
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
@ -266,6 +274,8 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (hydrating) {
if (length === 0 && fallback_fn) {
fallback = branch(() => fallback_fn(anchor));
} else if (length > state.items.size) {
each_key_duplicate('', '', '');
}
} else {
if (should_defer_append()) {
@ -363,6 +373,7 @@ function reconcile(
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var count = 0;
var length = array.length;
var items = state.items;
var first = state.first;
@ -451,6 +462,7 @@ function reconcile(
stashed = [];
current = prev.next;
count += 1;
continue;
}
@ -473,6 +485,19 @@ function reconcile(
var start = stashed[0];
var j;
// full key uniqueness check is dev-only,
// key duplicates cause crash only due to `matched` being empty
if (matched.length === 0) {
// reconcile can be called in the batch's callbacks which are
// executed outside of the effect tree, so error are not caught
try {
each_key_duplicate('', '', '');
} catch (error) {
invoke_error_boundary(error, each_effect);
return;
}
}
prev = start.prev;
var a = matched[0];
@ -506,6 +531,7 @@ function reconcile(
link(state, prev, item);
prev = item;
count += 1;
}
continue;
@ -534,6 +560,20 @@ function reconcile(
matched.push(item);
prev = item;
current = item.next;
count += 1;
}
// Full key uniqueness check is dev-only. If keys duplication didn't cause a crash,
// the rendered list will be shorter then the source array
if (count !== length) {
// reconcile can be called in the batch's callbacks which are
// executed outside of the effect tree, so error are not caught
try {
each_key_duplicate('', '', '');
} catch (error) {
invoke_error_boundary(error, each_effect);
return;
}
}
if (current !== null || seen !== undefined) {

@ -1,5 +1,4 @@
import { dev_current_component_function } from './context.js';
import { is_array } from '../shared/utils.js';
import * as e from './errors.js';
import { FILENAME } from '../../constants.js';
import { render_effect } from './reactivity/effects.js';
@ -7,35 +6,27 @@ import * as w from './warnings.js';
import { capture_store_binding } from './reactivity/store.js';
/**
* @param {() => any} collection
* @param {Array<any>} array
* @param {(item: any, index: number) => string} key_fn
* @returns {void}
*/
export function validate_each_keys(collection, key_fn) {
render_effect(() => {
const keys = new Map();
const maybe_array = collection();
const array = is_array(maybe_array)
? maybe_array
: maybe_array == null
? []
: Array.from(maybe_array);
const length = array.length;
for (let i = 0; i < length; i++) {
const key = key_fn(array[i], i);
if (keys.has(key)) {
const a = String(keys.get(key));
const b = String(i);
export function validate_each_keys(array, key_fn) {
const keys = new Map();
const length = array.length;
for (let i = 0; i < length; i++) {
const key = key_fn(array[i], i);
if (keys.has(key)) {
const a = String(keys.get(key));
const b = String(i);
/** @type {string | null} */
let k = String(key);
if (k.startsWith('[object ')) k = null;
/** @type {string | null} */
let k = String(key);
if (k.startsWith('[object ')) k = null;
e.each_key_duplicate(a, b, k);
}
keys.set(key, i);
e.each_key_duplicate(a, b, k);
}
});
keys.set(key, i);
}
}
/**

Loading…
Cancel
Save