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);
};