@ -687,193 +687,7 @@ export function analyze_component(root, source, options) {
}
}
/ * *
* @ param { ESTree . Node } expression
* @ param { Scope } scope
* @ param { Set < Binding > } touched
* @ param { Set < ESTree . Node > } seen
* /
const touch = ( expression , scope , touched , seen = new Set ( ) ) => {
if ( seen . has ( expression ) ) return ;
seen . add ( expression ) ;
walk (
expression ,
{ scope } ,
{
ImportDeclaration ( node ) { } ,
Identifier ( node , context ) {
const parent = /** @type {ESTree.Node} */ ( context . path . at ( - 1 ) ) ;
if ( is _reference ( node , parent ) ) {
const binding = context . state . scope . get ( node . name ) ;
if ( binding ) {
touched . add ( binding ) ;
for ( const assignment of binding . assignments ) {
touch ( assignment . value , assignment . scope , touched , seen ) ;
}
}
}
}
}
) ;
} ;
/ * *
* @ param { ESTree . Node } node
* @ param { Set < ESTree . Node > } seen
* @ param { Set < Binding > } reads
* @ param { Set < Binding > } writes
* /
const trace _references = ( node , reads , writes , seen = new Set ( ) ) => {
if ( seen . has ( node ) ) return ;
seen . add ( node ) ;
/ * *
* @ param { ESTree . Pattern } node
* @ param { Scope } scope
* /
function update ( node , scope ) {
for ( const pattern of unwrap _pattern ( node ) ) {
const node = object ( pattern ) ;
if ( ! node ) return ;
const binding = scope . get ( node . name ) ;
if ( ! binding ) return ;
writes . add ( binding ) ;
}
}
walk (
node ,
{ scope : instance . scope } ,
{
_ ( node , context ) {
const scope = scopes . get ( node ) ;
if ( scope ) {
context . next ( { scope } ) ;
} else {
context . next ( ) ;
}
} ,
AssignmentExpression ( node , context ) {
update ( node . left , context . state . scope ) ;
} ,
UpdateExpression ( node , context ) {
update (
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ ( node . argument ) ,
context . state . scope
) ;
} ,
CallExpression ( node , context ) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get _rune ( node , context . state . scope ) ;
if ( rune === '$effect' ) return ;
/** @type {Set<Binding>} */
const touched = new Set ( ) ;
touch ( node , context . state . scope , touched ) ;
for ( const b of touched ) {
writes . add ( b ) ;
}
} ,
// don't look inside functions until they are called
ArrowFunctionExpression ( _ , context ) { } ,
FunctionDeclaration ( _ , context ) { } ,
FunctionExpression ( _ , context ) { } ,
Identifier ( node , context ) {
const parent = /** @type {ESTree.Node} */ ( context . path . at ( - 1 ) ) ;
if ( is _reference ( node , parent ) ) {
const binding = context . state . scope . get ( node . name ) ;
if ( binding ) {
reads . add ( binding ) ;
}
}
}
}
) ;
} ;
let awaited = false ;
// TODO this should probably be attached to the scope?
var promises = b . id ( '$$promises' ) ;
/ * *
* @ param { ESTree . Identifier } id
* @ param { ESTree . Expression } blocker
* /
function push _declaration ( id , blocker ) {
analysis . instance _body . declarations . push ( id ) ;
const binding = /** @type {Binding} */ ( instance . scope . get ( id . name ) ) ;
binding . blocker = blocker ;
}
for ( let node of instance . ast . body ) {
if ( node . type === 'ImportDeclaration' ) {
analysis . instance _body . hoisted . push ( node ) ;
continue ;
}
if ( node . type === 'ExportDefaultDeclaration' || node . type === 'ExportAllDeclaration' ) {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue ;
}
if ( node . type === 'ExportNamedDeclaration' ) {
if ( node . declaration ) {
node = node . declaration ;
} else {
continue ;
}
}
const has _await = has _await _expression ( node ) ;
awaited || = has _await ;
if ( awaited && node . type !== 'FunctionDeclaration' ) {
/** @type {Set<Binding>} */
const reads = new Set ( ) ; // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set ( ) ;
trace _references ( node , reads , writes ) ;
const blocker = b . member ( promises , b . literal ( analysis . instance _body . async . length ) , true ) ;
for ( const binding of writes ) {
binding . blocker = blocker ;
}
if ( node . type === 'VariableDeclaration' ) {
for ( const declarator of node . declarations ) {
for ( const id of extract _identifiers ( declarator . id ) ) {
push _declaration ( id , blocker ) ;
}
// one declarator per declaration, makes things simpler
analysis . instance _body . async . push ( {
node : declarator ,
has _await
} ) ;
}
} else if ( node . type === 'ClassDeclaration' ) {
push _declaration ( node . id , blocker ) ;
analysis . instance _body . async . push ( { node , has _await } ) ;
} else {
analysis . instance _body . async . push ( { node , has _await } ) ;
}
} else {
analysis . instance _body . sync . push ( node ) ;
}
}
calculate _blockers ( instance , scopes , analysis ) ;
if ( analysis . runes ) {
const props _refs = module . scope . references . get ( '$$props' ) ;
@ -1118,6 +932,274 @@ export function analyze_component(root, source, options) {
return analysis ;
}
/ * *
* Analyzes the instance ' s top level statements to calculate which bindings need to wait on which
* top level statements . This includes indirect blockers such as functions referencing async top level statements .
*
* @ param { Js } instance
* @ param { Map < AST . SvelteNode , Scope > } scopes
* @ param { ComponentAnalysis } analysis
* @ returns { void }
* /
function calculate _blockers ( instance , scopes , analysis ) {
/ * *
* @ param { ESTree . Node } expression
* @ param { Scope } scope
* @ param { Set < Binding > } touched
* @ param { Set < ESTree . Node > } seen
* /
const touch = ( expression , scope , touched , seen = new Set ( ) ) => {
if ( seen . has ( expression ) ) return ;
seen . add ( expression ) ;
walk (
expression ,
{ scope } ,
{
ImportDeclaration ( node ) { } ,
Identifier ( node , context ) {
const parent = /** @type {ESTree.Node} */ ( context . path . at ( - 1 ) ) ;
if ( is _reference ( node , parent ) ) {
const binding = context . state . scope . get ( node . name ) ;
if ( binding ) {
touched . add ( binding ) ;
for ( const assignment of binding . assignments ) {
touch ( assignment . value , assignment . scope , touched , seen ) ;
}
}
}
}
}
) ;
} ;
/ * *
* @ param { ESTree . Node } node
* @ param { Set < ESTree . Node > } seen
* @ param { Set < Binding > } reads
* @ param { Set < Binding > } writes
* /
const trace _references = ( node , reads , writes , seen = new Set ( ) ) => {
if ( seen . has ( node ) ) return ;
seen . add ( node ) ;
/ * *
* @ param { ESTree . Pattern } node
* @ param { Scope } scope
* /
function update ( node , scope ) {
for ( const pattern of unwrap _pattern ( node ) ) {
const node = object ( pattern ) ;
if ( ! node ) return ;
const binding = scope . get ( node . name ) ;
if ( ! binding ) return ;
writes . add ( binding ) ;
}
}
walk (
node ,
{ scope : instance . scope } ,
{
_ ( node , context ) {
const scope = scopes . get ( node ) ;
if ( scope ) {
context . next ( { scope } ) ;
} else {
context . next ( ) ;
}
} ,
AssignmentExpression ( node , context ) {
update ( node . left , context . state . scope ) ;
} ,
UpdateExpression ( node , context ) {
update (
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ ( node . argument ) ,
context . state . scope
) ;
} ,
CallExpression ( node , context ) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects as they only run once async work has completed
const rune = get _rune ( node , context . state . scope ) ;
if ( rune === '$effect' ) return ;
/** @type {Set<Binding>} */
const touched = new Set ( ) ;
touch ( node , context . state . scope , touched ) ;
for ( const b of touched ) {
writes . add ( b ) ;
}
} ,
// don't look inside functions until they are called
ArrowFunctionExpression ( _ , context ) { } ,
FunctionDeclaration ( _ , context ) { } ,
FunctionExpression ( _ , context ) { } ,
Identifier ( node , context ) {
const parent = /** @type {ESTree.Node} */ ( context . path . at ( - 1 ) ) ;
if ( is _reference ( node , parent ) ) {
const binding = context . state . scope . get ( node . name ) ;
if ( binding ) {
reads . add ( binding ) ;
}
}
}
}
) ;
} ;
let awaited = false ;
// TODO this should probably be attached to the scope?
const promises = b . id ( '$$promises' ) ;
/ * *
* @ param { ESTree . Identifier } id
* @ param { NonNullable < Binding [ 'blocker' ] > } blocker
* /
function push _declaration ( id , blocker ) {
analysis . instance _body . declarations . push ( id ) ;
const binding = /** @type {Binding} */ ( instance . scope . get ( id . name ) ) ;
binding . blocker = blocker ;
}
/ * *
* Analysis of blockers for functions is deferred until we know which statements are async / blockers
* @ type { Array < ESTree . FunctionDeclaration | ESTree . VariableDeclarator > }
* /
const functions = [ ] ;
for ( let node of instance . ast . body ) {
if ( node . type === 'ImportDeclaration' ) {
analysis . instance _body . hoisted . push ( node ) ;
continue ;
}
if ( node . type === 'ExportDefaultDeclaration' || node . type === 'ExportAllDeclaration' ) {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue ;
}
if ( node . type === 'ExportNamedDeclaration' ) {
if ( node . declaration ) {
node = node . declaration ;
} else {
continue ;
}
}
const has _await = has _await _expression ( node ) ;
awaited || = has _await ;
if ( node . type === 'FunctionDeclaration' ) {
analysis . instance _body . sync . push ( node ) ;
functions . push ( node ) ;
} else if ( node . type === 'VariableDeclaration' ) {
for ( const declarator of node . declarations ) {
if (
declarator . init ? . type === 'ArrowFunctionExpression' ||
declarator . init ? . type === 'FunctionExpression'
) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis . instance _body . sync . push (
node . declarations . length === 1 ? node : b . declaration ( node . kind , [ declarator ] )
) ;
functions . push ( declarator ) ;
} else if ( ! awaited ) {
// One declarator per declaration, makes things simpler. The ternary ensures more accurate source maps in the common case
analysis . instance _body . sync . push (
node . declarations . length === 1 ? node : b . declaration ( node . kind , [ declarator ] )
) ;
} else {
/** @type {Set<Binding>} */
const reads = new Set ( ) ; // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set ( ) ;
trace _references ( declarator , reads , writes ) ;
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b . member ( promises , b . literal ( analysis . instance _body . async . length ) , true )
) ;
for ( const binding of writes ) {
binding . blocker = blocker ;
}
for ( const id of extract _identifiers ( declarator . id ) ) {
push _declaration ( id , blocker ) ;
}
// one declarator per declaration, makes things simpler
analysis . instance _body . async . push ( {
node : declarator ,
has _await
} ) ;
}
}
} else if ( awaited ) {
/** @type {Set<Binding>} */
const reads = new Set ( ) ; // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set ( ) ;
trace _references ( node , reads , writes ) ;
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b . member ( promises , b . literal ( analysis . instance _body . async . length ) , true )
) ;
for ( const binding of writes ) {
binding . blocker = blocker ;
}
if ( node . type === 'ClassDeclaration' ) {
push _declaration ( node . id , blocker ) ;
analysis . instance _body . async . push ( { node , has _await } ) ;
} else {
analysis . instance _body . async . push ( { node , has _await } ) ;
}
} else {
analysis . instance _body . sync . push ( node ) ;
}
}
for ( const fn of functions ) {
/** @type {Set<Binding>} */
const reads _writes = new Set ( ) ;
const body =
fn . type === 'VariableDeclarator'
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ ( fn . init ) . body
: fn . body ;
trace _references ( body , reads _writes , reads _writes ) ;
const max = [ ... reads _writes ] . reduce ( ( max , binding ) => {
return binding . blocker ? Math . max ( binding . blocker . property . value , max ) : max ;
} , - 1 ) ;
if ( max === - 1 ) continue ;
const blocker = b . member ( promises , b . literal ( max ) , true ) ;
const binding = /** @type {Binding} */ (
fn . type === 'FunctionDeclaration'
? instance . scope . get ( fn . id . name )
: instance . scope . get ( /** @type {ESTree.Identifier} */ ( fn . id ) . name )
) ;
binding . blocker = /** @type {typeof binding['blocker']} */ ( blocker ) ;
}
}
/ * *
* @ param { Map < import ( 'estree' ) . LabeledStatement , ReactiveStatement > } unsorted _reactive _declarations
* /