7nik 1 week ago committed by GitHub
commit 1a9649728a
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')]; 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) { if (has_await) {
context.state.init.push( context.state.init.push(
b.stmt( b.stmt(

@ -42,6 +42,9 @@ import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js'; import { derived_safe_equal } from '../../reactivity/deriveds.js';
import { current_batch } from '../../reactivity/batch.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 * 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; 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 */ /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false; let mismatch = false;
@ -266,6 +274,8 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (hydrating) { if (hydrating) {
if (length === 0 && fallback_fn) { if (length === 0 && fallback_fn) {
fallback = branch(() => fallback_fn(anchor)); fallback = branch(() => fallback_fn(anchor));
} else if (length > state.items.size) {
each_key_duplicate('', '', '');
} }
} else { } else {
if (should_defer_append()) { if (should_defer_append()) {
@ -363,6 +373,7 @@ function reconcile(
var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var count = 0;
var length = array.length; var length = array.length;
var items = state.items; var items = state.items;
var first = state.first; var first = state.first;
@ -451,6 +462,7 @@ function reconcile(
stashed = []; stashed = [];
current = prev.next; current = prev.next;
count += 1;
continue; continue;
} }
@ -473,6 +485,19 @@ function reconcile(
var start = stashed[0]; var start = stashed[0];
var j; 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; prev = start.prev;
var a = matched[0]; var a = matched[0];
@ -506,6 +531,7 @@ function reconcile(
link(state, prev, item); link(state, prev, item);
prev = item; prev = item;
count += 1;
} }
continue; continue;
@ -534,6 +560,20 @@ function reconcile(
matched.push(item); matched.push(item);
prev = item; prev = item;
current = item.next; 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) { if (current !== null || seen !== undefined) {

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

Loading…
Cancel
Save