@ -5,14 +5,16 @@
/** @import { ExpressionMetadata } from '../../nodes.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders' ;
import { is _ simple_expression , save } from '../../../utils/ast.js' ;
import { is _ event_attribute , is _ simple_expression , save } from '../../../utils/ast.js' ;
import {
PROPS _IS _LAZY _INITIAL ,
PROPS _IS _IMMUTABLE ,
PROPS _IS _RUNES ,
PROPS _IS _UPDATED ,
PROPS _IS _BINDABLE
PROPS _IS _BINDABLE ,
PROPS _IS _RETAINED
} from '../../../../constants.js' ;
import { get _rune } from '../../scope.js' ;
/ * *
* @ param { Binding } binding
@ -44,6 +46,291 @@ export function build_getter(node, state) {
return node ;
}
/ * *
* @ param { Binding } binding
* @ param { ClientTransformState } state
* /
export function prop _binding _may _be _in _teardown ( binding , state ) {
return ( ( binding . metadata ? ? = { } ) . prop _read _may _be _in _teardown ? ? = binding . references . some (
( reference ) => reference _may _be _in _teardown ( reference , state , new Set ( ) )
) ) ;
}
/ * *
* @ param { Binding [ 'references' ] [ number ] } reference
* @ param { ClientTransformState } state
* @ param { Set < Binding > } checked
* /
function reference _may _be _in _teardown ( reference , state , checked ) {
const { path } = reference ;
for ( let i = path . length - 1 ; i >= 0 ; i -= 1 ) {
const fn = path [ i ] ;
if (
fn . type !== 'ArrowFunctionExpression' &&
fn . type !== 'FunctionExpression' &&
fn . type !== 'FunctionDeclaration'
) {
continue ;
}
const parent = path [ i - 1 ] ;
if ( parent ? . type === 'CallExpression' ) {
// An IIFE executes in the same context as the function containing it.
if ( parent . callee === fn ) continue ;
if ( parent . arguments . includes ( /** @type {Expression} */ ( fn ) ) ) {
const scope = get _scope ( path , i - 1 , state ) ;
if ( is _save _svelte _import _call ( parent , scope ) ) return false ;
const rune = get _rune ( parent , scope ) ;
if ( is _effect _rune ( rune ) ) return false ;
if ( rune === '$derived.by' ) {
return derived _may _be _read _in _teardown ( parent , path , i - 1 , state , checked ) ;
}
}
}
if ( is _safe _element _event _handler ( path , i ) ) return false ;
const function _binding = get _function _binding ( fn , path , i , state ) ;
return function _binding === null
? true
: function _may _be _called _in _teardown ( function _binding , state , checked ) ;
}
for ( let i = path . length - 1 ; i >= 0 ; i -= 1 ) {
const node = path [ i ] ;
if ( node . type !== 'CallExpression' ) continue ;
const rune = get _rune ( node , get _scope ( path , i , state ) ) ;
if ( rune === '$derived' || rune === '$derived.by' ) {
return derived _may _be _read _in _teardown ( node , path , i , state , checked ) ;
}
}
return false ;
}
/ * *
* @ param { Binding } binding
* @ param { ClientTransformState } state
* @ param { Set < Binding > } checked
* /
function function _may _be _called _in _teardown ( binding , state , checked ) {
if ( checked . has ( binding ) ) return false ;
checked . add ( binding ) ;
for ( const reference of binding . references ) {
if ( reference . node === binding . node ) continue ;
const parent = reference . path . at ( - 1 ) ;
if ( parent ? . type !== 'CallExpression' ) {
if ( is _safe _element _event _handler ( reference . path , reference . path . length ) ) continue ;
return true ;
}
if ( parent . callee === reference . node ) {
if ( reference _may _be _in _teardown ( reference , state , checked ) ) return true ;
continue ;
}
if ( ! parent . arguments . includes ( reference . node ) ) return true ;
const scope = get _scope ( reference . path , reference . path . length - 1 , state ) ;
if ( is _save _svelte _import _call ( parent , scope ) ) continue ;
const rune = get _rune ( parent , scope ) ;
if ( is _effect _rune ( rune ) ) continue ;
if (
rune === '$derived.by' &&
! derived _may _be _read _in _teardown (
parent ,
reference . path ,
reference . path . length - 1 ,
state ,
checked
)
) {
continue ;
}
return true ;
}
return false ;
}
/ * *
* @ param { import ( 'estree' ) . CallExpression } call
* @ param { import ( '#compiler' ) . AST . SvelteNode [ ] } path
* @ param { number } index
* @ param { ClientTransformState } state
* @ param { Set < Binding > } checked
* /
function derived _may _be _read _in _teardown ( call , path , index , state , checked ) {
const parent = path [ index - 1 ] ;
if ( parent ? . type !== 'VariableDeclarator' || parent . init !== call ) return true ;
if ( parent . id . type !== 'Identifier' ) return true ;
const binding = get _scope ( path , index - 1 , state ) . get ( parent . id . name ) ;
if ( binding === null || checked . has ( binding ) ) return false ;
checked . add ( binding ) ;
for ( const reference of binding . references ) {
if ( reference . node === binding . node ) continue ;
if ( reference _may _be _in _teardown ( reference , state , checked ) ) return true ;
}
return false ;
}
/ * *
* @ param { import ( 'estree' ) . FunctionDeclaration | import ( 'estree' ) . FunctionExpression | import ( 'estree' ) . ArrowFunctionExpression } fn
* @ param { import ( '#compiler' ) . AST . SvelteNode [ ] } path
* @ param { number } index
* @ param { ClientTransformState } state
* @ returns { Binding | null }
* /
function get _function _binding ( fn , path , index , state ) {
if ( fn . type === 'FunctionDeclaration' && fn . id !== null ) {
return get _scope ( path , index - 1 , state ) . get ( fn . id . name ) ;
}
const parent = path [ index - 1 ] ;
if (
parent ? . type === 'VariableDeclarator' &&
parent . init === fn &&
parent . id . type === 'Identifier'
) {
return get _scope ( path , index - 1 , state ) . get ( parent . id . name ) ;
}
return null ;
}
/ * *
* @ param { import ( '#compiler' ) . AST . SvelteNode [ ] } path
* @ param { number } index
* @ param { ClientTransformState } state
* /
function get _scope ( path , index , state ) {
for ( let i = index ; i >= 0 ; i -= 1 ) {
const scope = state . scopes . get ( path [ i ] ) ;
if ( scope !== undefined ) return scope ;
}
return state . scope ;
}
/** @param {string | null} rune */
function is _effect _rune ( rune ) {
return rune === '$effect' || rune === '$effect.pre' || rune === '$effect.root' ;
}
/ * *
* @ param { import ( 'estree' ) . CallExpression } call
* @ param { Scope } scope
* /
function is _save _svelte _import _call ( call , scope ) {
if ( call . callee . type === 'Identifier' ) {
return (
is _svelte _import ( scope . get ( call . callee . name ) , call . callee . name , 'untrack' ) ||
is _svelte _import ( scope . get ( call . callee . name ) , call . callee . name , 'hydratable' )
) ;
}
if (
call . callee . type === 'MemberExpression' &&
call . callee . object . type === 'Identifier' &&
( ( ! call . callee . computed &&
call . callee . property . type === 'Identifier' &&
( call . callee . property . name === 'untrack' || call . callee . property . name === 'hydratable' ) ) ||
( call . callee . computed &&
call . callee . property . type === 'Literal' &&
( call . callee . property . value === 'untrack' || call . callee . property . value === 'hydratable' ) ) )
) {
return is _svelte _import ( scope . get ( call . callee . object . name ) , call . callee . object . name , null ) ;
}
return false ;
}
/ * *
* @ param { Binding | null } binding
* @ param { string } local _name
* @ param { string | null } imported _name
* /
function is _svelte _import ( binding , local _name , imported _name ) {
if ( binding ? . initial ? . type !== 'ImportDeclaration' || binding . initial . source . value !== 'svelte' ) {
return false ;
}
return binding . initial . specifiers . some ( ( specifier ) => {
if ( specifier . local . name !== local _name ) return false ;
if ( imported _name === null ) return specifier . type === 'ImportNamespaceSpecifier' ;
return (
specifier . type === 'ImportSpecifier' &&
specifier . imported . type === 'Identifier' &&
specifier . imported . name === imported _name
) ;
} ) ;
}
/ * *
* @ param { import ( '#compiler' ) . AST . SvelteNode [ ] } path
* @ param { number } index
* /
function is _safe _element _event _handler ( path , index ) {
for ( let i = index - 1 ; i >= 0 ; i -= 1 ) {
const node = path [ i ] ;
if (
node . type === 'ArrowFunctionExpression' ||
node . type === 'FunctionExpression' ||
node . type === 'FunctionDeclaration' ||
node . type === 'CallExpression' ||
node . type === 'NewExpression' ||
node . type === 'SpreadAttribute'
) {
return false ;
}
if ( node . type === 'Attribute' ) {
if ( ! is _event _attribute ( node ) ) return false ;
const element = path [ i - 1 ] ;
return (
( element ? . type === 'RegularElement' || element ? . type === 'SvelteElement' ) &&
is _safe _event _name ( node . name . slice ( 2 ) )
) ;
}
if ( node . type === 'OnDirective' ) {
const element = path [ i - 1 ] ;
return (
( element ? . type === 'RegularElement' || element ? . type === 'SvelteElement' ) &&
is _safe _event _name ( node . name )
) ;
}
}
return false ;
}
/** @param {string} name */
function is _safe _event _name ( name ) {
if ( name . endsWith ( 'capture' ) ) name = name . slice ( 0 , - 7 ) ;
return name !== 'blur' && name !== 'focusout' ;
}
/ * *
* @ param { Binding } binding
* @ param { ComponentClientTransformState } state
@ -69,6 +356,10 @@ export function get_prop_source(binding, state, name, initial) {
flags |= PROPS _IS _RUNES ;
}
if ( state . analysis . runes && prop _binding _may _be _in _teardown ( binding , state ) ) {
flags |= PROPS _IS _RETAINED ;
}
if (
state . analysis . accessors ||
( state . analysis . immutable
@ -123,7 +414,8 @@ export function is_prop_source(binding, state) {
binding . initial ||
// Until legacy mode is gone, we also need to use the prop source when only mutated is true,
// because the parent could be a legacy component which needs coarse-grained reactivity
binding . updated )
binding . updated ||
prop _binding _may _be _in _teardown ( binding , state ) )
) ;
}