print
Rich Harris 3 months ago
commit 9f42c0bcbe

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: untrack `$inspect.with` and add check for unsafe mutation

@ -1,5 +1,15 @@
# svelte
## 5.34.8
### Patch Changes
- fix: untrack `$inspect.with` and add check for unsafe mutation ([#16209](https://github.com/sveltejs/svelte/pull/16209))
- fix: use fine grained for template if the component is not explicitly in legacy mode ([#16232](https://github.com/sveltejs/svelte/pull/16232))
- lift unsafe_state_mutation constraints for SvelteSet, SvelteMap, SvelteDate, SvelteURL and SvelteURLSearchParams created inside the derived ([#16221](https://github.com/sveltejs/svelte/pull/16221))
## 5.34.7
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.34.7",
"version": "5.34.8",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -439,6 +439,29 @@ export function analyze_component(root, source, options) {
comments: root.comments,
elements: [],
runes,
// if we are not in runes mode but we have no reserved references ($$props, $$restProps)
// and no `export let` we might be in a wannabe runes component that is using runes in an external
// module...we need to fallback to the runic behavior
maybe_runes:
!runes &&
// if they explicitly disabled runes, use the legacy behavior
options.runes !== false &&
![...module.scope.references.keys()].some((name) =>
['$$props', '$$restProps'].includes(name)
) &&
!instance.ast.body.some(
(node) =>
node.type === 'LabeledStatement' ||
(node.type === 'ExportNamedDeclaration' &&
((node.declaration &&
node.declaration.type === 'VariableDeclaration' &&
node.declaration.kind === 'let') ||
node.specifiers.some(
(specifier) =>
specifier.local.type === 'Identifier' &&
instance.scope.get(specifier.local.name)?.declaration_kind === 'let'
)))
),
tracing: false,
classes: new Map(),
immutable: runes || options.immutable,

@ -370,7 +370,10 @@ export function validate_mutation(node, context, expression) {
export function build_expression(context, expression, metadata, state = context.state) {
const value = /** @type {Expression} */ (context.visit(expression, state));
if (context.state.analysis.runes) {
// Components not explicitly in legacy mode might be expected to be in runes mode (especially since we didn't
// adjust this behavior until recently, which broke people's existing components), so we also bail in this case.
// Kind of an in-between-mode.
if (context.state.analysis.runes || context.state.analysis.maybe_runes) {
return value;
}

@ -53,6 +53,7 @@ export interface ComponentAnalysis extends Analysis {
/** Used for CSS pruning and scoping */
elements: Array<AST.RegularElement | AST.SvelteElement>;
runes: boolean;
maybe_runes: boolean;
tracing: boolean;
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */

@ -1,6 +1,6 @@
/** @import { Source } from '#client' */
import { derived } from '../internal/client/index.js';
import { source, set } from '../internal/client/reactivity/sources.js';
import { set, state } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { active_reaction, get, set_active_reaction } from '../internal/client/runtime.js';
import { DEV } from 'esm-env';
@ -40,7 +40,7 @@ var inited = false;
* ```
*/
export class SvelteDate extends Date {
#time = source(super.getTime());
#time = state(super.getTime());
/** @type {Map<keyof Date, Source<unknown>>} */
#deriveds = new Map();

@ -1,6 +1,6 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { set, source } from '../internal/client/reactivity/sources.js';
import { set, source, state } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
@ -54,8 +54,8 @@ import { increment } from './utils.js';
export class SvelteMap extends Map {
/** @type {Map<K, Source<number>>} */
#sources = new Map();
#version = source(0);
#size = source(0);
#version = state(0);
#size = state(0);
/**
* @param {Iterable<readonly [K, V]> | null | undefined} [value]

@ -1,6 +1,6 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { source, set, state } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
@ -48,8 +48,8 @@ var inited = false;
export class SvelteSet extends Set {
/** @type {Map<T, Source<boolean>>} */
#sources = new Map();
#version = source(0);
#size = source(0);
#version = state(0);
#size = state(0);
/**
* @param {Iterable<T> | null | undefined} [value]

@ -1,5 +1,5 @@
import { DEV } from 'esm-env';
import { source } from '../internal/client/reactivity/sources.js';
import { state } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { get_current_url } from './url.js';
@ -34,7 +34,7 @@ export const REPLACE = Symbol();
* ```
*/
export class SvelteURLSearchParams extends URLSearchParams {
#version = DEV ? tag(source(0), 'SvelteURLSearchParams version') : source(0);
#version = DEV ? tag(state(0), 'SvelteURLSearchParams version') : state(0);
#url = get_current_url();
#updating = false;

@ -1,5 +1,5 @@
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { set, state } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { REPLACE, SvelteURLSearchParams } from './url-search-params.js';
@ -40,14 +40,14 @@ export function get_current_url() {
* ```
*/
export class SvelteURL extends URL {
#protocol = source(super.protocol);
#username = source(super.username);
#password = source(super.password);
#hostname = source(super.hostname);
#port = source(super.port);
#pathname = source(super.pathname);
#hash = source(super.hash);
#search = source(super.search);
#protocol = state(super.protocol);
#username = state(super.username);
#password = state(super.password);
#hostname = state(super.hostname);
#port = state(super.port);
#pathname = state(super.pathname);
#hash = state(super.hash);
#search = state(super.search);
#searchParams;
/**

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.34.7';
export const VERSION = '5.34.8';
export const PUBLIC_VERSION = '5';

@ -158,7 +158,10 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run
...config.compileOptions,
immutable: config.immutable,
accessors: 'accessors' in config ? config.accessors : true,
runes
runes:
config.compileOptions && 'runes' in config.compileOptions
? config.compileOptions.runes
: runes
};
// load_compiled can be used for debugging a test. It means the compiler will not run on the input

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '0');
}
});

@ -0,0 +1,8 @@
<svelte:options runes={false} />
<script>
import { get, set } from "./test.svelte.js";
</script>
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '0');
}
});

@ -0,0 +1,9 @@
<script>
import { get, set } from "./test.svelte.js";
$$props;
</script>
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '0');
}
});

@ -0,0 +1,9 @@
<script>
import { get, set } from "./test.svelte.js";
$$restProps;
</script>
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '1');
}
});

@ -0,0 +1,9 @@
<script>
import { get, set } from "./test.svelte.js";
export const x = 42;
</script>
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '0');
}
});

@ -0,0 +1,9 @@
<script>
import { get, set } from "./test.svelte.js";
$: console.log("");
</script>
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '0');
}
});

@ -0,0 +1,10 @@
<script>
import { get, set } from "./test.svelte.js";
export let x = 42;
</script>
{x}
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '0');
}
});

@ -0,0 +1,12 @@
<script>
import { get, set } from "./test.svelte.js";
let x = 42;
export { x };
</script>
{x}
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
runes: undefined
},
async test({ assert, target }) {
const p = target.querySelector('p');
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.equal(p?.innerHTML, '1');
}
});

@ -0,0 +1,7 @@
<script>
import { get, set } from "./test.svelte.js";
</script>
<p>{get()}</p>
<button onclick={()=>set()}></button>

@ -0,0 +1,9 @@
let count = $state(0);
export function get() {
return count;
}
export function set() {
count++;
}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,25 @@
<script>
let visibleExternal = $state(false);
let external = $state([]);
const throws = $derived.by(() => {
external.push(1);
return external;
});
let visibleInternal = $state(false)
const works = $derived.by(() => {
let internal = $state([]);
internal.push(1);
return internal;
});
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,27 @@
<script>
import { SvelteDate } from 'svelte/reactivity';
let visibleExternal = $state(false);
let external = new SvelteDate();
const throws = $derived.by(() => {
external.setTime(12345);
return external;
})
let visibleInternal = $state(false);
const works = $derived.by(() => {
let internal = new SvelteDate();
internal.setTime(12345);
return internal;
})
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,27 @@
<script>
import { SvelteMap } from 'svelte/reactivity';
let visibleExternal = $state(false);
let external = new SvelteMap();
const throws = $derived.by(() => {
external.set(1, 1);
return external;
});
let visibleInternal = $state(false);
const works = $derived.by(() => {
let internal = new SvelteMap();
internal.set(1, 1);
return internal;
});
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,25 @@
<script>
let visibleExternal = $state(false);
let external = $state({ v: 1 });
const throws = $derived.by(() => {
external.v = 2;
return external;
});
let visibleInternal = $state(false)
const works = $derived.by(() => {
let internal = $state({ v: 1 });
internal.v = 2;
return internal;
});
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,25 @@
<script>
let visibleExternal = $state(false);
let external = $state(1);
const throws = $derived.by(() => {
external = 2;
return external;
});
let visibleInternal = $state(false);
const works = $derived.by(() => {
let internal = $state(1);
internal = 2;
return internal;
});
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,27 @@
<script>
import { SvelteSet } from 'svelte/reactivity';
let visibleExternal = $state(false);
let external = new SvelteSet();
const throws = $derived.by(() => {
external.add(1);
return external;
})
let visibleInternal = $state(false);
const works = $derived.by(() => {
let internal = new SvelteSet();
internal.add(1);
return internal;
})
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,27 @@
<script>
import { SvelteURLSearchParams } from 'svelte/reactivity';
let visibleExternal = $state(false);
let external = new SvelteURLSearchParams();
const throws = $derived.by(() => {
external.append('foo', 'bar')
return external;
})
let visibleInternal = $state(false);
const works = $derived.by(() => {
let internal = new SvelteURLSearchParams();
internal.append('foo', 'bar')
return internal;
})
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
runes: true
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button2?.click();
flushSync();
});
}
});

@ -0,0 +1,27 @@
<script>
import { SvelteURL } from 'svelte/reactivity';
let visibleExternal = $state(false);
let external = new SvelteURL('https://svelte.dev');
const throws = $derived.by(() => {
external.host = 'kit.svelte.dev'
return external;
})
let visibleInternal = $state(false);
const works = $derived.by(() => {
let internal = new SvelteURL('https://svelte.dev');
internal.host = 'kit.svelte.dev'
return internal;
})
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
{/if}

@ -8,11 +8,11 @@ export default function Purity($$anchor) {
var fragment = root();
var p = $.first_child(fragment);
p.textContent = ($.untrack(() => Math.max(0, Math.min(0, 100))));
p.textContent = '0';
var p_1 = $.sibling(p, 2);
p_1.textContent = ($.untrack(() => location.href));
p_1.textContent = location.href;
var node = $.sibling(p_1, 2);

Loading…
Cancel
Save