pull/14977/merge
Paolo Ricciuti 3 months ago committed by GitHub
commit 3d659b53a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: thunkify deriveds on the server

@ -6,6 +6,7 @@ import type { ComponentAnalysis } from '../../types.js';
export interface ServerTransformState extends TransformState {
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
readonly is_destructured_derived?: boolean;
}
export interface ComponentServerTransformState extends ServerTransformState {

@ -31,7 +31,12 @@ export function CallExpression(node, context) {
if (rune === '$derived' || rune === '$derived.by') {
const fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
return b.call(
'$.derived',
rune === '$derived' ? b.thunk(fn) : fn,
context.state.is_destructured_derived && b.true
);
}
if (rune === '$state.snapshot') {

@ -81,22 +81,21 @@ export function VariableDeclaration(node, context) {
continue;
}
const args = /** @type {CallExpression} */ (init).arguments;
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
if (rune === '$derived.by') {
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value))
);
continue;
}
const value = init
? /** @type {Expression} */ (
context.visit(init, {
...context.state,
is_destructured_derived: declarator.id.type !== 'Identifier'
})
)
: b.void0;
if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(declarator.id, value));
continue;
}
if (rune === '$derived') {
if (rune === '$derived' || rune === '$derived.by') {
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value)
);

@ -163,6 +163,19 @@ export function build_element_attributes(node, context) {
])
);
} else {
/** @type {Expression} */
let expression = attribute.expression;
if (attribute.type === 'BindDirective' && expression.type === 'SequenceExpression') {
const getter = expression.expressions[0];
expression =
getter.type === 'ArrowFunctionExpression' &&
getter.params.length === 0 &&
getter.body.type !== 'BlockStatement'
? getter.body
: b.call(getter);
}
attributes.push(
create_attribute(attribute.name, -1, -1, [
{

@ -239,6 +239,10 @@ export function build_getter(node, state) {
b.literal(node.name),
build_getter(store_id, state)
);
} else if (binding.kind === 'derived') {
// we need a maybe_call because in case of `var`
// the user might use the variable before the initialization
return b.maybe_call(node.name);
}
return node;

@ -3,7 +3,7 @@
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { access_path_on_object, is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
UNINITIALIZED,
@ -18,6 +18,7 @@ import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import { reset_elements } from './dev.js';
import { Payload } from './payload.js';
import { is } from '../client/proxy.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
@ -518,15 +519,55 @@ export { escape_html as escape };
/**
* @template T
* @param {()=>T} fn
* @returns {(new_value?: T) => (T | void)}
* @param {boolean} is_destructured
* @returns {((new_value?: T) => (T | void)) | Record<string, (new_value?: T) => (T | void)>}
*/
export function derived(fn) {
export function derived(fn, is_destructured = false) {
const get_value = once(fn);
/**
* @type {T | undefined}
*/
let updated_value;
if (is_destructured) {
/**
*
* @param {(string | symbol)[]} path
* @returns
*/
function recursive_proxy(path = []) {
return new Proxy(
/** @type {(new_value: any)=>any} */ (
function (new_value) {
if (arguments.length === 0) {
return (
access_path_on_object(/** @type {*} */ (updated_value), path) ??
access_path_on_object(/** @type {*} */ (get_value()), path)
);
}
var last_key = path[path.length - 1];
const to_update = access_path_on_object(
/** @type {*} */ (updated_value),
path.slice(0, -1),
(current, key) => {
current[key] = {};
}
);
/** @type {*} */ (to_update)[last_key] = new_value;
return /** @type {*} */ (to_update)[last_key];
}
),
{
get(_, key) {
return recursive_proxy([...path, key]);
}
}
);
}
updated_value = /** @type {T} */ ({});
return recursive_proxy();
}
return function (new_value) {
if (arguments.length === 0) {
return updated_value ?? get_value();

@ -116,3 +116,25 @@ export function to_array(value, n) {
return array;
}
/**
*
* @param {Record<string|symbol, any>} obj
* @param {(string | symbol)[]} path
* @param {(current: Record<string|symbol, any>, key:string|symbol)=>void} [on_undefined]
* @returns
*/
export function access_path_on_object(obj, path, on_undefined) {
if (obj == null) return undefined;
let current = obj;
for (const key of path) {
if (current == null) return undefined;
if (current[key] == null && on_undefined) {
on_undefined(current, key);
}
current = /** @type {*} */ (current)[key];
}
return current;
}

@ -2,13 +2,13 @@ import * as $ from 'svelte/internal/server';
export default function Await_block_scope($$payload) {
let counter = { count: 0 };
const promise = Promise.resolve(counter);
const promise = $.derived(() => Promise.resolve(counter));
function increment() {
counter.count += 1;
}
$$payload.out += `<button>clicks: ${$.escape(counter.count)}</button> `;
$.await($$payload, promise, () => {}, (counter) => {});
$.await($$payload, promise?.(), () => {}, (counter) => {});
$$payload.out += `<!--]--> ${$.escape(counter.count)}`;
}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
mode: ['server']
});

@ -0,0 +1,54 @@
import * as $ from 'svelte/internal/server';
export default function Server_deriveds($$payload, $$props) {
$.push();
// destructuring stuff on the server needs a bit more code
// so that every identifier is a function
let stuff = { foo: true, bar: [1, 2, { baz: 'baz' }] };
let { foo, bar: [a, b, { baz }] } = $.derived(() => stuff, true);
let stuff2 = [1, 2, 3];
let [d, e, f] = $.derived(() => stuff2, true);
let count = 0;
let double = $.derived(() => count * 2);
let identifier = $.derived(() => count);
let dot_by = $.derived(() => () => count);
class Test {
state = 0;
#der = $.derived(() => this.state * 2);
get der() {
return this.#der();
}
set der($$value) {
return this.#der($$value);
}
#der_by = $.derived(() => this.state);
get der_by() {
return this.#der_by();
}
set der_by($$value) {
return this.#der_by($$value);
}
#identifier = $.derived(() => this.state);
get identifier() {
return this.#identifier();
}
set identifier($$value) {
return this.#identifier($$value);
}
}
const test = new Test();
$$payload.out += `<!---->${$.escape(foo?.())} ${$.escape(a?.())} ${$.escape(b?.())} ${$.escape(baz?.())} ${$.escape(d?.())} ${$.escape(e?.())} ${$.escape(f?.())} 0 0 ${$.escape(dot_by?.())} ${$.escape(test.der)} ${$.escape(test.der_by)} ${$.escape(test.identifier)}`;
$.pop();
}

@ -0,0 +1,25 @@
<script>
// destructuring stuff on the server needs a bit more code
// so that every identifier is a function
let stuff = $state({ foo: true, bar: [1, 2, {baz: 'baz'}] });
let { foo, bar: [a, b, { baz }]} = $derived(stuff);
let stuff2 = $state([1, 2, 3]);
let [d, e, f] = $derived(stuff2);
let count = $state(0);
let double = $derived(count * 2);
let identifier = $derived(count);
let dot_by = $derived(()=>count);
class Test{
state = $state(0);
der = $derived(this.state * 2);
der_by = $derived.by(()=>this.state);
identifier = $derived(this.state);
}
const test = new Test();
</script>
{foo} {a} {b} {baz} {d} {e} {f} {double} {identifier} {dot_by} {test.der} {test.der_by} {test.identifier}

@ -7,11 +7,13 @@ import { VERSION } from 'svelte/compiler';
interface SnapshotTest extends BaseTest {
compileOptions?: Partial<import('#compiler').CompileOptions>;
mode?: ('client' | 'server')[];
}
const { test, run } = suite<SnapshotTest>(async (config, cwd) => {
await compile_directory(cwd, 'client', config.compileOptions);
await compile_directory(cwd, 'server', config.compileOptions);
for (const mode of config?.mode ?? ['server', 'client']) {
await compile_directory(cwd, mode, config.compileOptions);
}
// run `UPDATE_SNAPSHOTS=true pnpm test snapshot` to update snapshot tests
if (process.env.UPDATE_SNAPSHOTS) {

Loading…
Cancel
Save