@@ -546,82 +546,112 @@ const javascript_visitors = {
546
546
547
547
/** @type {import('./types').Visitors } */
548
548
const javascript_visitors_runes = {
549
- ClassBody ( node , { state, visit, next } ) {
550
- if ( ! state . analysis . runes ) {
551
- next ( ) ;
552
- }
553
- /** @type {import('estree').PropertyDefinition[] } */
554
- const deriveds = [ ] ;
555
- /** @type {import('estree').MethodDefinition | null } */
556
- let constructor = null ;
557
- // Get the constructor
549
+ ClassBody ( node , { state, visit } ) {
550
+ /** @type {Map<string, import('../../3-transform/client/types.js').StateField> } */
551
+ const public_derived = new Map ( ) ;
552
+
553
+ /** @type {Map<string, import('../../3-transform/client/types.js').StateField> } */
554
+ const private_derived = new Map ( ) ;
555
+
556
+ /** @type {string[] } */
557
+ const private_ids = [ ] ;
558
+
558
559
for ( const definition of node . body ) {
559
- if ( definition . type === 'MethodDefinition' && definition . kind === 'constructor' ) {
560
- constructor = /** @type {import('estree').MethodDefinition } */ ( visit ( definition ) ) ;
561
- }
562
- }
563
- // Move $derived() runes to the end of the body if there is a constructor
564
- if ( constructor !== null ) {
565
- const body = [ ] ;
566
- for ( const definition of node . body ) {
567
- if (
568
- definition . type === 'PropertyDefinition' &&
569
- ( definition . key . type === 'Identifier' || definition . key . type === 'PrivateIdentifier' )
570
- ) {
571
- const is_private = definition . key . type === 'PrivateIdentifier' ;
572
-
573
- if ( definition . value ?. type === 'CallExpression' ) {
574
- const rune = get_rune ( definition . value , state . scope ) ;
575
-
576
- if ( rune === '$derived' ) {
577
- deriveds . push ( /** @type {import('estree').PropertyDefinition } */ ( visit ( definition ) ) ) ;
578
- if ( is_private ) {
579
- // Keep the private #name initializer if private, but remove initial value
580
- body . push ( {
581
- ...definition ,
582
- value : null
583
- } ) ;
584
- }
585
- continue ;
560
+ if (
561
+ definition . type === 'PropertyDefinition' &&
562
+ ( definition . key . type === 'Identifier' || definition . key . type === 'PrivateIdentifier' )
563
+ ) {
564
+ const { type, name } = definition . key ;
565
+
566
+ const is_private = type === 'PrivateIdentifier' ;
567
+ if ( is_private ) private_ids . push ( name ) ;
568
+
569
+ if ( definition . value ?. type === 'CallExpression' ) {
570
+ const rune = get_rune ( definition . value , state . scope ) ;
571
+ if ( rune === '$derived' || rune === '$derived.by' ) {
572
+ /** @type {import('../../3-transform/client/types.js').StateField } */
573
+ const field = {
574
+ kind : rune === '$derived.by' ? 'derived_call' : 'derived' ,
575
+ // @ts -expect-error this is set in the next pass
576
+ id : is_private ? definition . key : null
577
+ } ;
578
+
579
+ if ( is_private ) {
580
+ private_derived . set ( name , field ) ;
581
+ } else {
582
+ public_derived . set ( name , field ) ;
586
583
}
587
584
}
588
585
}
589
- if ( definition . type !== 'MethodDefinition' || definition . kind !== 'constructor' ) {
590
- body . push (
591
- /** @type {import('estree').PropertyDefinition | import('estree').MethodDefinition | import('estree').StaticBlock } */ (
592
- visit ( definition )
593
- )
594
- ) ;
595
- }
596
586
}
597
- if ( deriveds . length > 0 ) {
598
- body . push ( {
599
- ...constructor ,
600
- value : {
601
- ...constructor . value ,
602
- body : b . block ( [
603
- ...constructor . value . body . body ,
604
- ...deriveds . map ( ( d ) => {
605
- return b . stmt (
606
- b . assignment (
607
- '=' ,
608
- b . member ( b . this , d . key ) ,
609
- /** @type {import('estree').Expression } */ ( d . value )
610
- )
611
- ) ;
612
- } )
613
- ] )
587
+ }
588
+
589
+ // each `foo = $derived()` needs a backing `#foo` field
590
+ for ( const [ name , field ] of public_derived ) {
591
+ let deconflicted = name ;
592
+ while ( private_ids . includes ( deconflicted ) ) {
593
+ deconflicted = '_' + deconflicted ;
594
+ }
595
+
596
+ private_ids . push ( deconflicted ) ;
597
+ field . id = b . private_id ( deconflicted ) ;
598
+ }
599
+
600
+ /** @type {Array<import('estree').MethodDefinition | import('estree').PropertyDefinition> } */
601
+ const body = [ ] ;
602
+
603
+ const child_state = { ...state , private_derived } ;
604
+
605
+ // Replace parts of the class body
606
+ for ( const definition of node . body ) {
607
+ if (
608
+ definition . type === 'PropertyDefinition' &&
609
+ ( definition . key . type === 'Identifier' || definition . key . type === 'PrivateIdentifier' )
610
+ ) {
611
+ const name = definition . key . name ;
612
+
613
+ const is_private = definition . key . type === 'PrivateIdentifier' ;
614
+ const field = ( is_private ? private_derived : public_derived ) . get ( name ) ;
615
+
616
+ if ( definition . value ?. type === 'CallExpression' && field !== undefined ) {
617
+ const init = /** @type {import('estree').Expression } **/ (
618
+ visit ( definition . value . arguments [ 0 ] , child_state )
619
+ ) ;
620
+ const value =
621
+ field . kind === 'derived_call'
622
+ ? b . call ( '$.once' , init )
623
+ : b . call ( '$.once' , b . thunk ( init ) ) ;
624
+
625
+ if ( is_private ) {
626
+ body . push ( b . prop_def ( field . id , value ) ) ;
627
+ } else {
628
+ // #foo;
629
+ const member = b . member ( b . this , field . id ) ;
630
+ body . push ( b . prop_def ( field . id , value ) ) ;
631
+
632
+ // get foo() { return this.#foo; }
633
+ body . push ( b . method ( 'get' , definition . key , [ ] , [ b . return ( b . call ( member ) ) ] ) ) ;
634
+
635
+ if ( ( field . kind === 'derived' || field . kind === 'derived_call' ) && state . options . dev ) {
636
+ body . push (
637
+ b . method (
638
+ 'set' ,
639
+ definition . key ,
640
+ [ b . id ( '_' ) ] ,
641
+ [ b . throw_error ( `Cannot update a derived property ('${ name } ')` ) ]
642
+ )
643
+ ) ;
644
+ }
614
645
}
615
- } ) ;
616
- } else {
617
- body . push ( constructor ) ;
646
+
647
+ continue ;
648
+ }
618
649
}
619
- return {
620
- ...node ,
621
- body
622
- } ;
650
+
651
+ body . push ( /** @type {import('estree').MethodDefinition } **/ ( visit ( definition , child_state ) ) ) ;
623
652
}
624
- next ( ) ;
653
+
654
+ return { ...node , body } ;
625
655
} ,
626
656
PropertyDefinition ( node , { state, next, visit } ) {
627
657
if ( node . value != null && node . value . type === 'CallExpression' ) {
@@ -730,6 +760,16 @@ const javascript_visitors_runes = {
730
760
return transform_inspect_rune ( node , context ) ;
731
761
}
732
762
763
+ context . next ( ) ;
764
+ } ,
765
+ MemberExpression ( node , context ) {
766
+ if ( node . object . type === 'ThisExpression' && node . property . type === 'PrivateIdentifier' ) {
767
+ const field = context . state . private_derived . get ( node . property . name ) ;
768
+ if ( field ) {
769
+ return b . call ( node ) ;
770
+ }
771
+ }
772
+
733
773
context . next ( ) ;
734
774
}
735
775
} ;
@@ -2089,7 +2129,8 @@ export function server_component(analysis, options) {
2089
2129
metadata : {
2090
2130
namespace : options . namespace
2091
2131
} ,
2092
- preserve_whitespace : options . preserveWhitespace
2132
+ preserve_whitespace : options . preserveWhitespace ,
2133
+ private_derived : new Map ( )
2093
2134
} ;
2094
2135
2095
2136
const module = /** @type {import('estree').Program } */ (
@@ -2346,7 +2387,8 @@ export function server_module(analysis, options) {
2346
2387
// this is an anomaly — it can only be used in components, but it needs
2347
2388
// to be present for `javascript_visitors` and so is included in module
2348
2389
// transform state as well as component transform state
2349
- legacy_reactive_statements : new Map ( )
2390
+ legacy_reactive_statements : new Map ( ) ,
2391
+ private_derived : new Map ( )
2350
2392
} ;
2351
2393
2352
2394
const module = /** @type {import('estree').Program } */ (
0 commit comments