Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tiny-meals-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: mark `accessors` and `immutable` as deprecated
8 changes: 8 additions & 0 deletions packages/svelte/messages/compile-warnings/legacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ Using <slot> to render parent content is deprecated. Use {@render ...} tags inst
## deprecated_event_handler

Using on:%name% to listen to the %name% event is is deprecated. Use the event attribute on%name% instead.

## deprecated_accessors

The accessors option has been deprecated. It will have no effect in runes mode.

## deprecated_immutable

The immutable option has been deprecated. It will have no effect in runes mode.
11 changes: 8 additions & 3 deletions packages/svelte/scripts/process-messages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ for (const category of fs.readdirSync('messages')) {
for (const file of fs.readdirSync(`messages/${category}`)) {
if (!file.endsWith('.md')) continue;

const markdown = fs.readFileSync(`messages/${category}/${file}`, 'utf-8');
const markdown = fs
.readFileSync(`messages/${category}/${file}`, 'utf-8')
.replace(/\r\n/g, '\n');

for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
const [_, code, text] = match;
Expand All @@ -29,7 +31,9 @@ for (const category of fs.readdirSync('messages')) {
}

function transform(name, dest) {
const source = fs.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8');
const source = fs
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
.replace(/\r\n/g, '\n');

const comments = [];

Expand Down Expand Up @@ -213,7 +217,8 @@ function transform(name, dest) {

fs.writeFileSync(
dest,
`/* This file is generated by scripts/process-messages.js. Do not edit! */\n\n` + module.code,
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
module.code,
'utf-8'
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* This file is generated by scripts/process-messages.js. Do not edit! */
/* This file is generated by scripts/process-messages/index.js. Do not edit! */

/** @typedef {{ start?: number, end?: number }} NodeLike */
// interface is duplicated between here (used internally) and ./interfaces.js
Expand Down
18 changes: 15 additions & 3 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ export function analyze_component(root, source, options) {
inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement
? true
: !!options.accessors ||
: (runes ? false : !!options.accessors) ||
// because $set method needs accessors
!!options.legacy?.componentApi,
reactive_statements: new Map(),
Expand All @@ -395,8 +395,20 @@ export function analyze_component(root, source, options) {
source
};

if (!options.customElement && root.options?.customElement) {
w.missing_custom_element_compile_option(root.options);
if (root.options) {
for (const attribute of root.options.attributes) {
if (attribute.name === 'accessors') {
w.deprecated_accessors(attribute);
}

if (attribute.name === 'customElement' && !options.customElement) {
w.missing_custom_element_compile_option(attribute);
}

if (attribute.name === 'immutable') {
w.deprecated_immutable(attribute);
}
}
}

if (analysis.runes) {
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface CompileOptions extends ModuleCompileOptions {
* If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
*
* @default false
* @deprecated This will have no effect in runes mode
*/
accessors?: boolean;
/**
Expand All @@ -107,6 +108,7 @@ export interface CompileOptions extends ModuleCompileOptions {
* This allows it to be less conservative about checking whether values have changed.
*
* @default false
* @deprecated This will have no effect in runes mode
*/
immutable?: boolean;
/**
Expand Down
7 changes: 5 additions & 2 deletions packages/svelte/src/compiler/validate-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export const validate_component_options =
object({
...common,

accessors: boolean(false),
accessors: deprecate(
'The accessors option has been deprecated. It will have no effect in runes mode.',
boolean(false)
),

css: validator('external', (input) => {
if (input === true || input === false) {
Expand Down Expand Up @@ -73,7 +76,7 @@ export const validate_component_options =
discloseVersion: boolean(true),

immutable: deprecate(
'The immutable option has been deprecated. It has no effect in runes mode.',
'The immutable option has been deprecated. It will have no effect in runes mode.',
boolean(false)
),

Expand Down
18 changes: 17 additions & 1 deletion packages/svelte/src/compiler/warnings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* This file is generated by scripts/process-messages.js. Do not edit! */
/* This file is generated by scripts/process-messages/index.js. Do not edit! */

import { getLocator } from 'locate-character';

Expand Down Expand Up @@ -535,6 +535,22 @@ export function deprecated_event_handler(node, name) {
w(node, "deprecated_event_handler", `Using on:${name} to listen to the ${name} event is is deprecated. Use the event attribute on${name} instead.`);
}

/**
* The accessors option has been deprecated. It will have no effect in runes mode.
* @param {null | NodeLike} node
*/
export function deprecated_accessors(node) {
w(node, "deprecated_accessors", "The accessors option has been deprecated. It will have no effect in runes mode.");
}

/**
* The immutable option has been deprecated. It will have no effect in runes mode.
* @param {null | NodeLike} node
*/
export function deprecated_immutable(node) {
w(node, "deprecated_immutable", "The immutable option has been deprecated. It will have no effect in runes mode.");
}

/**
* Self-closing HTML tags for non-void elements are ambiguous — use <%name% ...></%name%> rather than <%name% ... />
* @param {null | NodeLike} node
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function mount(component, options) {
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* recover?: boolean;
* }} options
* @returns {Exports}
*/
Expand Down
66 changes: 51 additions & 15 deletions packages/svelte/tests/runtime-legacy/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as fs from 'node:fs';
import { setImmediate } from 'node:timers/promises';
import glob from 'tiny-glob/sync.js';
import { createClassComponent } from 'svelte/legacy';
import { flushSync } from 'svelte';
import { proxy } from 'svelte/internal/client';
import { flushSync, hydrate, mount, unmount } from 'svelte';
import { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest';
import { compile_directory } from '../helpers.js';
Expand Down Expand Up @@ -120,7 +121,7 @@ export function runtime_suite(runes: boolean) {
return common_setup(cwd, runes, config);
},
async (config, cwd, variant, common) => {
await run_test_variant(cwd, config, variant, common);
await run_test_variant(cwd, config, variant, common, runes);
}
);
}
Expand All @@ -130,7 +131,7 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run
generate: 'client',
...config.compileOptions,
immutable: config.immutable,
accessors: 'accessors' in config ? config.accessors : true,
accessors: 'accessors' in config ? config.accessors : runes ? undefined : true,
runes
};

Expand All @@ -148,7 +149,8 @@ async function run_test_variant(
cwd: string,
config: RuntimeTest,
variant: 'dom' | 'hydrate' | 'ssr',
compileOptions: CompileOptions
compileOptions: CompileOptions,
runes: boolean
) {
let unintended_error = false;

Expand Down Expand Up @@ -281,15 +283,45 @@ async function run_test_variant(
}
};

const instance = createClassComponent({
component: mod.default,
props: config.props,
target,
immutable: config.immutable,
intro: config.intro,
recover: config.recover ?? false,
hydrate: variant === 'hydrate'
});
let instance: any;
let component: any;

if (runes) {
const props: any = proxy({ ...(config.props || {}) });
const render = variant === 'hydrate' ? hydrate : mount;

instance = render(mod.default, {
target,
props,
intro: config.intro,
recover: config.recover ?? false
});

component = new Proxy(instance, {
get(target, key) {
if (key in target) return target[key];
return props[key];
},
set(target, key, value) {
if (key in target) {
target[key] = value;
} else {
flushSync(() => (props[key] = value));
}
return true;
}
});
} else {
instance = component = createClassComponent({
component: mod.default,
props: config.props,
target,
immutable: config.immutable,
intro: config.intro,
recover: config.recover ?? false,
hydrate: variant === 'hydrate'
});
}

// eslint-disable-next-line no-console
console.error = error;
Expand Down Expand Up @@ -319,7 +351,7 @@ async function run_test_variant(
htmlEqualWithOptions: assert_html_equal_with_options
},
variant,
component: instance,
component,
mod,
target,
snapshot,
Expand All @@ -336,7 +368,11 @@ async function run_test_variant(
assert.fail('Expected a runtime error');
}
} finally {
instance.$destroy();
if (runes) {
unmount(instance);
} else {
instance.$destroy();
}

if (config.warnings) {
assert.deepEqual(warnings, config.warnings);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { test } from '../../test';

export default test({
get props() {
return {
items: [{ src: 'https://ds' }]
};
},

async test({ assert, target, component }) {
assert.equal(target.querySelector('img'), component.items[0].img);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script>
let { items = $bindable([{ src: 'https://ds' }]) } = $props();
let { items } = $props();
</script>

{#each items as item, i}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export default test({
]);

logs.length = 0;

component.n += 1;

assert.deepEqual(logs, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export default test({
]);

logs.length = 0;

component.n += 1;

assert.deepEqual(logs, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,28 @@ export default test({
p2: 0,
p3: 0
},

html: `<p>props: 0 0 0 0 1 1 1 1</p><p>log: nested.fallback_value,fallback_fn`,

async test({ assert, target, component }) {
component.p0 = undefined;
component.p1 = undefined;
component.p2 = undefined;
component.p3 = undefined;
// Nuance: these are already undefined in the props, but we're setting them to undefined again,
// which calls the fallback value again, even if it will result in the same value. There's no way
// to prevent this, and in practise it won't matter - and you shouldn't use accessors in runes mode anyway.

assert.htmlEqual(
target.innerHTML,
`<p>props: 1 1 1 1 1 1 1 1</p><p>log: nested.fallback_value,fallback_fn,nested.fallback_value,fallback_fn`
);

component.p4 = undefined;
component.p5 = undefined;
component.p6 = undefined;
component.p7 = undefined;

assert.htmlEqual(
target.innerHTML,
`<p>props: 1 1 1 1 1 1 1 1</p><p>log: nested.fallback_value,fallback_fn,nested.fallback_value,fallback_fn,nested.fallback_value,fallback_fn`
`<p>props: 1 1 1 1 1 1 1 1</p><p>log: nested.fallback_value,fallback_fn,nested.fallback_value,fallback_fn`
);
}
});
4 changes: 2 additions & 2 deletions packages/svelte/tests/runtime-runes/samples/props/_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ export default test({
default2: undefined
};
},

html: `x 1 2 3 z`,

async test({ assert, target, component }) {
component.foo = 'y';
assert.htmlEqual(target.innerHTML, `y 1 2 3 z`);

// rest props don't generate accessors, so we need to use $set
await component.$set({ bar: 'w' });
component.bar = 'w';
assert.htmlEqual(target.innerHTML, `y 1 2 3 w`);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"message": "The 'customElement' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?",
"start": {
"line": 1,
"column": 0
"column": 16
},
"end": {
"line": 1,
"column": 49
"column": 46
}
}
]
Loading