@@ -18,11 +18,17 @@ import type ESTree from "estree";
18
18
import type { SvelteAttribute , SvelteHTMLElement } from "../../../ast" ;
19
19
import { globals , globalsForRunes } from "../../../parser/globals" ;
20
20
import type { NormalizedParserOptions } from "../../parser-options" ;
21
+ import { setParent } from "../set-parent" ;
21
22
22
23
export type AnalyzeTypeScriptContext = {
23
24
slots : Set < SvelteHTMLElement > ;
24
25
} ;
25
26
27
+ type TransformInfo = {
28
+ node : TSESTree . Node ;
29
+ transform : ( ctx : VirtualTypeScriptContext ) => void ;
30
+ } ;
31
+
26
32
/**
27
33
* Analyze TypeScript source code in <script>.
28
34
* Generate virtual code to provide correct type information for Svelte store reference names, scopes, and runes.
@@ -55,7 +61,10 @@ export function analyzeTypeScriptInSvelte(
55
61
56
62
analyzeRuneVariables ( result , ctx ) ;
57
63
58
- analyzeReactiveScopes ( result , ctx ) ;
64
+ applyTransforms (
65
+ [ ...analyzeReactiveScopes ( result ) , ...analyzeDollarDerivedScopes ( result ) ] ,
66
+ ctx ,
67
+ ) ;
59
68
60
69
analyzeRenderScopes ( code , ctx ) ;
61
70
@@ -84,6 +93,8 @@ export function analyzeTypeScript(
84
93
85
94
analyzeRuneVariables ( result , ctx ) ;
86
95
96
+ applyTransforms ( [ ...analyzeDollarDerivedScopes ( result ) ] , ctx ) ;
97
+
87
98
ctx . appendOriginalToEnd ( ) ;
88
99
89
100
return ctx ;
@@ -390,10 +401,9 @@ function analyzeRuneVariables(
390
401
* Analyze the reactive scopes.
391
402
* Transform source code to provide the correct type information in the `$:` statements.
392
403
*/
393
- function analyzeReactiveScopes (
404
+ function * analyzeReactiveScopes (
394
405
result : TSESParseForESLintResult ,
395
- ctx : VirtualTypeScriptContext ,
396
- ) {
406
+ ) : Iterable < TransformInfo > {
397
407
const scopeManager = result . scopeManager ;
398
408
const throughIds = scopeManager . globalScope ! . through . map (
399
409
( reference ) => reference . identifier ,
@@ -417,17 +427,57 @@ function analyzeReactiveScopes(
417
427
left . range [ 0 ] <= id . range [ 0 ] && id . range [ 1 ] <= left . range [ 1 ] ,
418
428
)
419
429
) {
420
- transformForDeclareReactiveVar (
421
- statement ,
422
- statement . body . expression . left ,
423
- statement . body . expression ,
424
- result . ast . tokens ! ,
425
- ctx ,
426
- ) ;
430
+ const node = statement ;
431
+ const expression = statement . body . expression ;
432
+ yield {
433
+ node,
434
+ transform : ( ctx ) =>
435
+ transformForDeclareReactiveVar (
436
+ node ,
437
+ left ,
438
+ expression ,
439
+ result . ast . tokens ! ,
440
+ ctx ,
441
+ ) ,
442
+ } ;
427
443
continue ;
428
444
}
429
445
}
430
- transformForReactiveStatement ( statement , ctx ) ;
446
+ yield {
447
+ node : statement ,
448
+ transform : ( ctx ) => transformForReactiveStatement ( statement , ctx ) ,
449
+ } ;
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Analyze the $derived scopes.
456
+ * Transform source code to provide the correct type information in the `$derived(...)` expression.
457
+ */
458
+ function * analyzeDollarDerivedScopes (
459
+ result : TSESParseForESLintResult ,
460
+ ) : Iterable < TransformInfo > {
461
+ const scopeManager = result . scopeManager ;
462
+ const derivedReferences = scopeManager . globalScope ! . through . filter (
463
+ ( reference ) => reference . identifier . name === "$derived" ,
464
+ ) ;
465
+ if ( ! derivedReferences . length ) {
466
+ return ;
467
+ }
468
+ setParent ( result ) ;
469
+ for ( const ref of derivedReferences ) {
470
+ const derived = ref . identifier ;
471
+ if (
472
+ derived . parent . type === "CallExpression" &&
473
+ derived . parent . callee === derived &&
474
+ derived . parent . arguments [ 0 ] ?. type !== "SpreadElement"
475
+ ) {
476
+ const node = derived . parent ;
477
+ yield {
478
+ node,
479
+ transform : ( ctx ) => transformForDollarDerived ( node , ctx ) ,
480
+ } ;
431
481
}
432
482
}
433
483
}
@@ -464,6 +514,26 @@ function analyzeRenderScopes(
464
514
} ) ;
465
515
}
466
516
517
+ /**
518
+ * Applies the given transforms.
519
+ * Note that intersecting transformations are not applied.
520
+ */
521
+ function applyTransforms (
522
+ transforms : TransformInfo [ ] ,
523
+ ctx : VirtualTypeScriptContext ,
524
+ ) {
525
+ transforms . sort ( ( a , b ) => a . node . range [ 0 ] - b . node . range [ 0 ] ) ;
526
+
527
+ let offset = 0 ;
528
+ for ( const transform of transforms ) {
529
+ const range = transform . node . range ;
530
+ if ( offset <= range [ 0 ] ) {
531
+ transform . transform ( ctx ) ;
532
+ }
533
+ offset = range [ 1 ] ;
534
+ }
535
+ }
536
+
467
537
/**
468
538
* Transform for `$: id = ...` to `$: let id = ...`
469
539
*/
@@ -720,6 +790,76 @@ function transformForReactiveStatement(
720
790
} ) ;
721
791
}
722
792
793
+ /**
794
+ * Transform for `$derived(expr)` to `$derived((()=>{ return fn(); function fn () { return expr } })())`
795
+ */
796
+ function transformForDollarDerived (
797
+ derivedCall : TSESTree . CallExpression ,
798
+ ctx : VirtualTypeScriptContext ,
799
+ ) {
800
+ const functionId = ctx . generateUniqueId ( "$derivedArgument" ) ;
801
+ const expression = derivedCall . arguments [ 0 ] ;
802
+ ctx . appendOriginal ( expression . range [ 0 ] ) ;
803
+ ctx . appendVirtualScript (
804
+ `(()=>{return ${ functionId } ();function ${ functionId } (){return ` ,
805
+ ) ;
806
+ ctx . appendOriginal ( expression . range [ 1 ] ) ;
807
+ ctx . appendVirtualScript ( `}})()` ) ;
808
+
809
+ ctx . restoreContext . addRestoreExpressionProcess < TSESTree . CallExpression > ( {
810
+ target : "CallExpression" as TSESTree . AST_NODE_TYPES . CallExpression ,
811
+ restore :
812
+ // eslint-disable-next-line complexity -- ignore
813
+ ( node , result ) => {
814
+ if (
815
+ node . callee . type !== "Identifier" ||
816
+ node . callee . name !== "$derived"
817
+ ) {
818
+ return false ;
819
+ }
820
+ const arg = node . arguments [ 0 ] ;
821
+ if (
822
+ ! arg ||
823
+ arg . type !== "CallExpression" ||
824
+ arg . arguments . length !== 0 ||
825
+ arg . callee . type !== "ArrowFunctionExpression" ||
826
+ arg . callee . body . type !== "BlockStatement" ||
827
+ arg . callee . body . body . length !== 2 ||
828
+ arg . callee . body . body [ 0 ] . type !== "ReturnStatement" ||
829
+ arg . callee . body . body [ 0 ] . argument ?. type !== "CallExpression" ||
830
+ arg . callee . body . body [ 0 ] . argument . callee . type !== "Identifier" ||
831
+ arg . callee . body . body [ 0 ] . argument . callee . name !== functionId ||
832
+ arg . callee . body . body [ 1 ] . type !== "FunctionDeclaration" ||
833
+ arg . callee . body . body [ 1 ] . id . name !== functionId
834
+ ) {
835
+ return false ;
836
+ }
837
+ const fnNode = arg . callee . body . body [ 1 ] ;
838
+ if (
839
+ fnNode . body . body . length !== 1 ||
840
+ fnNode . body . body [ 0 ] . type !== "ReturnStatement" ||
841
+ ! fnNode . body . body [ 0 ] . argument
842
+ ) {
843
+ return false ;
844
+ }
845
+
846
+ const expr = fnNode . body . body [ 0 ] . argument ;
847
+
848
+ node . arguments [ 0 ] = expr ;
849
+ expr . parent = node ;
850
+
851
+ const scopeManager = result . scopeManager as ScopeManager ;
852
+ removeFunctionScope ( arg . callee . body . body [ 1 ] , scopeManager ) ;
853
+ removeIdentifierReference (
854
+ arg . callee . body . body [ 0 ] . argument . callee ,
855
+ scopeManager . acquire ( arg . callee ) ! ,
856
+ ) ;
857
+ removeFunctionScope ( arg . callee , scopeManager ) ;
858
+ return true ;
859
+ } ,
860
+ } ) ;
861
+ }
862
+
723
863
/** Remove function scope and marge child scopes to upper scope */
724
864
function removeFunctionScope (
725
865
node :
0 commit comments