diff --git a/.changeset/sixty-zoos-enjoy.md b/.changeset/sixty-zoos-enjoy.md
new file mode 100644
index 0000000000..71f3db3e1f
--- /dev/null
+++ b/.changeset/sixty-zoos-enjoy.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: support `#each` without `as`
diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md
index e4246b6e9a..df0ba4d8f5 100644
--- a/documentation/docs/03-template-syntax/03-each.md
+++ b/documentation/docs/03-template-syntax/03-each.md
@@ -74,6 +74,30 @@ You can freely use destructuring and rest patterns in each blocks.
{/each}
```
+## Each blocks without an item
+
+```svelte
+
+{#each expression}...{/each}
+```
+
+```svelte
+
+{#each expression, index}...{/each}
+```
+
+In case you just want to render something `n` times, you can omit the `as` part ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0W7CMAxFf8XKNAk0WsSeUEaRpn3Guoc0MbQiJFHiMlDVf18SOrZJ48259_jaVgZmxBEZZ28thgCNFV6xBdt1GgPj7wOji0t2EqI-wa_OleGEmpLWiID_6dIaQkMxhm1UdwKpRQhVzWSaVORJNdvWpqbhAYVsYQCNZk8thzWMC_DCHMZk3wPSThNQ088I3mghD9UwSwHwlLE5PMIzVFUFq3G7WUZ2OyUvU3JOuZU332wCXTRmtPy1NgzXZtUFp8WFw9536uWqpbIgPEaDsJBW90cTOHh0KGi2XsBq5-cT6-3nPauxXqHnsHJnCFZ3CvJVkyuCQ0mFF9TZyCQ162WGvteLKfG197Y3iv_pz_fmS68Hxt8iPBPj5HscP8YvCNX7uhYCAAA=)):
+
+```svelte
+
+ {#each { length: 8 }, rank}
+ {#each { length: 8 }, file}
+
+ {/each}
+ {/each}
+
+```
+
## Else blocks
```svelte
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
index 3366c9ec97..317afe6f2f 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
@@ -1,4 +1,4 @@
-/** @import { ArrowFunctionExpression, Expression, Identifier } from 'estree' */
+/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import read_pattern from '../read/context.js';
@@ -142,16 +142,25 @@ function open(parser) {
parser.index = end;
}
}
- parser.eat('as', true);
- parser.require_whitespace();
-
- const context = read_pattern(parser);
-
- parser.allow_whitespace();
+ /** @type {Pattern | null} */
+ let context = null;
let index;
let key;
+ if (parser.eat('as')) {
+ parser.require_whitespace();
+
+ context = read_pattern(parser);
+ } else {
+ // {#each Array.from({ length: 10 }), i} is read as a sequence expression,
+ // which is set back above - we now gotta reset the index as a consequence
+ // to properly read the , i part
+ parser.index = /** @type {number} */ (expression.end);
+ }
+
+ parser.allow_whitespace();
+
if (parser.eat(',')) {
parser.allow_whitespace();
index = parser.read_identifier();
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js
index ac9e75bf8c..bd6c936f99 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js
@@ -16,7 +16,7 @@ export function EachBlock(node, context) {
validate_block_not_empty(node.fallback, context);
const id = node.context;
- if (id.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
+ if (id?.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
// TODO weird that this is necessary
e.state_invalid_placement(node, id.name);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
index 55d7ded247..d34f39f4c7 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
@@ -47,8 +47,8 @@ export function EachBlock(node, context) {
const key_is_item =
node.key?.type === 'Identifier' &&
- node.context.type === 'Identifier' &&
- node.context.name === node.key.name;
+ node.context?.type === 'Identifier' &&
+ node.context?.name === node.key.name;
// if the each block expression references a store subscription, we need
// to use mutable stores internally
@@ -147,7 +147,7 @@ export function EachBlock(node, context) {
// which needs a reference to the index
const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
- const item = node.context.type === 'Identifier' ? node.context : b.id('$$item');
+ const item = node.context?.type === 'Identifier' ? node.context : b.id('$$item');
let uses_index = each_node_meta.contains_group_binding;
let key_uses_index = false;
@@ -185,7 +185,7 @@ export function EachBlock(node, context) {
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
- if (node.context.type === 'Identifier') {
+ if (node.context?.type === 'Identifier') {
const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name));
child_state.transform[node.context.name] = {
@@ -218,7 +218,7 @@ export function EachBlock(node, context) {
};
delete key_state.transform[node.context.name];
- } else {
+ } else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
for (const path of extract_paths(node.context)) {
@@ -260,11 +260,12 @@ export function EachBlock(node, context) {
let key_function = b.id('$.index');
if (node.metadata.keyed) {
+ const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided
const expression = /** @type {Expression} */ (
context.visit(/** @type {Expression} */ (node.key), key_state)
);
- key_function = b.arrow(key_uses_index ? [node.context, index] : [node.context], expression);
+ key_function = b.arrow(key_uses_index ? [pattern, index] : [pattern], expression);
}
if (node.index && each_node_meta.contains_group_binding) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
index 478bb355a7..104f1f2405 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
@@ -21,7 +21,11 @@ export function EachBlock(node, context) {
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
/** @type {Statement[]} */
- const each = [b.let(/** @type {Pattern} */ (node.context), b.member(array_id, index, true))];
+ const each = [];
+
+ if (node.context) {
+ each.push(b.let(node.context, b.member(array_id, index, true)));
+ }
if (index.name !== node.index && node.index != null) {
each.push(b.let(node.index, index));
diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js
index 454bb8c34e..7f22aa7c87 100644
--- a/packages/svelte/src/compiler/phases/scope.js
+++ b/packages/svelte/src/compiler/phases/scope.js
@@ -527,31 +527,33 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const scope = state.scope.child();
scopes.set(node, scope);
- // declarations
- for (const id of extract_identifiers(node.context)) {
- const binding = scope.declare(id, 'each', 'const');
-
- let inside_rest = false;
- let is_rest_id = false;
- walk(node.context, null, {
- Identifier(node) {
- if (inside_rest && node === id) {
- is_rest_id = true;
+ if (node.context) {
+ // declarations
+ for (const id of extract_identifiers(node.context)) {
+ const binding = scope.declare(id, 'each', 'const');
+
+ let inside_rest = false;
+ let is_rest_id = false;
+ walk(node.context, null, {
+ Identifier(node) {
+ if (inside_rest && node === id) {
+ is_rest_id = true;
+ }
+ },
+ RestElement(_, { next }) {
+ const prev = inside_rest;
+ inside_rest = true;
+ next();
+ inside_rest = prev;
}
- },
- RestElement(_, { next }) {
- const prev = inside_rest;
- inside_rest = true;
- next();
- inside_rest = prev;
- }
- });
+ });
- binding.metadata = { inside_rest: is_rest_id };
- }
+ binding.metadata = { inside_rest: is_rest_id };
+ }
- // Visit to pick up references from default initializers
- visit(node.context, { scope });
+ // Visit to pick up references from default initializers
+ visit(node.context, { scope });
+ }
if (node.index) {
const is_keyed =
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index ede4e1693c..1758b98d24 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -401,7 +401,8 @@ export namespace AST {
export interface EachBlock extends BaseNode {
type: 'EachBlock';
expression: Expression;
- context: Pattern;
+ /** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
+ context: Pattern | null;
body: Fragment;
fallback?: Fragment;
index?: string;
diff --git a/packages/svelte/tests/runtime-runes/samples/each-without-as/_config.js b/packages/svelte/tests/runtime-runes/samples/each-without-as/_config.js
new file mode 100644
index 0000000000..29a59cf6dd
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/each-without-as/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `hi
hi
0
1
`
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/each-without-as/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-without-as/main.svelte
new file mode 100644
index 0000000000..975ba2667b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/each-without-as/main.svelte
@@ -0,0 +1,7 @@
+{#each [10, 20]}
+ hi
+{/each}
+
+{#each [10, 20], i}
+ {i}
+{/each}
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index a03b7de570..0814796d27 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1193,7 +1193,8 @@ declare module 'svelte/compiler' {
export interface EachBlock extends BaseNode {
type: 'EachBlock';
expression: Expression;
- context: Pattern;
+ /** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
+ context: Pattern | null;
body: Fragment;
fallback?: Fragment;
index?: string;