pull/16015/head
Rich Harris 4 months ago
parent 5b49610e23
commit 9f61a3bf4f

@ -234,7 +234,22 @@ export function EachBlock(node, context) {
} else if (node.context) { } else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item; const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
const { paths } = extract_paths(node.context, unwrapped); const { inserts, paths } = extract_paths(node.context, unwrapped);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
child_state.transform[id.name] = { read: get_value };
declarations.push(
b.var(
id,
b.call(
'$.derived',
/** @type {Expression} */ (context.visit(b.thunk(value), child_state))
)
)
);
}
for (const path of paths) { for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;

@ -43,7 +43,16 @@ export function SnippetBlock(node, context) {
let arg_alias = `$$arg${i}`; let arg_alias = `$$arg${i}`;
args.push(b.id(arg_alias)); args.push(b.id(arg_alias));
const { paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias))); const { inserts, paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias)));
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
transform[id.name] = { read: get_value };
declarations.push(
b.var(id, b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))))
);
}
for (const path of paths) { for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;

@ -8,6 +8,7 @@ import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { is_hoisted_function } from '../../utils.js'; import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js';
/** /**
* @param {VariableDeclaration} node * @param {VariableDeclaration} node
@ -116,7 +117,7 @@ export function VariableDeclaration(node, context) {
} }
const args = /** @type {CallExpression} */ (init).arguments; const args = /** @type {CallExpression} */ (init).arguments;
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether?
if (rune === '$state' || rune === '$state.raw') { if (rune === '$state' || rune === '$state.raw') {
/** /**
@ -138,15 +139,31 @@ export function VariableDeclaration(node, context) {
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
declarations.push( declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value)) b.declarator(
declarator.id,
create_state_declarator(
declarator.id,
/** @type {Expression} */ (context.visit(value))
)
)
); );
} else { } else {
const tmp = b.id(context.state.scope.generate('tmp')); const tmp = b.id(context.state.scope.generate('tmp'));
const { paths } = extract_paths(declarator.id, tmp); const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push( declarations.push(
b.declarator(tmp, value), b.declarator(tmp, value),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
return b.declarator(
id,
b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value))))
);
}),
...paths.map((path) => { ...paths.map((path) => {
const value = path.expression; const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name); const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator( return b.declarator(
path.node, path.node,
@ -163,12 +180,10 @@ export function VariableDeclaration(node, context) {
if (rune === '$derived' || rune === '$derived.by') { if (rune === '$derived' || rune === '$derived.by') {
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
declarations.push( let expression = /** @type {Expression} */ (context.visit(value));
b.declarator( if (rune === '$derived') expression = b.thunk(expression);
declarator.id,
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)) declarations.push(b.declarator(declarator.id, b.call('$.derived', expression)));
)
);
} else { } else {
const init = /** @type {CallExpression} */ (declarator.init); const init = /** @type {CallExpression} */ (declarator.init);
@ -178,24 +193,32 @@ export function VariableDeclaration(node, context) {
const id = b.id(context.state.scope.generate('$$d')); const id = b.id(context.state.scope.generate('$$d'));
rhs = b.call('$.get', id); rhs = b.call('$.get', id);
declarations.push( let expression = /** @type {Expression} */ (context.visit(value));
b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))) if (rune === '$derived') expression = b.thunk(expression);
);
declarations.push(b.declarator(id, b.call('$.derived', expression)));
} }
const { inserts, paths } = extract_paths(declarator.id, rhs); const { inserts, paths } = extract_paths(declarator.id, rhs);
for (const insert of inserts) { for (const { id, value } of inserts) {
insert.id.name = context.state.scope.generate('$$array'); id.name = context.state.scope.generate('$$array');
declarations.push(b.declarator(insert.id, insert.value)); context.state.transform[id.name] = { read: get_value };
}
for (const path of paths) {
declarations.push( declarations.push(
b.declarator(path.node, b.call('$.derived', b.thunk(path.expression))) b.declarator(
id,
b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value))))
)
); );
} }
for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression));
declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression))));
}
} }
continue; continue;
} }
} }
@ -225,7 +248,7 @@ export function VariableDeclaration(node, context) {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = b.id(context.state.scope.generate('tmp')); const tmp = b.id(context.state.scope.generate('tmp'));
const { paths } = extract_paths(declarator.id, tmp); const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push( declarations.push(
b.declarator( b.declarator(
@ -234,10 +257,23 @@ export function VariableDeclaration(node, context) {
) )
); );
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
declarations.push(
b.declarator(
id,
b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value))))
)
);
}
for (const path of paths) { for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.state.scope.get(name)); const binding = /** @type {Binding} */ (context.state.scope.get(name));
const value = path.expression; const value = /** @type {Expression} */ (context.visit(path.expression));
declarations.push( declarations.push(
b.declarator( b.declarator(
path.node, path.node,
@ -271,7 +307,7 @@ export function VariableDeclaration(node, context) {
declarations.push( declarations.push(
...create_state_declarators( ...create_state_declarators(
declarator, declarator,
context.state, context,
/** @type {Expression} */ (declarator.init && context.visit(declarator.init)) /** @type {Expression} */ (declarator.init && context.visit(declarator.init))
) )
); );
@ -291,30 +327,41 @@ export function VariableDeclaration(node, context) {
/** /**
* Creates the output for a state declaration in legacy mode. * Creates the output for a state declaration in legacy mode.
* @param {VariableDeclarator} declarator * @param {VariableDeclarator} declarator
* @param {ComponentClientTransformState} scope * @param {ComponentContext} context
* @param {Expression} value * @param {Expression} value
*/ */
function create_state_declarators(declarator, { scope, analysis }, value) { function create_state_declarators(declarator, context, value) {
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
return [ return [
b.declarator( b.declarator(
declarator.id, declarator.id,
b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
) )
]; ];
} }
const tmp = b.id(scope.generate('tmp')); const tmp = b.id(context.state.scope.generate('tmp'));
const { paths } = extract_paths(declarator.id, tmp); const { inserts, paths } = extract_paths(declarator.id, tmp);
return [ return [
b.declarator(tmp, value), b.declarator(tmp, value),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
return b.declarator(
id,
b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value))))
);
}),
...paths.map((path) => { ...paths.map((path) => {
const value = path.expression; const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = scope.get(/** @type {Identifier} */ (path.node).name); const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator( return b.declarator(
path.node, path.node,
binding?.kind === 'state' binding?.kind === 'state'
? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) ? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
: value : value
); );
}) })

@ -121,13 +121,20 @@ export function VariableDeclaration(node, context) {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = b.id(context.state.scope.generate('tmp')); const tmp = b.id(context.state.scope.generate('tmp'));
const { paths } = extract_paths(declarator.id, tmp); const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push( declarations.push(
b.declarator( b.declarator(
tmp, tmp,
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init))) /** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
) )
); );
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
declarations.push(b.declarator(id, value));
}
for (const path of paths) { for (const path of paths) {
const value = path.expression; const value = path.expression;
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;
@ -135,6 +142,7 @@ export function VariableDeclaration(node, context) {
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true); const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
declarations.push(b.declarator(path.node, build_fallback(prop, value))); declarations.push(b.declarator(path.node, build_fallback(prop, value)));
} }
continue; continue;
} }

@ -1,8 +1,9 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */ /** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern, Statement } from 'estree' */
/** @import { Context as ClientContext } from '../client/types.js' */ /** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */ /** @import { Context as ServerContext } from '../server/types.js' */
import { extract_paths, is_expression_async } from '../../../utils/ast.js'; import { extract_paths, is_expression_async } from '../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_value } from '../client/visitors/shared/declarations.js';
/** /**
* @template {ClientContext | ServerContext} Context * @template {ClientContext | ServerContext} Context
@ -23,7 +24,11 @@ export function visit_assignment_expression(node, context, build_assignment) {
let changed = false; let changed = false;
const { paths } = extract_paths(node.left, rhs); const { inserts, paths } = extract_paths(node.left, rhs);
for (const { id } of inserts) {
id.name = context.state.scope.generate('$$array');
}
const assignments = paths.map((path) => { const assignments = paths.map((path) => {
const value = path.expression; const value = path.expression;
@ -47,16 +52,20 @@ export function visit_assignment_expression(node, context, build_assignment) {
} }
const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
const sequence = b.sequence(assignments);
if (!is_standalone) { if (inserts.length > 0 || should_cache) {
// this is part of an expression, we need the sequence to end with the value /** @type {Statement[]} */
sequence.expressions.push(rhs); const statements = [
} ...inserts.map(({ id, value }) => b.var(id, value)),
...assignments.map(b.stmt)
];
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
statements.push(b.return(rhs));
}
if (should_cache) { const iife = b.arrow([rhs], b.block(statements));
// the right hand side is a complex expression, wrap in an IIFE to cache it
const iife = b.arrow([rhs], sequence);
const iife_is_async = const iife_is_async =
is_expression_async(value) || is_expression_async(value) ||
@ -65,6 +74,13 @@ export function visit_assignment_expression(node, context, build_assignment) {
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value); return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
} }
const sequence = b.sequence(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
sequence.expressions.push(rhs);
}
return sequence; return sequence;
} }

