@@ -87,7 +87,7 @@ export interface ExplicitTemplateOnlyOptions extends BaseTemplateOptions {
8787 * static {
8888 * template('{{this.#greeting}}, {{@place }}!',
8989 * { component: this },
90- * scope: (instance) => ({ '#greeting': instance ? instance .#greeting : undefined }),
90+ * scope: (instance) => ({ '#greeting': instance.#greeting }),
9191 * );
9292 * }
9393 * }
@@ -216,7 +216,7 @@ export type ImplicitTemplateOnlyOptions = BaseTemplateOptions & ImplicitEvalOpti
216216 *
217217 * ## Private Fields Support
218218 *
219- * The implicit form now supports private fields. You can reference private
219+ * The implicit form supports private fields. You can reference private
220220 * class members in templates using the `this.#fieldName` syntax:
221221 *
222222 * ```ts
@@ -228,7 +228,7 @@ export type ImplicitTemplateOnlyOptions = BaseTemplateOptions & ImplicitEvalOpti
228228 * template(
229229 * '<button {{on "click" this.#increment}}>{{this.#count}}</button>',
230230 * { component: this },
231- * eval() { return arguments[0] }
231+ * eval() { return eval( arguments[0]) }
232232 * );
233233 * }
234234 * }
@@ -239,12 +239,13 @@ export type ImplicitClassOptions<C extends ComponentClass> = BaseClassTemplateOp
239239
240240/**
241241 * Well-known symbol key used to store private field getter closures on a
242- * component class. The getters are created via `eval` inside the class's
243- * `static` block so they retain access to the class's private field brand.
242+ * component class. The getters are created inside the class's `static` block
243+ * (via the explicit `scope` function or via `eval`) so they retain access to
244+ * the class's private field brand.
244245 *
245246 * Shape: `Record<string, (instance: object) => unknown>`
246247 */
247- export const PRIVATE_FIELD_GETTERS = Symbol . for ( 'ember:private-field-getters' ) ;
248+ const PRIVATE_FIELD_GETTERS = Symbol . for ( 'ember:private-field-getters' ) ;
248249
249250/**
250251 * Extract private field names (e.g. `#count`, `#increment`) referenced
@@ -262,96 +263,39 @@ function extractPrivateFields(templateString: string): string[] {
262263
263264export function template (
264265 templateString : string ,
265- options ?: ( ExplicitTemplateOnlyOptions | ImplicitTemplateOnlyOptions ) & { component ?: never }
266+ options ?: ExplicitTemplateOnlyOptions | ImplicitTemplateOnlyOptions
266267) : TemplateOnlyComponent ;
267268
268- export function template < C extends ComponentClass > (
269+ // Overload for explicit scope with class component.
270+ // Uses a direct instance type parameter `I` instead of `InstanceType<C>` so
271+ // TypeScript can contextually type the `scope` callback parameter without
272+ // needing to resolve a conditional type during inference.
273+ export function template < I extends object > (
269274 templateString : string ,
270- options : ExplicitClassOptions < C >
271- ) : C ;
275+ options : {
276+ component : abstract new ( ...args : any [ ] ) => I ;
277+ scope : ( instance : I ) => Record < string , unknown > ;
278+ } & BaseTemplateOptions
279+ ) : abstract new ( ...args : any [ ] ) => I ;
272280
273281export function template < C extends ComponentClass > (
274282 templateString : string ,
275- options : ( ImplicitClassOptions < C > | BaseClassTemplateOptions < C > & { scope ?: never } )
283+ options : ImplicitClassOptions < C > | BaseClassTemplateOptions < C >
276284) : C ;
285+
277286export function template (
278287 templateString : string ,
279288 providedOptions ?: BaseTemplateOptions | BaseClassTemplateOptions < any >
280289) : object {
281290 const options : EmberPrecompileOptions = { strictMode : true , ...providedOptions } ;
282291
283- const privateFields = extractPrivateFields ( templateString ) ;
284-
285- // When using the explicit scope form with private fields, the scope
286- // function has the shape:
287- //
288- // (instance) => ({ Section, '#secret': instance?.#secret })
289- //
290- // The optional chaining (`?.`) on the instance parameter is required so
291- // that calling the scope function without an instance (at compile time)
292- // returns `undefined` for private field entries instead of crashing.
293- //
294- // At compile time we use the scope to:
295- // 1. Discover which variables are in lexical scope (non-private entries)
296- // 2. Build an evaluator that can resolve the compiled wire format
297- //
298- // Private fields are not resolved through the evaluator. Instead, at
299- // runtime they are accessed via PRIVATE_FIELD_GETTERS stored on the
300- // component class, which _getProp looks up when it encounters a `#` key.
301- let originalScopeWithInstance : ( ( instance : any ) => Record < string , unknown > ) | undefined ;
302-
303- if ( privateFields . length > 0 && options . scope && ! options . eval && options . component ) {
304- originalScopeWithInstance = options . scope as ( instance : any ) => Record < string , unknown > ;
305- const origScope = originalScopeWithInstance ;
306-
307- // Wrap the scope function so that compile-time callers (compileOptions
308- // and buildEvaluator) receive only non-private entries. The private
309- // field names are injected as placeholders so lexicalScope recognises
310- // them as in-scope.
311- options . scope = ( ) => {
312- // Call with no instance — private fields evaluate to `undefined`
313- // thanks to optional chaining in the scope function.
314- const fullScope = origScope ( undefined ) ;
315- const safeScope : Record < string , unknown > = { } ;
316- for ( const key of Object . keys ( fullScope ) ) {
317- if ( key [ 0 ] !== '#' ) {
318- safeScope [ key ] = fullScope [ key ] ;
319- }
320- }
321- // Inject private field names so lexicalScope reports them as in-scope.
322- for ( const field of privateFields ) {
323- safeScope [ field ] = true ;
324- }
325- return safeScope ;
326- } ;
327- }
328-
329- const evaluate = buildEvaluator ( options ) ;
292+ const { evaluate, privateFieldGetters } = buildEvaluator ( templateString , options ) ;
330293
331294 const normalizedOptions = compileOptions ( options ) ;
332295 const component = normalizedOptions . component ?? templateOnly ( ) ;
333296
334- // If the template references private fields (this.#field), create getter
335- // closures that can access the private fields at runtime. These are stored
336- // on the component class and looked up by _getProp.
337- if ( privateFields . length > 0 ) {
338- if ( originalScopeWithInstance ) {
339- // Explicit form: build getters from the scope(instance) function.
340- const scopeFn = originalScopeWithInstance ;
341- const getters : Record < string , ( obj : object ) => unknown > = { } ;
342- for ( const field of privateFields ) {
343- getters [ field ] = ( obj : object ) => scopeFn ( obj ) [ field ] ;
344- }
345- ( component as any ) [ PRIVATE_FIELD_GETTERS ] = getters ;
346- } else if ( evaluate !== evaluator ) {
347- // Implicit (eval) form: generate getter closures via eval.
348- const getterEntries = privateFields . map ( ( f ) => `${ JSON . stringify ( f ) } : (obj) => obj.${ f } ` ) ;
349- const getters = evaluate ( `({${ getterEntries . join ( ', ' ) } })` ) as Record <
350- string ,
351- ( obj : object ) => unknown
352- > ;
353- ( component as any ) [ PRIVATE_FIELD_GETTERS ] = getters ;
354- }
297+ if ( privateFieldGetters ) {
298+ ( component as any ) [ PRIVATE_FIELD_GETTERS ] = privateFieldGetters ;
355299 }
356300
357301 const source = glimmerPrecompile ( templateString , normalizedOptions ) ;
@@ -366,38 +310,73 @@ const evaluator = (source: string) => {
366310 return new Function ( `return ${ source } ` ) ( ) ;
367311} ;
368312
369- function buildEvaluator ( options : Partial < EmberPrecompileOptions > | undefined ) {
370- if ( options === undefined ) {
371- return evaluator ;
372- }
313+ type EvaluatorResult = {
314+ evaluate : ( source : string ) => unknown ;
315+ privateFieldGetters ?: Record < string , ( obj : object ) => unknown > ;
316+ } ;
373317
318+ function buildEvaluator (
319+ templateString : string ,
320+ options : Partial < EmberPrecompileOptions >
321+ ) : EvaluatorResult {
374322 if ( options . eval ) {
375- return options . eval ;
376- } else {
377- const scope = options . scope ?.( ) ;
323+ const evaluate = options . eval ;
378324
379- if ( ! scope ) {
380- return evaluator ;
381- }
382-
383- // Filter out private field entries (#-prefixed keys) — those are not
384- // lexical variables and cannot be used as Function parameter names.
385- // Private fields are handled separately via PRIVATE_FIELD_GETTERS.
386- const argNames : string [ ] = [ ] ;
387- const argValues : unknown [ ] = [ ] ;
388- for ( const [ key , value ] of Object . entries ( scope ) ) {
389- if ( key [ 0 ] !== '#' ) {
390- argNames . push ( key ) ;
391- argValues . push ( value ) ;
325+ // Implicit (eval) form: extract `this.#field` references from the template
326+ // and build getter closures via eval while we still have access to the
327+ // class's private field brand.
328+ if ( options . component ) {
329+ const privateFields = extractPrivateFields ( templateString ) ;
330+ if ( privateFields . length > 0 ) {
331+ const entries = privateFields . map ( ( f ) => `${ JSON . stringify ( f ) } : (obj) => obj.${ f } ` ) ;
332+ const privateFieldGetters = evaluate ( `({${ entries . join ( ', ' ) } })` ) as Record <
333+ string ,
334+ ( obj : object ) => unknown
335+ > ;
336+ return { evaluate, privateFieldGetters } ;
392337 }
393338 }
394339
395- if ( argNames . length === 0 ) {
396- return evaluator ;
340+ return { evaluate } ;
341+ }
342+
343+ const scopeFn = options . scope as ( ( instance ?: any ) => Record < string , unknown > ) | undefined ;
344+
345+ if ( ! scopeFn ) {
346+ return { evaluate : evaluator } ;
347+ }
348+
349+ const scope = scopeFn ( ) ;
350+
351+ // Separate #-prefixed keys (private fields) from regular lexical variables.
352+ // Private field names can't be used as Function parameter names, so they must
353+ // be filtered out of the evaluator. They're resolved at runtime via getters.
354+ const argNames : string [ ] = [ ] ;
355+ const argValues : unknown [ ] = [ ] ;
356+ const privateKeys : string [ ] = [ ] ;
357+
358+ for ( const [ key , value ] of Object . entries ( scope ) ) {
359+ if ( key [ 0 ] === '#' ) {
360+ privateKeys . push ( key ) ;
361+ } else {
362+ argNames . push ( key ) ;
363+ argValues . push ( value ) ;
397364 }
365+ }
398366
399- return ( source : string ) => {
400- return new Function ( ...argNames , `return (${ source } )` ) ( ...argValues ) ;
401- } ;
367+ const evaluate =
368+ argNames . length > 0
369+ ? ( source : string ) => new Function ( ...argNames , `return (${ source } )` ) ( ...argValues )
370+ : evaluator ;
371+
372+ // Build private field getters from the scope function if any #-keys exist.
373+ let privateFieldGetters : Record < string , ( obj : object ) => unknown > | undefined ;
374+ if ( privateKeys . length > 0 && options . component ) {
375+ privateFieldGetters = { } ;
376+ for ( const key of privateKeys ) {
377+ privateFieldGetters [ key ] = ( obj : object ) => scopeFn ( obj ) [ key ] ;
378+ }
402379 }
380+
381+ return { evaluate, privateFieldGetters } ;
403382}
0 commit comments