diff --git a/.changeset/fuzzy-bags-camp.md b/.changeset/fuzzy-bags-camp.md new file mode 100644 index 0000000000..eafc09cf27 --- /dev/null +++ b/.changeset/fuzzy-bags-camp.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: add reactive Map class to svelte/reactivity diff --git a/.changeset/pre.json b/.changeset/pre.json index 186efbdf21..35159be8ee 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -98,6 +98,7 @@ "friendly-candles-relate", "friendly-lies-camp", "funny-wombats-argue", + "fuzzy-bags-camp", "gentle-dolls-juggle", "gentle-sheep-hug", "gentle-spies-happen", @@ -234,6 +235,7 @@ "selfish-dragons-knock", "selfish-tools-hide", "serious-kids-deliver", + "serious-needles-joke", "serious-socks-cover", "serious-zebras-scream", "seven-deers-jam", @@ -344,6 +346,7 @@ "wild-foxes-wonder", "wise-apples-care", "wise-dancers-hang", + "wise-dodos-tell", "wise-donkeys-marry", "wise-jobs-admire", "wise-radios-exercise", diff --git a/.changeset/serious-needles-joke.md b/.changeset/serious-needles-joke.md new file mode 100644 index 0000000000..1091bcf329 --- /dev/null +++ b/.changeset/serious-needles-joke.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add types for svelte/reactivity diff --git a/.changeset/ten-eels-move.md b/.changeset/ten-eels-move.md new file mode 100644 index 0000000000..dfada3b8b2 --- /dev/null +++ b/.changeset/ten-eels-move.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure capture events don't call delegated events diff --git a/.changeset/wise-dodos-tell.md b/.changeset/wise-dodos-tell.md new file mode 100644 index 0000000000..55e477c9fc --- /dev/null +++ b/.changeset/wise-dodos-tell.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure arguments are supported on all reactive Date methods diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b9ab526514..051d4cd970 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.0.0-next.80 + +### Patch Changes + +- fix: add types for svelte/reactivity ([#10817](https://github.com/sveltejs/svelte/pull/10817)) + +- fix: ensure arguments are supported on all reactive Date methods ([#10813](https://github.com/sveltejs/svelte/pull/10813)) + +## 5.0.0-next.79 + +### Patch Changes + +- feat: add reactive Map class to svelte/reactivity ([#10803](https://github.com/sveltejs/svelte/pull/10803)) + ## 5.0.0-next.78 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c26b6fc988..befcdd03ae 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.78", + "version": "5.0.0-next.80", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index 5c0b042cf6..703180a143 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -29,6 +29,7 @@ await createBundle({ [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, [`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`, + [`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.js`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index bd5a77dffb..330bcc2661 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -17,7 +17,10 @@ export function event(event_name, dom, handler, capture, passive) { * @this {EventTarget} */ function target_handler(/** @type {Event} */ event) { - handle_event_propagation(dom, event); + if (!capture) { + // Only call in the bubble phase, else delegated events would be called before the capturing events + handle_event_propagation(dom, event); + } if (!event.cancelBubble) { return handler.call(this, event); } diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 801cd5c15e..198e16f939 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -48,7 +48,6 @@ export function proxy(value, immutable = true, owners) { const prototype = get_prototype_of(value); - // TODO handle Map and Set as well if (prototype === object_prototype || prototype === array_prototype) { const proxy = new Proxy(value, state_proxy_handler); diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js index eb1e72fcc9..cf43dfe4ab 100644 --- a/packages/svelte/src/reactivity/date.js +++ b/packages/svelte/src/reactivity/date.js @@ -55,29 +55,30 @@ const write = [ 'setYear' ]; +var inited = false; + export class ReactiveDate extends Date { #raw_time = source(super.getTime()); - static #inited = false; // We init as part of the first instance so that we can treeshake this class #init() { - if (!ReactiveDate.#inited) { - ReactiveDate.#inited = true; + if (!inited) { + inited = true; const proto = ReactiveDate.prototype; const date_proto = Date.prototype; for (const method of read) { // @ts-ignore - proto[method] = function () { + proto[method] = function (...args) { get(this.#raw_time); // @ts-ignore - return date_proto[method].call(this); + return date_proto[method].apply(this, args); }; } for (const method of write) { // @ts-ignore - proto[method] = function (/** @type {any} */ ...args) { + proto[method] = function (...args) { // @ts-ignore const v = date_proto[method].apply(this, args); const time = date_proto.getTime.call(this); diff --git a/packages/svelte/src/reactivity/index.js b/packages/svelte/src/reactivity/index.js index 67c4279fce..38c7d91885 100644 --- a/packages/svelte/src/reactivity/index.js +++ b/packages/svelte/src/reactivity/index.js @@ -1,2 +1,3 @@ export { ReactiveDate as Date } from './date.js'; export { ReactiveSet as Set } from './set.js'; +export { ReactiveMap as Map } from './map.js'; diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js new file mode 100644 index 0000000000..f57f52e7b7 --- /dev/null +++ b/packages/svelte/src/reactivity/map.js @@ -0,0 +1,157 @@ +import { DEV } from 'esm-env'; +import { source, set } from '../internal/client/reactivity/sources.js'; +import { get } from '../internal/client/runtime.js'; +import { UNINITIALIZED } from '../internal/client/constants.js'; +import { map } from './utils.js'; + +/** + * @template K + * @template V + */ +export class ReactiveMap extends Map { + /** @type {Map>} */ + #sources = new Map(); + #version = source(0); + #size = source(0); + + /** + * @param {Iterable | null | undefined} [value] + */ + constructor(value) { + super(); + + // If the value is invalid then the native exception will fire here + if (DEV) new Map(value); + + if (value) { + var sources = this.#sources; + + for (var [key, v] of value) { + sources.set(key, source(v)); + super.set(key, v); + } + + this.#size.v = sources.size; + } + } + + #increment_version() { + set(this.#version, this.#version.v + 1); + } + + /** @param {K} key */ + has(key) { + var s = this.#sources.get(key); + + if (s === undefined) { + // We should always track the version in case + // the Set ever gets this value in the future. + get(this.#version); + + return false; + } + + get(s); + return true; + } + + /** + * @param {(value: V, key: K, map: Map) => void} callbackfn + * @param {any} [this_arg] + */ + forEach(callbackfn, this_arg) { + get(this.#version); + + return super.forEach(callbackfn, this_arg); + } + + /** @param {K} key */ + get(key) { + var s = this.#sources.get(key); + + if (s === undefined) { + // We should always track the version in case + // the Set ever gets this value in the future. + get(this.#version); + + return undefined; + } + + return get(s); + } + + /** + * @param {K} key + * @param {V} value + * */ + set(key, value) { + var sources = this.#sources; + var s = sources.get(key); + + if (s === undefined) { + sources.set(key, source(value)); + set(this.#size, sources.size); + this.#increment_version(); + } else { + set(s, value); + } + + return super.set(key, value); + } + + /** @param {K} key */ + delete(key) { + var sources = this.#sources; + var s = sources.get(key); + + if (s !== undefined) { + sources.delete(key); + set(this.#size, sources.size); + set(s, /** @type {V} */ (UNINITIALIZED)); + this.#increment_version(); + } + + return super.delete(key); + } + + clear() { + var sources = this.#sources; + + if (sources.size !== 0) { + set(this.#size, 0); + for (var s of sources.values()) { + set(s, /** @type {V} */ (UNINITIALIZED)); + } + this.#increment_version(); + } + + sources.clear(); + super.clear(); + } + + keys() { + get(this.#version); + return this.#sources.keys(); + } + + values() { + get(this.#version); + return map(this.#sources.values(), get); + } + + entries() { + get(this.#version); + return map( + this.#sources.entries(), + ([key, source]) => /** @type {[K, V]} */ ([key, get(source)]) + ); + } + + [Symbol.iterator]() { + return this.entries(); + } + + get size() { + return get(this.#size); + } +} diff --git a/packages/svelte/src/reactivity/map.test.ts b/packages/svelte/src/reactivity/map.test.ts new file mode 100644 index 0000000000..fbbb11e340 --- /dev/null +++ b/packages/svelte/src/reactivity/map.test.ts @@ -0,0 +1,151 @@ +import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js'; +import { flushSync } from '../main/main-client.js'; +import { ReactiveMap } from './map.js'; +import { assert, test } from 'vitest'; + +test('map.values()', () => { + const map = new ReactiveMap([ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5] + ]); + + const log: any = []; + + const cleanup = user_root_effect(() => { + pre_effect(() => { + log.push(map.size); + }); + + pre_effect(() => { + log.push(map.has(3)); + }); + + pre_effect(() => { + log.push(Array.from(map.values())); + }); + }); + + flushSync(() => { + map.delete(3); + }); + + flushSync(() => { + map.clear(); + }); + + assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, [], false]); // TODO update when we fix effect ordering bug + + cleanup(); +}); + +test('map.get(...)', () => { + const map = new ReactiveMap([ + [1, 1], + [2, 2], + [3, 3] + ]); + + const log: any = []; + + const cleanup = user_root_effect(() => { + pre_effect(() => { + log.push('get 1', map.get(1)); + }); + + pre_effect(() => { + log.push('get 2', map.get(2)); + }); + + pre_effect(() => { + log.push('get 3', map.get(3)); + }); + }); + + flushSync(() => { + map.delete(2); + }); + + flushSync(() => { + map.set(2, 2); + }); + + assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', undefined, 'get 2', 2]); + + cleanup(); +}); + +test('map.has(...)', () => { + const map = new ReactiveMap([ + [1, 1], + [2, 2], + [3, 3] + ]); + + const log: any = []; + + const cleanup = user_root_effect(() => { + pre_effect(() => { + log.push('has 1', map.has(1)); + }); + + pre_effect(() => { + log.push('has 2', map.has(2)); + }); + + pre_effect(() => { + log.push('has 3', map.has(3)); + }); + }); + + flushSync(() => { + map.delete(2); + }); + + flushSync(() => { + map.set(2, 2); + }); + + assert.deepEqual(log, [ + 'has 1', + true, + 'has 2', + true, + 'has 3', + true, + 'has 2', + false, + 'has 2', + true + ]); + + cleanup(); +}); + +test('map handling of undefined values', () => { + const map = new ReactiveMap(); + + const log: any = []; + + const cleanup = user_root_effect(() => { + map.set(1, undefined); + + pre_effect(() => { + log.push(map.get(1)); + }); + + flushSync(() => { + map.delete(1); + }); + + flushSync(() => { + map.set(1, 1); + }); + }); + + assert.deepEqual(log, [undefined, undefined, 1]); + + cleanup(); +}); diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 173c0095a0..ca969bea9c 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -1,33 +1,10 @@ import { DEV } from 'esm-env'; import { source, set } from '../internal/client/reactivity/sources.js'; import { get } from '../internal/client/runtime.js'; +import { map } from './utils.js'; -var read = [ - 'difference', - 'forEach', - 'intersection', - 'isDisjointFrom', - 'isSubsetOf', - 'isSupersetOf', - 'symmetricDifference', - 'union' -]; - -/** - * @template T - * @param {IterableIterator} iterator - */ -function make_iterable(iterator) { - iterator[Symbol.iterator] = get_self; - return iterator; -} - -/** - * @this {any} - */ -function get_self() { - return this; -} +var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; +var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; var inited = false; @@ -41,7 +18,7 @@ export class ReactiveSet extends Set { #size = source(0); /** - * @param {Iterable | null | undefined} value + * @param {Iterable | null | undefined} [value] */ constructor(value) { super(); @@ -50,9 +27,14 @@ export class ReactiveSet extends Set { if (DEV) new Set(value); if (value) { + var sources = this.#sources; + for (var element of value) { - this.add(element); + sources.set(element, source(true)); + super.add(element); } + + this.#size.v = sources.size; } if (!inited) this.#init(); @@ -65,7 +47,10 @@ export class ReactiveSet extends Set { var proto = ReactiveSet.prototype; var set_proto = Set.prototype; - for (var method of read) { + /** @type {string} */ + var method; + + for (method of read_methods) { // @ts-ignore proto[method] = function (...v) { get(this.#version); @@ -73,6 +58,17 @@ export class ReactiveSet extends Set { return set_proto[method].apply(this, v); }; } + + for (method of set_like_methods) { + // @ts-ignore + proto[method] = function (...v) { + get(this.#version); + + // @ts-ignore + var set = /** @type {Set} */ (set_proto[method].apply(this, v)); + return new ReactiveSet(set); + }; + } } #increment_version() { @@ -81,9 +77,9 @@ export class ReactiveSet extends Set { /** @param {T} value */ has(value) { - var source = this.#sources.get(value); + var s = this.#sources.get(value); - if (source === undefined) { + if (s === undefined) { // We should always track the version in case // the Set ever gets this value in the future. get(this.#version); @@ -91,7 +87,7 @@ export class ReactiveSet extends Set { return false; } - return get(source); + return get(s); } /** @param {T} value */ @@ -110,12 +106,12 @@ export class ReactiveSet extends Set { /** @param {T} value */ delete(value) { var sources = this.#sources; - var source = sources.get(value); + var s = sources.get(value); - if (source !== undefined) { + if (s !== undefined) { sources.delete(value); set(this.#size, sources.size); - set(source, false); + set(s, false); this.#increment_version(); } @@ -127,8 +123,8 @@ export class ReactiveSet extends Set { if (sources.size !== 0) { set(this.#size, 0); - for (var source of sources.values()) { - set(source, false); + for (var s of sources.values()) { + set(s, false); } this.#increment_version(); } @@ -138,45 +134,20 @@ export class ReactiveSet extends Set { } keys() { - return this.values(); + get(this.#version); + return this.#sources.keys(); } values() { - get(this.#version); - - var iterator = this.#sources.keys(); - - return make_iterable( - /** @type {IterableIterator} */ ({ - next() { - for (var value of iterator) { - return { value, done: false }; - } - - return { done: true }; - } - }) - ); + return this.keys(); } entries() { - var iterator = this.values(); - - return make_iterable( - /** @type {IterableIterator<[T, T]>} */ ({ - next() { - for (var value of iterator) { - return { value: [value, value], done: false }; - } - - return { done: true }; - } - }) - ); + return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key])); } [Symbol.iterator]() { - return this.values(); + return this.keys(); } get size() { diff --git a/packages/svelte/src/reactivity/utils.js b/packages/svelte/src/reactivity/utils.js new file mode 100644 index 0000000000..6c5e573858 --- /dev/null +++ b/packages/svelte/src/reactivity/utils.js @@ -0,0 +1,24 @@ +/** + * @template T + * @template U + * @param {Iterable} iterable + * @param {(value: T) => U} fn + * @returns {IterableIterator} + */ +export function map(iterable, fn) { + return { + [Symbol.iterator]: get_this, + next() { + for (const value of iterable) { + return { done: false, value: fn(value) }; + } + + return { done: true, value: undefined }; + } + }; +} + +/** @this {any} */ +function get_this() { + return this; +} diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index be54549ae6..cdd49a7d21 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.78'; +export const VERSION = '5.0.0-next.80'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/_config.js new file mode 100644 index 0000000000..69f1788171 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../test'; +import { log } from './log.js'; + +export default test({ + before_test() { + log.length = 0; + }, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + await Promise.resolve(); + assert.deepEqual(log, ['div onclickcapture', 'button onclick']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/log.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/log.js new file mode 100644 index 0000000000..d3df521f4d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/main.svelte new file mode 100644 index 0000000000..b79cc9008f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-delegation-7/main.svelte @@ -0,0 +1,7 @@ + + +
log.push('div onclickcapture')}> + +
diff --git a/packages/svelte/tests/runtime-runes/samples/reactive-map/_config.js b/packages/svelte/tests/runtime-runes/samples/reactive-map/_config.js new file mode 100644 index 0000000000..45f72a95e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/reactive-map/_config.js @@ -0,0 +1,50 @@ +import { flushSync } from '../../../../src/main/main-client'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const [btn, btn2, btn3] = target.querySelectorAll('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
1:1
` + ); + + flushSync(() => { + btn?.click(); + }); + + flushSync(() => { + btn?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
1:1
2:2
3:3
` + ); + + flushSync(() => { + btn2?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
1:1
2:2
` + ); + + flushSync(() => { + btn3?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/reactive-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/reactive-map/main.svelte new file mode 100644 index 0000000000..703ef4cf99 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/reactive-map/main.svelte @@ -0,0 +1,21 @@ + + + + + + + + +{#each state as [key, value]} +
{key}:{value}
+{/each} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 7718824342..e2f0e9f5f3 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1808,6 +1808,48 @@ declare module 'svelte/motion' { export function tweened(value?: T | undefined, defaults?: TweenedOptions | undefined): Tweened; } +declare module 'svelte/reactivity' { + export class Date extends Date { + + constructor(...values: any[]); + #private; + } + export class Set extends Set { + + constructor(value?: Iterable | null | undefined); + + has(value: T): boolean; + + add(value: T): this; + + delete(value: T): boolean; + keys(): IterableIterator; + values(): IterableIterator; + entries(): IterableIterator<[T, T]>; + [Symbol.iterator](): IterableIterator; + #private; + } + export class Map extends Map { + + constructor(value?: Iterable | null | undefined); + + has(key: K): boolean; + + forEach(callbackfn: (value: V, key: K, map: Map) => void, this_arg?: any): void; + + get(key: K): V | undefined; + + set(key: K, value: V): this; + + delete(key: K): boolean; + keys(): IterableIterator; + values(): IterableIterator; + entries(): IterableIterator<[K, V]>; + [Symbol.iterator](): IterableIterator<[K, V]>; + #private; + } +} + declare module 'svelte/server' { export function render(component: (...args: any[]) => void, options: { props: Record; diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index ce32e57ebd..14c7c7f375 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -93,6 +93,25 @@ This can improve performance with large arrays and objects that you weren't plan > Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead. +### Reactive Map, Set and Date + +Svelte provides reactive `Map`, `Set` and `Date` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. + +```svelte + + +

{map.get('message')}

+``` + ## `$derived` Derived state is declared with the `$derived` rune: diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md index d8d85ba86b..49239beb9d 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md @@ -103,7 +103,17 @@ No. You can do the migration towards runes incrementally when Svelte 5 comes out ### When can I `npm install` the Svelte 5 preview? -We plan to publish a pre-release version with enough time for brave souls to try it out in their apps and give us feedback on what breaks. Watch this space. +Right now! + +```bash +npm install svelte@next +``` + +You can also opt into Svelte 5 when creating a new SvelteKit project: + +```bash +npm create svelte@latest +``` ### What's left to do? @@ -119,7 +129,7 @@ We know that some of you are very keen on certain feature ideas, and we are too. ### I want to help. How do I contribute? -We appreciate your enthusiasm! Right now it's not possible to accept contributions, but once we enter public beta, everything will be available on the Svelte GitHub repository. +We appreciate your enthusiasm! We welcome issues on the [sveltejs/svelte](https://github.com/sveltejs/svelte) repo. Pull requests are a little dicier right now since many things are in flux, so we recommended starting with an issue. ### How can I share feedback or cool examples of what this enables?