@ -341,22 +341,19 @@ function _extract_paths(paths, inserts, param, expression, update_expression, ha
// the consumer is responsible for setting the name of the identifier // the consumer is responsible for setting the name of the identifier
const id = b.id('#'); const id = b.id('#');
const is_rest = param.elements.at(-1)?.type === 'RestElement'; const value = b.call(
const call = b.call(
'$.to_array', '$.to_array',
expression, expression,
is_rest ? undefined : b.literal(param.elements.length) param.elements.at(-1)?.type === 'RestElement' ? undefined : b.literal(param.elements.length)
); );
inserts.push({ id, value: b.call('$.derived', b.thunk(call)) }); inserts.push({ id, value });
const array = b.call('$.get', id);
for (let i = 0; i < param.elements.length; i += 1) { for (let i = 0; i < param.elements.length; i += 1) {
const element = param.elements[i]; const element = param.elements[i];
if (element) { if (element) {
if (element.type === 'RestElement') { if (element.type === 'RestElement') {
const rest_expression = b.call(b.member(array, 'slice'), b.literal(i)); const rest_expression = b.call(b.member(id, 'slice'), b.literal(i));
if (element.argument.type === 'Identifier') { if (element.argument.type === 'Identifier') {
paths.push({ paths.push({
@ -377,7 +374,7 @@ function _extract_paths(paths, inserts, param, expression, update_expression, ha
); );
} }
} else { } else {
const array_expression = b.member(array, b.literal(i), true); const array_expression = b.member(id, b.literal(i), true);
_extract_paths( _extract_paths(
paths, paths,

@ -1,34 +0,0 @@
/**
* When encountering a situation like `let [a, b, c] = $derived(blah())`,
* we need to stash an intermediate value that `a`, `b`, and `c` derive
* from, in case it's an iterable
* @template T
* @param {ArrayLike<T> | Iterable<T>} value
* @param {number} [n]
* @returns {Array<T>}
*/
export function to_array(value, n) {
// return arrays unchanged
if (Array.isArray(value)) {
return value;
}
// if value is not iterable, or `n` is unspecified (indicates a rest
// element, which means we're not concerned about unbounded iterables)
// convert to an array with `Array.from`
if (n === undefined || !(Symbol.iterator in value)) {
return Array.from(value);
}
// otherwise, populate an array with `n` values
/** @type {T[]} */
const array = [];
for (const element of value) {
array.push(element);
if (array.length === n) break;
}
return array;
}

@ -1,7 +1,6 @@
export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop } from './context.js'; export { push, pop } from './context.js';
export { to_array } from './destructuring.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js'; export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js'; export { add_locations } from './dev/elements.js';
@ -155,7 +154,7 @@ export {
} from './dom/operations.js'; } from './dom/operations.js';
export { attr, clsx } from '../shared/attributes.js'; export { attr, clsx } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js'; export { snapshot } from '../shared/clone.js';
export { noop, fallback } from '../shared/utils.js'; export { noop, fallback, to_array } from '../shared/utils.js';
export { export {
invalid_default_snippet, invalid_default_snippet,
validate_dynamic_element_tag, validate_dynamic_element_tag,

@ -504,7 +504,7 @@ export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js'; export { snapshot } from '../shared/clone.js';
export { fallback } from '../shared/utils.js'; export { fallback, to_array } from '../shared/utils.js';
export { export {
invalid_default_snippet, invalid_default_snippet,

@ -81,3 +81,38 @@ export function fallback(value, fallback, lazy = false) {
: /** @type {V} */ (fallback) : /** @type {V} */ (fallback)
: value; : value;
} }
/**
* When encountering a situation like `let [a, b, c] = $derived(blah())`,
* we need to stash an intermediate value that `a`, `b`, and `c` derive
* from, in case it's an iterable
* @template T
* @param {ArrayLike<T> | Iterable<T>} value
* @param {number} [n]
* @returns {Array<T>}
*/
export function to_array(value, n) {
// return arrays unchanged
if (Array.isArray(value)) {
return value;
}
// if value is not iterable, or `n` is unspecified (indicates a rest
// element, which means we're not concerned about unbounded iterables)
// convert to an array with `Array.from`
if (n === undefined || !(Symbol.iterator in value)) {
return Array.from(value);
}
// otherwise, populate an array with `n` values
/** @type {T[]} */
const array = [];
for (const element of value) {
array.push(element);
if (array.length === n) break;
}
return array;
}

@ -7,10 +7,12 @@ let c = 3;
let d = 4; let d = 4;
export function update(array) { export function update(array) {
( ((array) => {
$.set(a, array[0], true), var $$array = $.to_array(array, 2);
$.set(b, array[1], true)
); $.set(a, $$array[0], true);
$.set(b, $$array[1], true);
})(array);
[c, d] = array; [c, d] = array;
} }
Loading…
Cancel
Save