11import { isEmpty , merge , toString } from "lodash-es" ;
2+ import { parse as acornParse } from "acorn" ;
23import type { ScreenContextDefinition } from "../state/screen" ;
34import type { InvokableMethods , WidgetState } from "../state/widget" ;
45import {
@@ -9,6 +10,73 @@ import {
910 replace ,
1011} from "../shared" ;
1112
13+ /**
14+ * Cache of compiled global / imported scripts keyed by the full script string.
15+ * Each entry stores the symbol names and their corresponding values so that we
16+ * can inject them as parameters when evaluating bindings, removing the need to
17+ * re-parse the same script for every binding.
18+ */
19+ interface CachedScriptEntry {
20+ symbols : string [ ] ;
21+ // compiled function that, given a context, returns an object of exports
22+ fn : ( ctx : { [ key : string ] : unknown } ) => { [ key : string ] : unknown } ;
23+ }
24+
25+ const globalScriptCache = new Map < string , CachedScriptEntry > ( ) ;
26+
27+ const parseScriptSymbols = ( script : string ) : string [ ] => {
28+ const symbols = new Set < string > ( ) ;
29+ try {
30+ const ast : any = acornParse ( script , {
31+ ecmaVersion : 2020 ,
32+ sourceType : "script" ,
33+ } ) ;
34+
35+ ast . body ?. forEach ( ( node : any ) => {
36+ if ( node . type === "FunctionDeclaration" && node . id ) {
37+ symbols . add ( node . id . name ) ;
38+ }
39+ if ( node . type === "VariableDeclaration" ) {
40+ node . declarations . forEach ( ( decl : any ) => {
41+ if ( decl . id ?. type === "Identifier" ) {
42+ symbols . add ( decl . id . name ) ;
43+ }
44+ } ) ;
45+ }
46+ } ) ;
47+ } catch ( e ) {
48+ debug ( e ) ;
49+ }
50+ return Array . from ( symbols ) ;
51+ } ;
52+
53+ const getCachedGlobals = (
54+ script : string ,
55+ ctx : { [ key : string ] : unknown } ,
56+ ) : { symbols : string [ ] ; values : unknown [ ] } => {
57+ if ( isEmpty ( script . trim ( ) ) ) return { symbols : [ ] , values : [ ] } ;
58+
59+ let entry = globalScriptCache . get ( script ) ;
60+ if ( ! entry ) {
61+ const symbols = parseScriptSymbols ( script ) ;
62+
63+ // build a function that executes the script within the provided context using `with`
64+ // and returns an object containing the exported symbols
65+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
66+ const compiled = new Function (
67+ "ctx" ,
68+ `with (ctx) {\n${ script } \nreturn { ${ symbols . join ( ", " ) } };\n}` ,
69+ ) as CachedScriptEntry [ "fn" ] ;
70+
71+ entry = { symbols, fn : compiled } ;
72+ globalScriptCache . set ( script , entry ) ;
73+ }
74+
75+ const exportsObj = entry . fn ( ctx ) ;
76+ const values = entry . symbols . map ( ( name ) => exportsObj [ name ] ) ;
77+ return { symbols : entry . symbols , values } ;
78+ } ;
79+
1280export const widgetStatesToInvokables = ( widgets : {
1381 [ key : string ] : WidgetState | undefined ;
1482} ) : [ string , InvokableMethods | undefined ] [ ] => {
@@ -38,17 +106,23 @@ export const buildEvaluateFn = (
38106 // Need to filter out invalid JS identifiers
39107 ] . filter ( ( [ key , _ ] ) => ! key . includes ( "." ) ) ,
40108 ) ;
41- const globalBlock = screen . model ?. global ;
42- const importedScriptBlock = screen . model ?. importedScripts ;
109+ const globalBlock = screen . model ?. global ?? "" ;
110+ const importedScriptBlock = screen . model ?. importedScripts ?? "" ;
111+ const combinedScript = `${ importedScriptBlock } \n${ globalBlock } ` ;
112+
113+ const { symbols : globalSymbols , values : globalValues } = getCachedGlobals (
114+ combinedScript ,
115+ merge ( { } , context , invokableObj ) ,
116+ ) ;
43117
44118 // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
45119 const jsFunc = new Function (
46120 ...Object . keys ( invokableObj ) ,
47- addScriptBlock ( formatJs ( js ) , globalBlock , importedScriptBlock ) ,
121+ ...globalSymbols ,
122+ formatJs ( js ) ,
48123 ) ;
49124
50- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
51- return ( ) => jsFunc ( ...Object . values ( invokableObj ) ) ;
125+ return ( ) => jsFunc ( ...Object . values ( invokableObj ) , ...globalValues ) ;
52126} ;
53127
54128const formatJs = ( js ?: string ) : string => {
@@ -96,24 +170,6 @@ const formatJs = (js?: string): string => {
96170 return `return ${ sanitizedJs } ` ;
97171} ;
98172
99- const addScriptBlock = (
100- js : string ,
101- globalBlock ?: string ,
102- importedScriptBlock ?: string ,
103- ) : string => {
104- let jsString = `` ;
105-
106- if ( importedScriptBlock ) {
107- jsString += `${ importedScriptBlock } \n\n` ;
108- }
109-
110- if ( globalBlock ) {
111- jsString += `${ globalBlock } \n\n` ;
112- }
113-
114- return ( jsString += `${ js } ` ) ;
115- } ;
116-
117173/**
118174 * @deprecated Consider using useEvaluate or createBinding which will
119175 * optimize creating the evaluation context
0 commit comments