@@ -18,11 +18,17 @@ import type ESTree from "estree";
1818import type { SvelteAttribute , SvelteHTMLElement } from "../../../ast" ;
1919import { globals , globalsForRunes } from "../../../parser/globals" ;
2020import type { NormalizedParserOptions } from "../../parser-options" ;
21+ import { setParent } from "../set-parent" ;
2122
2223export type AnalyzeTypeScriptContext = {
2324 slots : Set < SvelteHTMLElement > ;
2425} ;
2526
27+ type TransformInfo = {
28+ node : TSESTree . Node ;
29+ transform : ( ctx : VirtualTypeScriptContext ) => void ;
30+ } ;
31+
2632/**
2733 * Analyze TypeScript source code in <script>.
2834 * Generate virtual code to provide correct type information for Svelte store reference names, scopes, and runes.
@@ -55,7 +61,10 @@ export function analyzeTypeScriptInSvelte(
5561
5662 analyzeRuneVariables ( result , ctx ) ;
5763
58- analyzeReactiveScopes ( result , ctx ) ;
64+ applyTransforms (
65+ [ ...analyzeReactiveScopes ( result ) , ...analyzeDollarDerivedScopes ( result ) ] ,
66+ ctx ,
67+ ) ;
5968
6069 analyzeRenderScopes ( code , ctx ) ;
6170
@@ -84,6 +93,8 @@ export function analyzeTypeScript(
8493
8594 analyzeRuneVariables ( result , ctx ) ;
8695
96+ applyTransforms ( [ ...analyzeDollarDerivedScopes ( result ) ] , ctx ) ;
97+
8798 ctx . appendOriginalToEnd ( ) ;
8899
89100 return ctx ;
@@ -390,10 +401,9 @@ function analyzeRuneVariables(
390401 * Analyze the reactive scopes.
391402 * Transform source code to provide the correct type information in the `$:` statements.
392403 */
393- function analyzeReactiveScopes (
404+ function * analyzeReactiveScopes (
394405 result : TSESParseForESLintResult ,
395- ctx : VirtualTypeScriptContext ,
396- ) {
406+ ) : Iterable < TransformInfo > {
397407 const scopeManager = result . scopeManager ;
398408 const throughIds = scopeManager . globalScope ! . through . map (
399409 ( reference ) => reference . identifier ,
@@ -417,17 +427,57 @@ function analyzeReactiveScopes(
417427 left . range [ 0 ] <= id . range [ 0 ] && id . range [ 1 ] <= left . range [ 1 ] ,
418428 )
419429 ) {
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+ } ;
427443 continue ;
428444 }
429445 }
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+ } ;
431481 }
432482 }
433483}
@@ -464,6 +514,26 @@ function analyzeRenderScopes(
464514 } ) ;
465515}
466516
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+
467537/**
468538 * Transform for `$: id = ...` to `$: let id = ...`
469539 */
@@ -720,6 +790,76 @@ function transformForReactiveStatement(
720790 } ) ;
721791}
722792
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+
723863/** Remove function scope and marge child scopes to upper scope */
724864function removeFunctionScope (
725865 node :
0 commit comments