1
- /** @import { VariableDeclarator, Node, Identifier } from 'estree' */
1
+ /** @import { VariableDeclarator, Node, Identifier, AssignmentExpression, LabeledStatement, ExpressionStatement } from 'estree' */
2
2
/** @import { Visitors } from 'zimmerframe' */
3
3
/** @import { ComponentAnalysis } from '../phases/types.js' */
4
4
/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
@@ -10,7 +10,7 @@ import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
10
10
import { analyze_component } from '../phases/2-analyze/index.js' ;
11
11
import { get_rune } from '../phases/scope.js' ;
12
12
import { reset , reset_warning_filter } from '../state.js' ;
13
- import { extract_identifiers } from '../utils/ast.js' ;
13
+ import { extract_identifiers , extract_all_identifiers_from_expression } from '../utils/ast.js' ;
14
14
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js' ;
15
15
import { validate_component_options } from '../validate-options.js' ;
16
16
import { is_svg , is_void } from '../../utils.js' ;
@@ -90,7 +90,8 @@ export function migrate(source) {
90
90
} ,
91
91
legacy_imports : new Set ( ) ,
92
92
script_insertions : new Set ( ) ,
93
- derived_components : new Map ( )
93
+ derived_components : new Map ( ) ,
94
+ derived_labeled_statements : new Set ( )
94
95
} ;
95
96
96
97
if ( parsed . module ) {
@@ -301,7 +302,8 @@ export function migrate(source) {
301
302
* names: Record<string, string>;
302
303
* legacy_imports: Set<string>;
303
304
* script_insertions: Set<string>;
304
- * derived_components: Map<string, string>
305
+ * derived_components: Map<string, string>,
306
+ * derived_labeled_statements: Set<LabeledStatement>
305
307
* }} State
306
308
*/
307
309
@@ -349,7 +351,7 @@ const instance_script = {
349
351
state . str . remove ( /** @type {number } */ ( node . start ) , /** @type {number } */ ( node . end ) ) ;
350
352
}
351
353
} ,
352
- VariableDeclaration ( node , { state, path } ) {
354
+ VariableDeclaration ( node , { state, path, visit } ) {
353
355
if ( state . scope !== state . analysis . instance . scope ) {
354
356
return ;
355
357
}
@@ -470,10 +472,118 @@ const instance_script = {
470
472
state . str . prependLeft ( start , '$state(' ) ;
471
473
state . str . appendRight ( end , ')' ) ;
472
474
} else {
473
- state . str . prependLeft (
474
- /** @type {number } */ ( declarator . id . typeAnnotation ?. end ?? declarator . id . end ) ,
475
- ' = $state()'
475
+ /**
476
+ * @type {AssignmentExpression | undefined }
477
+ */
478
+ let assignment_in_labeled ;
479
+ /**
480
+ * @type {LabeledStatement | undefined }
481
+ */
482
+ let labeled_statement ;
483
+
484
+ // Analyze declaration bindings to see if they're exclusively updated within a single reactive statement
485
+ const possible_derived = bindings . every ( ( binding ) =>
486
+ binding . references . every ( ( reference ) => {
487
+ const declaration = reference . path . find ( ( el ) => el . type === 'VariableDeclaration' ) ;
488
+ const assignment = reference . path . find ( ( el ) => el . type === 'AssignmentExpression' ) ;
489
+ const update = reference . path . find ( ( el ) => el . type === 'UpdateExpression' ) ;
490
+ const labeled = reference . path . find (
491
+ ( el ) => el . type === 'LabeledStatement' && el . label . name === '$'
492
+ ) ;
493
+
494
+ if ( assignment && labeled ) {
495
+ if ( assignment_in_labeled ) return false ;
496
+ assignment_in_labeled = /** @type {AssignmentExpression } */ ( assignment ) ;
497
+ labeled_statement = /** @type {LabeledStatement } */ ( labeled ) ;
498
+ }
499
+
500
+ return ! update && ( declaration || ( labeled && assignment ) || ( ! labeled && ! assignment ) ) ;
501
+ } )
476
502
) ;
503
+
504
+ const labeled_has_single_assignment =
505
+ labeled_statement ?. body . type === 'BlockStatement' &&
506
+ labeled_statement . body . body . length === 1 ;
507
+
508
+ const is_expression_assignment =
509
+ labeled_statement ?. body . type === 'ExpressionStatement' &&
510
+ labeled_statement . body . expression . type === 'AssignmentExpression' ;
511
+
512
+ let should_be_state = false ;
513
+
514
+ if ( is_expression_assignment ) {
515
+ const body = /**@type {ExpressionStatement }*/ ( labeled_statement ?. body ) ;
516
+ const expression = /**@type {AssignmentExpression }*/ ( body . expression ) ;
517
+ const [ , ids ] = extract_all_identifiers_from_expression ( expression . right ) ;
518
+ if ( ids . length === 0 ) {
519
+ should_be_state = true ;
520
+ state . derived_labeled_statements . add (
521
+ /** @type {LabeledStatement } */ ( labeled_statement )
522
+ ) ;
523
+ }
524
+ }
525
+
526
+ if (
527
+ ! should_be_state &&
528
+ possible_derived &&
529
+ assignment_in_labeled &&
530
+ labeled_statement &&
531
+ ( labeled_has_single_assignment || is_expression_assignment )
532
+ ) {
533
+ // Someone wrote a `$: { ... }` statement which we can turn into a `$derived`
534
+ state . str . appendRight (
535
+ /** @type {number } */ ( declarator . id . typeAnnotation ?. end ?? declarator . id . end ) ,
536
+ ' = $derived('
537
+ ) ;
538
+ visit ( assignment_in_labeled . right ) ;
539
+ state . str . appendRight (
540
+ /** @type {number } */ ( declarator . id . typeAnnotation ?. end ?? declarator . id . end ) ,
541
+ state . str
542
+ . snip (
543
+ /** @type {number } */ ( assignment_in_labeled . right . start ) ,
544
+ /** @type {number } */ ( assignment_in_labeled . right . end )
545
+ )
546
+ . toString ( )
547
+ ) ;
548
+ state . str . remove (
549
+ /** @type {number } */ ( labeled_statement . start ) ,
550
+ /** @type {number } */ ( labeled_statement . end )
551
+ ) ;
552
+ state . str . appendRight (
553
+ /** @type {number } */ ( declarator . id . typeAnnotation ?. end ?? declarator . id . end ) ,
554
+ ')'
555
+ ) ;
556
+ state . derived_labeled_statements . add ( labeled_statement ) ;
557
+ } else {
558
+ state . str . prependLeft (
559
+ /** @type {number } */ ( declarator . id . typeAnnotation ?. end ?? declarator . id . end ) ,
560
+ ' = $state('
561
+ ) ;
562
+ if ( should_be_state ) {
563
+ // someone wrote a `$: foo = ...` statement which we can turn into `let foo = $state(...)`
564
+ state . str . appendRight (
565
+ /** @type {number } */ ( declarator . id . typeAnnotation ?. end ?? declarator . id . end ) ,
566
+ state . str
567
+ . snip (
568
+ /** @type {number } */ (
569
+ /** @type {AssignmentExpression } */ ( assignment_in_labeled ) . right . start
570
+ ) ,
571
+ /** @type {number } */ (
572
+ /** @type {AssignmentExpression } */ ( assignment_in_labeled ) . right . end
573
+ )
574
+ )
575
+ . toString ( )
576
+ ) ;
577
+ state . str . remove (
578
+ /** @type {number } */ ( /** @type {LabeledStatement } */ ( labeled_statement ) . start ) ,
579
+ /** @type {number } */ ( /** @type {LabeledStatement } */ ( labeled_statement ) . end )
580
+ ) ;
581
+ }
582
+ state . str . appendRight (
583
+ /** @type {number } */ ( declarator . id . typeAnnotation ?. end ?? declarator . id . end ) ,
584
+ ')'
585
+ ) ;
586
+ }
477
587
}
478
588
}
479
589
@@ -504,6 +614,7 @@ const instance_script = {
504
614
if ( state . analysis . runes ) return ;
505
615
if ( path . length > 1 ) return ;
506
616
if ( node . label . name !== '$' ) return ;
617
+ if ( state . derived_labeled_statements . has ( node ) ) return ;
507
618
508
619
next ( ) ;
509
620
@@ -512,6 +623,9 @@ const instance_script = {
512
623
node . body . expression . type === 'AssignmentExpression'
513
624
) {
514
625
const ids = extract_identifiers ( node . body . expression . left ) ;
626
+ const [ , expression_ids ] = extract_all_identifiers_from_expression (
627
+ node . body . expression . right
628
+ ) ;
515
629
const bindings = ids . map ( ( id ) => state . scope . get ( id . name ) ) ;
516
630
const reassigned_bindings = bindings . filter ( ( b ) => b ?. reassigned ) ;
517
631
if ( reassigned_bindings . length === 0 && ! bindings . some ( ( b ) => b ?. kind === 'store_sub' ) ) {
@@ -542,14 +656,24 @@ const instance_script = {
542
656
return ;
543
657
} else {
544
658
for ( const binding of reassigned_bindings ) {
545
- if ( binding && ids . includes ( binding . node ) ) {
659
+ if ( binding && ( ids . includes ( binding . node ) || expression_ids . length === 0 ) ) {
660
+ const init =
661
+ binding . kind === 'state'
662
+ ? ' = $state()'
663
+ : expression_ids . length === 0
664
+ ? ` = $state(${ state . str . original . substring ( /** @type {number } */ ( node . body . expression . right . start ) , node . body . expression . right . end ) } )`
665
+ : '' ;
546
666
// implicitly-declared variable which we need to make explicit
547
- state . str . prependRight (
667
+ state . str . prependLeft (
548
668
/** @type {number } */ ( node . start ) ,
549
- `let ${ binding . node . name } ${ binding . kind === 'state' ? ' = $state()' : '' } ;\n${ state . indent } `
669
+ `let ${ binding . node . name } ${ init } ;\n${ state . indent } `
550
670
) ;
551
671
}
552
672
}
673
+ if ( expression_ids . length === 0 && ! bindings . some ( ( b ) => b ?. kind === 'store_sub' ) ) {
674
+ state . str . remove ( /** @type {number } */ ( node . start ) , /** @type {number } */ ( node . end ) ) ;
675
+ return ;
676
+ }
553
677
}
554
678
}
555
679
0 commit comments