From b181759ba4e8b936ee633657749874a79d8b3c91 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 May 2025 10:01:44 -0400 Subject: [PATCH 1/6] Do we need the microtask wrapper here? --- packages/@ember/template-compiler/lib/template.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts index 7140656fffd..51f77b71f8f 100644 --- a/packages/@ember/template-compiler/lib/template.ts +++ b/packages/@ember/template-compiler/lib/template.ts @@ -242,12 +242,10 @@ export function template( const normalizedOptions = compileOptions(options); const component = normalizedOptions.component ?? templateOnly(); - queueMicrotask(() => { - const source = glimmerPrecompile(templateString, normalizedOptions); - const template = templateFactory(evaluate(`(${source})`) as SerializedTemplateWithLazyBlock); + const source = glimmerPrecompile(templateString, normalizedOptions); + const template = templateFactory(evaluate(`(${source})`) as SerializedTemplateWithLazyBlock); - setComponentTemplate(template, component); - }); + setComponentTemplate(template, component); return component; } From b05e716be5ca74c2b545e4de5e9ee538eafa8dc4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 May 2025 11:26:06 -0400 Subject: [PATCH 2/6] Failing tests! --- ...runtime-template-compiler-explicit-test.ts | 43 +++++++++++++++++++ ...runtime-template-compiler-implicit-test.ts | 37 ++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts index 99dd11c8a8d..41e1cffd74d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts @@ -1,3 +1,4 @@ +import { tracked } from '@ember/-internals/metal'; import { template } from '@ember/template-compiler/runtime'; import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers'; import GlimmerishComponent from '../../utils/glimmerish-component'; @@ -20,6 +21,48 @@ moduleFor( this.assertStableRerender(); } + async '@test can derive the template string from tracked'() { + class State { + @tracked str = `hello there`; + + get component() { + return template(this.str); + } + } + let state = new State(); + + this.render('', { state }); + + this.assertHTML('hello there'); + this.assertStableRerender(); + + state.str += '!'; + + this.assertHTML('hello there!'); + this.assertStableRerender(); + } + + async '@test can have tracked data in the scope bag - and changes to that scope bag dont re-compile'() { + class State { + @tracked str = `hello there`; + + get component() { + return template(`{{greeting}}`, { scope: () => ({ greeting: this.str }) }); + } + } + let state = new State(); + + this.render('', { state }); + + this.assertHTML('hello there'); + this.assertStableRerender(); + + state.str += '!'; + + this.assertHTML('hello there!'); + this.assertStableRerender(); + } + async '@test Can use a custom helper in scope (in append position)'() { await this.renderComponentModule(() => { let foo = () => 'Hello, world!'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts index 3aaf14d29f4..b3a1061ed9f 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts @@ -1,3 +1,4 @@ +import { tracked } from '@ember/-internals/metal'; import { template } from '@ember/template-compiler/runtime'; import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers'; import GlimmerishComponent from '../../utils/glimmerish-component'; @@ -7,6 +8,42 @@ import { fn } from '@ember/helper'; moduleFor( 'Strict Mode - Runtime Template Compiler (implicit)', class extends RenderingTestCase { + async '@test can have in-scope tracked data'() { + class State { + @tracked str = `hello there`; + + get component() { + let getStr = () => this.str; + + hide(getStr); + + return template(`{{ (getStr) }}`, { + eval() { + return eval(arguments[0]); + }, + }); + } + } + + let state = new State(); + + await this.renderComponentModule(() => { + return template('', { + eval() { + return eval(arguments[0]); + }, + }); + }); + + this.assertHTML('hello there'); + this.assertStableRerender(); + + state.str += '!'; + + this.assertHTML('hello there!'); + this.assertStableRerender(); + } + async '@test Can use a component in scope'() { await this.renderComponentModule(() => { let Foo = template('Hello, world!', { From 2994276731006ee84d2716d2b8dc78b306630099 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 May 2025 11:30:35 -0400 Subject: [PATCH 3/6] Use the test utilities correctly --- .../components/runtime-template-compiler-explicit-test.ts | 6 +++--- .../components/runtime-template-compiler-implicit-test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts index 41e1cffd74d..74d9b4dd4f1 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts @@ -1,6 +1,6 @@ import { tracked } from '@ember/-internals/metal'; import { template } from '@ember/template-compiler/runtime'; -import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers'; +import { RenderingTestCase, defineSimpleModifier, moduleFor, runTask } from 'internal-test-helpers'; import GlimmerishComponent from '../../utils/glimmerish-component'; import { on } from '@ember/modifier/on'; import { fn } from '@ember/helper'; @@ -36,7 +36,7 @@ moduleFor( this.assertHTML('hello there'); this.assertStableRerender(); - state.str += '!'; + runTask(() => (state.str += '!')); this.assertHTML('hello there!'); this.assertStableRerender(); @@ -57,7 +57,7 @@ moduleFor( this.assertHTML('hello there'); this.assertStableRerender(); - state.str += '!'; + runTask(() => (state.str += '!')); this.assertHTML('hello there!'); this.assertStableRerender(); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts index b3a1061ed9f..351fd52f27b 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts @@ -1,6 +1,6 @@ import { tracked } from '@ember/-internals/metal'; import { template } from '@ember/template-compiler/runtime'; -import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers'; +import { RenderingTestCase, defineSimpleModifier, moduleFor, runTask } from 'internal-test-helpers'; import GlimmerishComponent from '../../utils/glimmerish-component'; import { on } from '@ember/modifier/on'; import { fn } from '@ember/helper'; @@ -38,7 +38,7 @@ moduleFor( this.assertHTML('hello there'); this.assertStableRerender(); - state.str += '!'; + runTask(() => (state.str += '!')); this.assertHTML('hello there!'); this.assertStableRerender(); From 9d72875964d68a8d2391f2af897b58d28ad4fc55 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 May 2025 11:40:07 -0400 Subject: [PATCH 4/6] Unify --- ...runtime-template-compiler-explicit-test.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts index 74d9b4dd4f1..cea1d962895 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts @@ -21,46 +21,63 @@ moduleFor( this.assertStableRerender(); } - async '@test can derive the template string from tracked'() { + async '@test can derive the template string from tracked'(assert: Assert) { class State { @tracked str = `hello there`; get component() { + assert.step('get component'); return template(this.str); } } let state = new State(); - this.render('', { state }); + await this.renderComponentModule(() => { + return template('', { scope: () => ({ state }) }); + }); this.assertHTML('hello there'); this.assertStableRerender(); + assert.verifySteps(['get component']); runTask(() => (state.str += '!')); this.assertHTML('hello there!'); this.assertStableRerender(); + assert.verifySteps(['get component']); } - async '@test can have tracked data in the scope bag - and changes to that scope bag dont re-compile'() { + async '@test can have tracked data in the scope bag - and changes to that scope bag dont re-compile'( + assert: Assert + ) { class State { @tracked str = `hello there`; get component() { - return template(`{{greeting}}`, { scope: () => ({ greeting: this.str }) }); + assert.step('get component'); + return template(`{{greeting}}`, { + scope: () => { + assert.step('scope()'); + return { greeting: this.str }; + }, + }); } } let state = new State(); - this.render('', { state }); + await this.renderComponentModule(() => { + return template('', { scope: () => ({ state }) }); + }); this.assertHTML('hello there'); this.assertStableRerender(); + assert.verifySteps(['get component', 'scope()']); runTask(() => (state.str += '!')); this.assertHTML('hello there!'); this.assertStableRerender(); + assert.verifySteps(['scope()']); } async '@test Can use a custom helper in scope (in append position)'() { From 99133c12274da134c517da81e2fab468202a8187 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 May 2025 12:25:34 -0400 Subject: [PATCH 5/6] eval sure gets called a lot --- ...runtime-template-compiler-implicit-test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts index 351fd52f27b..7d35a2fab58 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts @@ -8,12 +8,17 @@ import { fn } from '@ember/helper'; moduleFor( 'Strict Mode - Runtime Template Compiler (implicit)', class extends RenderingTestCase { - async '@test can have in-scope tracked data'() { + async '@test can have in-scope tracked data'(assert: Assert) { class State { @tracked str = `hello there`; get component() { - let getStr = () => this.str; + assert.step('get component'); + + let getStr = () => { + assert.step('getStr()'); + return this.str; + }; hide(getStr); @@ -30,6 +35,7 @@ moduleFor( await this.renderComponentModule(() => { return template('', { eval() { + assert.step('eval'); return eval(arguments[0]); }, }); @@ -37,11 +43,21 @@ moduleFor( this.assertHTML('hello there'); this.assertStableRerender(); + assert.verifySteps([ + // for every value in the component, for eevry node traversed in the compiler + 'eval', // precompileJSON -> ... ElementNode -> ... -> lexicalScope -> isScope('state', ...) + 'eval', // "..." + 'eval', // "..." + 'eval', // creating the templateFactory + 'get component', + 'getStr()', + ]); runTask(() => (state.str += '!')); this.assertHTML('hello there!'); this.assertStableRerender(); + assert.verifySteps(['getStr()']); } async '@test Can use a component in scope'() { From 163b9210bd5ae8c64d7b7cb23fbcb7db23443d37 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 30 May 2025 08:15:19 -0400 Subject: [PATCH 6/6] We can use untrack() a bit to help, but now something else is goofy --- .../runtime-template-compiler-explicit-test.ts | 6 ++---- .../@ember/template-compiler/lib/template.ts | 16 +++++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts index cea1d962895..6d0625a37db 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts @@ -57,7 +57,6 @@ moduleFor( assert.step('get component'); return template(`{{greeting}}`, { scope: () => { - assert.step('scope()'); return { greeting: this.str }; }, }); @@ -69,15 +68,14 @@ moduleFor( return template('', { scope: () => ({ state }) }); }); + assert.verifySteps(['get component']); this.assertHTML('hello there'); this.assertStableRerender(); - assert.verifySteps(['get component', 'scope()']); runTask(() => (state.str += '!')); - + assert.verifySteps([]); this.assertHTML('hello there!'); this.assertStableRerender(); - assert.verifySteps(['scope()']); } async '@test Can use a custom helper in scope (in append position)'() { diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts index 51f77b71f8f..4d31b51f857 100644 --- a/packages/@ember/template-compiler/lib/template.ts +++ b/packages/@ember/template-compiler/lib/template.ts @@ -1,3 +1,4 @@ +import { untrack } from '@glimmer/validator'; import templateOnly, { type TemplateOnlyComponent } from '@ember/component/template-only'; import { precompile as glimmerPrecompile } from '@glimmer/compiler'; import type { SerializedTemplateWithLazyBlock } from '@glimmer/interfaces'; @@ -236,10 +237,15 @@ export function template( templateString: string, providedOptions?: BaseTemplateOptions | BaseClassTemplateOptions ): object { + // When figuring out how to compile the template, we don't want to engage + // with auto-tracking in a way that would cause the template + // to be re-rendered when the intended-to-be-lazily-accessesd scope bag's contents changes. + // + // This is why we use our reactivity-evading utility untrack() here const options: EmberPrecompileOptions = { strictMode: true, ...providedOptions }; - const evaluate = buildEvaluator(options); + const evaluate = untrack(() => buildEvaluator(options)); - const normalizedOptions = compileOptions(options); + const normalizedOptions = untrack(() => compileOptions(options)); const component = normalizedOptions.component ?? templateOnly(); const source = glimmerPrecompile(templateString, normalizedOptions); @@ -262,15 +268,15 @@ function buildEvaluator(options: Partial | undefined) { if (options.eval) { return options.eval; } else { - const scope = options.scope?.(); + let scope = options.scope?.(); if (!scope) { return evaluator; } return (source: string) => { - const argNames = Object.keys(scope); - const argValues = Object.values(scope); + const argNames = Object.keys(scope ?? {}); + const argValues = Object.values(scope ?? {}); return new Function(...argNames, `return (${source})`)(...argValues); };