Repository navigation
Conversation
This was referenced Apr 25, 2026
Closed
lifeart
added a commit
to lifeart/ember.js
that referenced
this pull request
May 5, 2026
The $_dc_ember dynamic-component path tracks the rendered Ember instance
via __gxtDcCaptureCallback so that swap-outs (`{{component this.name}}`
when this.name changes) can fire willDestroy on the previous instance.
That callback was only invoked from renderClassicComponent. If the
resolved class went down the glimmerish branch
(renderGlimmerComponent — tagless/Glimmer.Component shapes), the
callback never fired and _dcEmberInstance stayed null, so
destroyCurrentDcInstance() short-circuited at swap time and the
previous instance never got its willDestroy hook.
Cumulative-state symptom (PR emberjs#21340): "component helper destroys
underlying component when it is swapped out" asserts
`{ 'foo-bar': 1, 'foo-bar-baz': 0 }` after the first swap but observes
`{ 'foo-bar': 0, ... }`. The classic-componentTemplate path resolves
foo-bar through renderGlimmerComponent in some cumulative-state code
paths (likely after pool reuse / stale state).
Fix: mirror the callback fire+clear from renderClassicComponent into
renderGlimmerComponent. setInstanceCapture (stack-based) is already
called above; this adds the global-callback path used by $_dc_ember.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lifeart
added a commit
to lifeart/ember.js
that referenced
this pull request
May 7, 2026
The 8 _gxtEffect(...) call sites in gxt-backend/manager.ts each install a GXT formula (sourceTag + tag + opcode) but ignored the cleanup function returned by effect(). When a component instance / DOM element was torn down between tests, those formulas stayed live in the global formula tracker. Across hundreds of cumulative tests, leaked formulas accumulated. The leak surface explains the cumulative-state cascade observed in PR emberjs#21340: a single @tracked-property dirty in the failing test propagated to every leaked formula, each of which re-evaluated its closure (re-reading `get person`, which constructs a fresh Person and dirties five more tags), saturating the main thread with __gxtExternalSchedule calls (493k+ in 10 minutes) and starving the 16ms interval-fallback sync. Wrap each call site with `_gxtEffectWithOwner(owner, cb)`, which: * Calls _gxtEffect(cb) and captures its cleanup. * Increments / decrements globalThis.__gxtActiveEffectCount (a diagnostic counter — useful for catching future leaks via QUnit.testDone instrumentation; cost is one ++/-- per effect). * Registers the cleanup on the owning destroyable via ./destroyable.registerDestructor, so when Ember tears down the component / element the formula's sourceTag, tag, and opcode all destruct, releasing the formula tracker entry. Owner mapping per call site: * manager.ts:1593 (createComponentInstance arg-cell effect) -> instance * manager.ts:2369 (updateInstanceWithNewArgs arg-cell effect) -> instance * manager.ts:5627 (createRenderContext path 4) -> instance || renderContext * manager.ts:5748 (createRenderContext path 3) -> instance || renderContext * manager.ts:9467 (renderCustomElement modifier ON_CREATED) -> el * manager.ts:9742 (renderLinkToElement modifier ON_CREATED) -> instance || el * manager.ts:10451 (Input/Textarea modifier ON_CREATED) -> instance || el * manager.ts:10879 (renderClassicComponent wrapper ON_CREATED) -> instance || wrapper Verification: * Component Tracked Properties module-isolation: 17/17 (no regression). * Smoke suite (14 modules / 333 tests): 333/333 PASS. * Diagnostic short repro (chromium loading just the Component Tracked Properties module): __gxtActiveEffectCount is now stable at 2 across 13 consecutive testDone events including the previously cascading "tracked properties rerender when updated outside of a runloop" test (was: count grew unboundedly per testDone before fix). The compile.ts:8165 site is intentionally not patched here: that reference to _gxtEffect is currently a free identifier wrapped in try/catch (silent ReferenceError), so it never installs a leaking effect in practice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lifeart
added a commit
to lifeart/ember.js
that referenced
this pull request
May 23, 2026
…dlers + lower-footprint args Prevents the laptop-freeze observed during parallel agent test runs: each runner spawned 1 chromium + ~8 child processes, and concurrent runs accumulated 17+ chrome-headless-shell at 55%+ CPU each. Symptom captured in PR emberjs#21340 session. Additions: - Lockfile at /tmp/gxt-test-runner.lock — refuses concurrent runs (with stale-pid detection) - SIGINT/SIGTERM/SIGHUP handlers + uncaughtException handler — guaranteed browser.close() - Idempotent cleanup() helper via cleanupRan flag - Lower-footprint chromium args: --disable-gpu, --disable-extensions, --disable-audio-output, --disable-software-rasterizer, --no-sandbox, --disable-features=...IsolateOrigins, site-per-process Default behavior unchanged for single-runner invocations. Concurrent runs now refuse to start instead of compounding resource exhaustion.
lifeart
force-pushed
the
glimmer-next-fresh
branch
3 times, most recently
from
June 11, 2026 03:53
109ea71 to
99f0b68
Compare
lifeart
added a commit
to lifeart/ember.js
that referenced
this pull request
Jun 11, 2026
…machinery Closes the @glimmer/component import-identity fork on the GXT side (PR emberjs#21340 Known-limitations bullet) with the documented Option-2 fallback, while leaving protocol extraction as the committed Option-1 target. - packages/@glimmer/component-gxt/: workspace-private sibling. `.` re-exports @glimmer/component 2.x's public surface (default Component); `./reactive` re-exports createTag/CURRENT_TAG/tracked/cached/createCache/getValue from the @ember/-internals/gxt-backend shims so its reactive symbols are identity-equal with the GXT build (not a second copy). Thin re-export, not reimplementation. Excluded from the root tsconfig like the gxt-backend shims (specifiers resolve only under the GXT rollup alias map); own tsconfig + ambient .d.ts keep it independently type-checkable. - identity test (__tests__/identity.test.ts) + demo vitest config: asserts === between symbols reached through @glimmer/component-gxt and through the shims, and demonstrates the fork by showing they differ from the classic @glimmer/validator copy. - scripts/ember-cli-gxt.mjs: `enable` now runs the import-identity guard (scans package.json deps + optional app-source grep, excluding the -gxt sibling) and refuses with the RFC-mandated message + non-zero exit on a direct @glimmer/component; `status` reports guard state. Self-contained node. - RFC §6: records the fallback as implemented/tested in-repo, notes real shipping still needs the ember-source-gxt packaging from §5.5. Gates (light gate: new files + script + tsconfig exclude; no gxt-backend runtime source touched): root `tsc --noEmit` 22/22 unchanged; package-local tsc clean; identity test 4/4; ember-cli-gxt status/enable-refusal/clean-enable node-smoked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lifeart
added a commit
to lifeart/ember.js
that referenced
this pull request
Jun 11, 2026
… outcome) Addresses the last open Known-limitations bullet of PR emberjs#21340: the FastBoot/SSR pipeline blockers. Phase 4.1 (root-context isolation in gxt-backend/compile.ts): mapped every piece of root-scoped singleton state and classified (a)/(b)/(c). Outcome is analysis-only, not a refactor. The genuinely root-scoped state in compile.ts (_gxtRootContext, _gxtTopOutletRef, _gxtEngineInstances) is a thin, already-bridged layer, but the real "one root" boundary -- GXT's module-global node counter, rehydration stack, and setupGlobalScope -- lives in upstream @lifeart/gxt and cannot be isolated from compile.ts. Threading N roots through compile.ts while the upstream counter stays global would be cosmetic and regression-prone on the backend's most load-bearing file. Documented the smallest correct shape (upstream seedable counter + a synchronous withRootContext swap API) for when an SSR driver exists. Phase 4.2 (nested-engine outlet cursor-ID translation): located the lossy point -- GxtPartialRehydrationDelegate re-renders the sub-tree from scratch (innerHTML='') because GXT's node counter exposes only resetNodeCounter() (reset to 0), with no setNodeCounter(n) seed, so a mid-tree boundary cannot resume the parent's counter namespace on the client. Documented the fix shape (upstream seedable counter + boundary base-offset in withRehydration; delegate reads the boundary data-node-id). No code change; documentation-only, so the full GXT gate is not triggered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lifeart
force-pushed
the
glimmer-next-fresh
branch
2 times, most recently
from
June 12, 2026 12:10
96acc59 to
e401aaa
Compare
Contributor
📊 Size reportTarball size — dist/dev 3%↑
Show files (29 files)
dist/prod 3%↑
Show files (28 files)
smoke-tests/v2-app-template/dist 2%↑
smoke-tests/v2-app-hello-world-template/dist 3%↑
🤖 This report was automatically generated by wyvox/pkg-size |
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Jun 12, 2026
The vite compiler plugin's .ts/.js content sniff (/hbs\s*`/) false-positived on doc-comment prose containing the byte sequence hbs-backtick — e.g. "components were authored in paired `.hbs` and `.js` files", where the closing backtick of the inline-code span follows "hbs" directly. The plugin then routed the entire module through the template transform and mangled its exports (emberjs/ember.js#21340: "FunctionBasedHelper is not exported" broke the GXT vite build on the PR merge ref after an upstream api-docs sweep). The sniff now requires a genuine tag position via negative lookbehind (excluding `.hbs`, 'hbs', "hbs", `hbs and identifier tails like stubhbs`), extracted as hasHbsTaggedTemplate() with unit coverage. Gates: tsc clean, vitest full suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Jun 13, 2026
…integration APIs (#222) * feat(host): registerHostHooks() formal host-integration API Replaces the optional globalThis.__gxt* extension slots with module-local hook slots behind an explicit registration API (HOST_HOOKS table). Covered hooks: toBool, scheduleRevalidate (__gxtExternalSchedule), registerListMarker, rebindEachItem, registerObjectValueOwner, getCurrentTemplateThis, isFunctionalHelper/markFunctionalHelper (the EmberFunctionalHelpers Set), and dynamicEval (the globalThis.$_eval fallback). Every runtime call site reads the hook slot first and falls back to the historical global, so hosts running the legacy wiring keep working unchanged (no lockstep upgrade). Gates: tsc clean, vitest 3817/3817, qunit 1219/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(host): registerHostManagers() formal manager-registration API Companion to registerHostHooks: replaces the legacy contract of hunting down the module-original $_MANAGERS object reference (bridge accessors + deferred microtask retries on the host side) and mutating it from outside. The function lives in the same module instance the runtime call sites close over, so registration always reaches the canonical table regardless of how the host resolved the package — no object-identity hazards. Slots are replaced wholesale (hosts carry extra private state on their manager objects). Gates: tsc clean, vitest 3819/3819, qunit 1219/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(compiler): don't treat hbs-extension prose as a tagged template The vite compiler plugin's .ts/.js content sniff (/hbs\s*`/) false-positived on doc-comment prose containing the byte sequence hbs-backtick — e.g. "components were authored in paired `.hbs` and `.js` files", where the closing backtick of the inline-code span follows "hbs" directly. The plugin then routed the entire module through the template transform and mangled its exports (emberjs/ember.js#21340: "FunctionBasedHelper is not exported" broke the GXT vite build on the PR merge ref after an upstream api-docs sweep). The sniff now requires a genuine tag position via negative lookbehind (excluding `.hbs`, 'hbs', "hbs", `hbs and identifier tails like stubhbs`), extracted as hasHbsTaggedTemplate() with unit coverage. Gates: tsc clean, vitest full suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Jun 13, 2026
…228) Two independent regressions shipped in 0.0.66 broke the Ember dual-backend integration (emberjs/ember.js#21340). Both root-caused by bisect against the Ember GXT suite (each: 266 fail, Helpers: 666 fail under 0.0.66; 0/0 here). 1. Frame mode (#226) wrongly engaged for Ember hosts. frameHostHooksInstalled() detected the host via three globalThis.__gxt* slots, but the Ember integration migrated exactly those channels off globalThis into registerHostHooks (HOST_HOOKS module-local table). Worse, HOST_HOOKS is duplicated across entry chunks, so even a direct HOST_HOOKS probe from the frame-gate chunk sees an empty copy. Fix: registerHostHooks sets a single cross-instance globalThis sentinel (__gxtHostHooksInstalled) — restoring the cross-module-instance visibility the legacy __gxt* slots had — and frameHostHooksInstalled checks it (plus the legacy slots for pre-API hosts). Frame mode stays ON for standalone consumers; Ember bails to v1. 2. The drain-overhead diet (#227) broke the Ember formula-bridge reactivity (stale values, wrong arg order) despite its "semantics unchanged" claim. Reverted pending a re-attempt with the Ember integration suite as a gate — it was a modest constant-factor refactor, no functional dependency. Validated against the full Ember GXT suite (both probes and gates): the six affected suites (each/Helpers/dynamic-component/strict-mode/engine/rehydration) return to 0 failures. gxt's own suite: 3905 functional tests pass. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Jun 15, 2026
Two independent regressions shipped in 0.0.66 broke the Ember dual-backend integration (emberjs/ember.js#21340). Both root-caused by bisect against the Ember GXT suite (each: 266 fail, Helpers: 666 fail under 0.0.66; 0/0 here). 1. Frame mode (#226) wrongly engaged for Ember hosts. frameHostHooksInstalled() detected the host via three globalThis.__gxt* slots, but the Ember integration migrated exactly those channels off globalThis into registerHostHooks (HOST_HOOKS module-local table). Worse, HOST_HOOKS is duplicated across entry chunks, so even a direct HOST_HOOKS probe from the frame-gate chunk sees an empty copy. Fix: registerHostHooks sets a single cross-instance globalThis sentinel (__gxtHostHooksInstalled) — restoring the cross-module-instance visibility the legacy __gxt* slots had — and frameHostHooksInstalled checks it (plus the legacy slots for pre-API hosts). Frame mode stays ON for standalone consumers; Ember bails to v1. 2. The drain-overhead diet (#227) broke the Ember formula-bridge reactivity (stale values, wrong arg order) despite its "semantics unchanged" claim. Reverted pending a re-attempt with the Ember integration suite as a gate — it was a modest constant-factor refactor, no functional dependency. Validated against the full Ember GXT suite (both probes and gates): the six affected suites (each/Helpers/dynamic-component/strict-mode/engine/rehydration) return to 0 failures. gxt's own suite: 3905 functional tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lifeart
force-pushed
the
glimmer-next-fresh
branch
from
July 4, 2026 10:11
78a9661 to
bfee346
Compare
This was referenced Oct 11, 2026
Merged
lifeart
force-pushed
the
glimmer-next-fresh
branch
from
October 13, 2026 09:08
68874e5 to
8679ee1
Compare
This was referenced Oct 13, 2026
lifeart
force-pushed
the
glimmer-next-fresh
branch
2 times, most recently
from
October 25, 2026 14:49
bc3fda9 to
2d71e91
Compare
Adds the two GXT flavors next to the classic build, without changing the classic one: - `EMBER_RENDER_BACKEND=gxt` (rollup) / `GXT_MODE=true` (vite): the compat backend. A shared alias table (scripts/gxt-alias-map.mjs) swaps the @glimmer/* reactivity and manager packages for the gxt-backend shims and drops the Glimmer VM packages from the dist. `@glimmer/tracking` is not aliased: it is metal's `tracked` / `cached` in every build. - `EMBER_RENDER_BACKEND=gxt-native`: the native layer. Classic Ember source with `__GXT_MODE__ = false`; only the modules that touch the Glimmer VM are aliased to gxt-backend/native (same table, native section), and templates compile with the gxt compiler. `__GXT_MODE__` / `__GXT_CLASSIC_COMPONENTS__` are build-time constants (scripts/replace-build-constant.mjs, declared in types/gxt-ambient.d.ts) that both bundlers inline, so classic dists fold every GXT branch away. Other pieces: `@lifeart/gxt` pinned to exactly 0.0.86 as a root devDependency (ember-source does not install it; only the GXT packages declare it), playwright for the GXT browser tests, the bundle-size check and budgets (with a guard against GXT code left live in the classic dist), and the compat package assembler (scripts/build-gxt-package.mjs). Production dists keep only the `@__PURE__`-style and license comments (rollup `shouldPrintComment`). gxt-backend and @glimmer/component-gxt are linted and type-checked with the rest of the repo: `pnpm lint`, with overrides scoped to the two trees (barrel imports are allowed there, because the alias map matches exact specifiers; an empty catch block is an error), and both root tsc programs (tsconfig `paths` for the specifiers the trees use that no workspace link provides; the stable-decorators program leaves out one test of legacy-only decorator forms). The published types still leave the two trees out. The compat test configs compile templates with Glimmer's whitespace rules (gxt's EMBER_WHITESPACE flag), so a template's text is Glimmer's; the gxt compiler imports decorator-transforms' runtime itself, so a decorated .gjs module needs no `dt7948` global; a test module the gxt compiler cannot compile becomes a failing synthetic module instead of a page that fails to load. On top of main's build: the classic test build keeps main's `treeshake: false`; `@glimmer/component` resolves to its source in the GXT configs; the compat package's renamed-modules map leaves out main's internal mixin copies (`*-internal.js`), as packageMeta does. The per-fix history, including the lint and type-check coverage (5174ffe), is on lifeart:gxt-pre-squash-b1bf210aef. Folded in after the squash: the native forbidden list also covers the compat Glimmer host modules, and the bundle budgets are re-baselined (at the measured floor + 5%) after the GXT leftovers left the classic dist. The history of these follow-ups is on lifeart:gxt-followups. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
packages/@ember/-internals/gxt-backend renders Ember with glimmer-next
(@lifeart/gxt) instead of the Glimmer VM:
- compile.ts / ast-transforms.ts: the Ember template dialect on the gxt
compiler (runtime `precompileTemplate`, `plugins.ast`, the Ember keyword
helpers) and the gxt runtime patches the dialect needs. A template the gxt
compiler cannot compile throws with gxt's message (Glimmer's exact error
text is not a goal);
- manager.ts / ember-gxt-wrappers.ts / helper-manager.ts: component, helper
and modifier managers (classic curly components behind
`__GXT_CLASSIC_COMPONENTS__`), the `$_tag` / `$_maybeHelper` / `$_dc`
wrappers compiled templates call;
- validator.ts / reference.ts / destroyable.ts (+ the `-vm-compat` test
surfaces): @glimmer/validator, /reference and /destroyable on gxt cells,
including main's standalone `trackedValue` on the shim's tags;
- gxt-bridge.ts: the side-effect-free registry the classic packages reach
the backend through, so classic builds never load it. Its glimmer-host
slot is filled by glimmer-host.ts with the GXT code of the glimmer
package: the root template (root-template.ts), `renderComponent` and the
debug render tree (render-component.ts), the classic roots' render,
destruction, bounds and forced re-render (classic-roots.ts) and the
template adapter (template-adapter.ts);
- outlet.ts: route outlets. Main renders routes through route managers and a
`RootOutlet` component; the compat backend keeps its own outlet machinery
(the `<ember-outlet>` element, the root template's outlet path) driven by
the `{ render, outlets }` chain that `Route#setup` records through the
bridge's route-outlet hooks, and invokes any other route manager's wrapper
as main's outlet does.
Also @glimmer/component-gxt and the backend's own QUnit tests (__tests__),
which run on the GXT test page.
Main fixes since the first version of this series, each with a test:
- No re-render loops: reading a helper's value no longer writes reactive
state (helper args follow Glimmer's args proxy), and (hash)/(array)
arguments keep one identity while their members do not change.
- Reactivity: every write to a @Tracked property re-renders (a gxt cell
accessor no longer shadows the tracked one); a trackedArray changed in
place, a property dirtied with dirtyTagFor and a cell written in a
modifier's install all render by the end of the run loop; creating a
binding evaluates its expression once; Glimmer component arguments, (mut),
(readonly), dependent keys through arguments and createCache follow what
they read; a render asserts when it writes a tracked value it already read.
- Instances: one helper instance per call site and render scope; one
component instance per invocation (custom-managed and classic); each
modifier on an element has its own instance; components, modifiers and
helpers are destroyed with the block that rendered them, in node order, in
the run loop that removes it.
- Rendering: attributes are not arguments and reach a component only through
`...attributes`, with Glimmer's precedence; a null/undefined/false
attribute or class is removed; URL attributes use Glimmer's own
sanitization; a <template> component invoked by name, a class with a
class-body template and a class with a registered template render through
their component manager, with blocks, block params and `...attributes`; a
content mustache renders what its value is (text, node, component, helper,
curried value) and keeps its component while the curried value stays;
dynamic components follow their definition; {{#each}} rows, {{#each-in}}
keys, {{#in-element}}, SVG namespaces, named and else blocks, {{log}} and
{{debugger}} behave as in Glimmer.
- renderComponent: a root updates in place instead of being rendered again
on every tag change, is a root of its owner's renderer, renders into a
cursor, an <svg> or with a custom-managed plain-object definition, and is
part of the debug render tree.
- Errors: no error is swallowed any more. A render throws only its own
errors, an update's errors (its hooks, a new branch's component, the DOM
sync at the end of a run loop) reach the code that ran the run loop, and
empty catch blocks are gone.
- Lint and type-check clean (ESLint 376 -> 0, tsc 497 -> 0 problems); the two
vitest files became QUnit modules on the GXT test page.
The per-fix history, including the lint/type-check work and the moves of
gated code from the glimmer package into the host modules
(5ec62d6..b1bf210), is on lifeart:gxt-pre-squash-b1bf210aef.
Folded in after the squash: the GXT `template()` of @ember/template-compiler
(template-function.ts), the GXT re-render trigger and dependent recompute of
metal's property events (property-events.ts), the GXT parts of Route#refresh,
Route#setup and didTransition (outlet hooks), the view-tree rebuild of
getRootViews / getChildViews and a classic root's GXT render state
(classic-roots.ts) moved here from the classic files.
The history of these follow-ups is on lifeart:gxt-followups.
Also folded in after the third rebase onto main: a loose-mode
`{{attrs.x}}` raises Ember's assert-against-attrs assertion with its
source location; `{{debugger}}` in a template-only component sees the
template-only self through the render proxy; a `(fn ...)` result passed
to a helper is not invoked early; a helper recomputes when an EmberArray
argument changes in place; a custom modifier whose install throws keeps
the DOM already rendered (the error is rethrown after insert, as Glimmer
does). Their history is on lifeart:gxt-ember-easy-fixes and
lifeart:gxt-ember-hard-fixes.
Adapted in the rebase onto main 493985e, where a rendering test
renders its template through a Glimmer root (`BaseRenderer#render`) and
`Evented` is deprecated:
- a root of `BaseRenderer#render` renders through the glimmer host's
`renderRoot`, at its cursor and without renderComponent's clear,
replace and dedupe bookkeeping, and is returned to the caller;
- a classic component's lifecycle events are sent as Glimmer's curly
manager sends them (`sendCoreViewEvent`: the listeners, then the
method), without the `Evented#trigger` deprecation: the innermost of
the backend's per-instance `trigger` wrappers sends a dispatch the
backend marks, and an instance without a wrapper gets it directly; a
`trigger` call from user code, also one inside a hook, stays the
deprecated method;
- a component root gets the context cells of a classic root, so its
function-valued and ArrayProxy-valued properties re-render when set;
- a component root renders inside a render pass, so a write of a value
it already read raises the backtracking assertion;
- an error of a component root's commit phase (a modifier's install)
keeps the render and is rethrown once the render is complete;
- backtracking messages label a component root `{ROOT}` and a classic
root `-top-level`;
- a classic component destroyed with its block runs willDestroyElement
while its element is still in the document;
- the arguments of an internal component detect `(mut)` and `(readonly)`
values without reading other properties of an argument's value;
- a root whose first render failed stays in its renderer until the
renderer is destroyed;
- the backend's tests run main's deprecated `A()` and
`ObjectProxy.create` inside `ignoreDeprecation`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gxt-backend/native replaces only the Ember modules that touch the Glimmer VM
(renderer, outlet, managers, validator / reference / destroyable, template
factories, runtime helpers) with gxt-native implementations; everything else
is classic Ember source built with `__GXT_MODE__ = false`.
Main renders routes through route managers. Native aliases
`route-managers/root-outlet` to native/root-outlet.ts: the root outlet state
holds a native `OutletView` whose levels are live views of main's
`OutletState`s (owner, controller, tracked context as `@model`, the route
template looked up once per route bucket), and `renderComponent` renders a
`RootOutlet` through the owner's `appendOutletView`. The classic route
wrapper and the classic route manager's `RouteTemplate` (both Glimmer VM
internal components) are plain records, the route-manager registry asserts
through `@ember/debug` instead of `@glimmer/global-context`, `base-renderer`
maps to the native renderer, and owners with live roots are listed in main's
`renderers` list for `captureRenderTree`. Like main, the root outlet adds no
render-tree node.
Packaging: scripts/build-gxt-native-package.mjs and
scripts/gxt-native-package (ember-source-gxt-native, which pins the root's
exact @lifeart/gxt), assert-native-dist (the VM-free dist check), and the
modern-app bundle/boot harness (scripts/bundle-modern-app*.mjs,
smoke-tests/modern-app) plus the packaged consumer smoke test
(smoke-tests/gxt-native-package). Following main: the package and the
harness's dist snapshots carry ember-source's `sideEffects`;
@glimmer/component builds a dev and a prod dist of its own, and the package,
the preset and the dist assertion pick the one of the mode; the consumer app
compiles for main's browser targets (lib/browsers.cjs), so its bundle stays
byte-identical to the harness measurement; the renamed-modules map leaves out
main's internal mixin copies (`*-internal.js`), as packageMeta does.
Main fixes since the first version of this series:
- Decorated .gjs/.gts components boot in a consumer app: the package's vite
preset makes gxt's decorator output import decorator-transforms' runtime
instead of reading a `dt7948` global nothing installs.
- URL attributes are sanitized with Glimmer's own rules; a bound attribute
that is a property of its element is set through the property, and a
null/undefined one is removed.
- A component without a template yields its block (Glimmer's default
`{{yield}}` template), and the nearest template of a class chain wins,
registered or class-body.
- Retained memory per row: a call site without named arguments keeps one
shared empty hash instead of the fresh `{}` gxt passes on every
evaluation, and an element modifier is a class instance instead of two
closures over its state.
- `{{debugger}}` calls Glimmer's debugger callback; the package build warns
about a local gxt only when there is one.
The per-fix history is on lifeart:gxt-pre-squash-b1bf210aef.
Folded in after the squash: assert-native-dist knows the compat Glimmer host
modules.
The history of these follow-ups is on lifeart:gxt-followups.
Also folded in after the third rebase onto main: `{{get}}` reads
properties of primitives (Glimmer's `isDict`); `{{hash}}` members are lazy
getters that stay reactive, with a custom tag for computed keys through
them; modifiers are destroyed child-first, in element-closing order; the
debug render tree names a renderComponent root `{ROOT}` and a component
bound through a strict-mode scope by its binding. Their history is on
lifeart:gxt-ember-easy-fixes.
Adapted in the rebase onto main 493985e: a root whose first render
failed is torn down and stays registered until its owner is destroyed;
the native rendering test case exposes the render root as `this.root`,
which main's tests destroy; a native test expects the deprecation of
main's `A()`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The places where classic Ember hands over to the compat backend. Every hook
is behind the build-time `__GXT_MODE__` constant and reaches gxt-backend only
through gxt-bridge, so the classic dists fold them away; the surrounding code
is main's.
- glimmer: base-renderer, renderer, templates/root.ts and unwrap-template.ts
call the bridge's glimmer host (gxt-backend's root template,
`renderComponent`, classic-root render / destroy / bounds, template
adapter) from whole `if (__GXT_MODE__)` blocks or a top-level ternary, and
assert that it is installed; a VM-free renderer state under GXT; GXT-aware
`template()`, internal components, Input, element helper,
scheduleRevalidate. root.ts keeps one side-effect-only import
(@glimmer/manager) that the native build's module order needs.
- metal: the single gxt cell behind `@tracked`, the GXT re-render trigger in
`notifyPropertyChange`, the CP re-entrancy guards, `set` hooks.
- routing: `Route#setup` / `teardownViews` record and drop the route's GXT
render state, `_setOutlets` renders through the GXT outlet machinery (and
skips main's incremental mid-transition passes, which have nothing new to
render there), QP / meta fixes; the classic route manager keeps a raw gxt
route template uninvoked, and the classic outlet component is registered
as the wrapper the GXT outlets render themselves.
- runtime / object / runloop / application / views / template-compiler:
smaller GXT hooks (observer flush budget, proxy unknownProperty during
renders, settled PromiseProxy fast path, view-tree rebuild, GXT
`template()`); `visit()` asserts that GXT supports neither the serialize
nor the rehydrate render mode.
One classic change: the `{{debugger}}` callback moves from the VM opcode
module to @glimmer/runtime/lib/debugger-callback.ts (the opcode module
re-exports `setDebuggerCallback` / `resetDebuggerCallback`), so a renderer
without the VM calls the same callback.
Main fixes since the first version of this series: the hooks no longer
swallow errors (an error of the DOM sync at the end of a run loop is thrown
from the run loop's last step instead of being dropped; `template()`'s
implicit scope keeps only the expected eval failures); a render throws only
its own render errors; `renderComponent` of a plain-object definition runs
its custom manager, and a root with a `hasSubOwner` manager renders with the
manager's owner; a Glimmer component used as a route template is created by
its manager. Most of the GXT code these hooks used to carry inline now lives
in gxt-backend (the moves were c741f0d..b1bf210 on
lifeart:gxt-pre-squash-b1bf210aef).
The per-fix history is on lifeart:gxt-pre-squash-b1bf210aef.
Folded in after the squash: the moves out of template-compiler, routing,
metal and views utils, and the removal of the classic leftovers (the
side-effect-only imports of renderer.ts, the input's GXT methods, the GXT
locals of ClassicRootState / set / tracked / ArrayProxy, barrel imports in
place of main's deep paths). The classic prod dist is main's plus the
`{{debugger}}` callback module and one alias line.
The history of these follow-ups is on lifeart:gxt-followups.
Adapted in the rebase onto main 493985e: main's rendering tests call
`BaseRenderer#render` directly, and under GXT it hands the render to the
glimmer host (`renderRoot`), as `renderComponent` does. `CoreView` stays
main's: gxt-backend sends the lifecycle events itself.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- scripts/gxt-test-runner: the Playwright + QUnit runner (one module per page load, baselines, smoke list, native allow-list and skips, contract tests, baseline gate; the gate's `--subset` mode compares only the modules a shard ran). `--shard` splits the sorted module list round-robin, and merge-runs.mjs merges the shard outputs into the summary an unsharded run writes, so CI gates the merged run and a baseline can be regenerated from it. The native allow-list follows main's test files; - index.html: the GXT test-page setup (behind `__GXT_MODE__`) on main's page; native.html: the gxt-native test page, which also runs main's `setupTestHelpers()` and loads QUnit by package name like index.html; - internal-test-helpers: GXT compile through the compat compiler, the GXT teardown harness (a module-level value, so classic test builds keep none of it; it throws what destroying a modifier or a helper throws), GXT artifact stripping in equalTokens / equalInnerHTML / qunit-dom `hasHtml`, GXT run helpers (`runTask` / `runAppend` sync the DOM after their run loop and rethrow its errors); the router test case exposes its root element to GXT; - integration-tests: the GXT rehydration delegate (two plain GXT renders, the server one not interactive; serialization-dependent tests skipped with a reason) on main's `RenderDelegate` / `RenderHandle` interface; the tests that check Glimmer VM internals (list-update steps, serialization markers, dynamic variables other than outletState) skipped under GXT with a reason; GXT marker comments (`<!---->`, curried-component boundaries) ignored by the assertions; the jit delegate makes a curried component the way the GXT `component` helper does; no empty component sub-modules under GXT. The per-fix history is on lifeart:gxt-pre-squash-b1bf210aef. Folded in after the squash: native-skip.json drops the entries that no longer match a test after the rebase onto main (tests main moved, rewrote or renamed). The history of these follow-ups is on lifeart:gxt-followups. After the third rebase onto main, which moved fragment-components-test.js into components/classic/ and renamed its module, the native allow-list names the new path and the module's 12 native-skip.json keys carry the new module name. Adapted in the rebase onto main 493985e: the rendering and router test cases render through main's Glimmer root, under GXT through `gxtHarness.runRender`, which keeps the semantics of `runAppend` (one render pass id, the DOM sync after the run loop and its errors), and `runAppend` starts a render pass for a classic `-top-level` root. Main's two new rehydration-stats tests of a curly-invoked Glimmer component are unsupported in the GXT delegate, as their classic siblings are. native-skip.json drops the 18 skips of tests that main rewrote from classic components to a custom-managed component under the same names, and two skip reasons name the `{ROOT}` label. The README's smoke counts are those of the rebased suite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GXT branches in shared tests (cache-counter parity, debug render tree,
resolver cache, helper registration through the runtime `hbs`), GXT skips
(hot reload, `{{mount}}`, classic `append`, the rehydration render mode, the
compile-options meta test), and renames that keep the names `array` and
`element` out of module scope in the single-chunk GXT test bundle.
New regression tests, which run on both backends, for the behaviours the
compat backend had wrong: has-block-params in nested blocks; the
(hash)/(array) identity tests (helpers/gxt-hash-array-identity-test.js,
because main rewrote array-test / hash-test as .gjs); attributes are not
arguments and URL attributes forwarded through `...attributes` are
sanitized, also on update (angle-bracket invocation, attribute bindings,
Input, LinkTo); a null/undefined/false attribute or class is removed and a
triple curly in an attribute renders its value (content-test); `<template>`
components invoked by name and classes with a class-body template
(template-tag-by-name-test.gjs, class-body-template-test.gjs,
template-tag-*-test.gjs); a `{{component}}` mustache passed as an argument
is curried; each `{{on}}` and each custom modifier on one element is its own
instance; renderComponent of a custom-managed plain-object definition, and a
root that updates in place, evaluates each binding once and throws only its
own errors. The named-outlets assertion test runs again (its method lacked
`@test`), with the column the compiler reports.
The rebase onto main keeps main's rewritten arranged-content tests, with the
`array` rename on top.
The per-fix history is on lifeart:gxt-pre-squash-b1bf210aef.
Adapted in the rebase onto main 493985e: contextual-components-test.js
is main's file plus this series' new test, and mount-test.js keeps the
GXT skip of the whole file around main's version; the runtime resolver
cache and template factory tests take main's expectations with GXT
branches (no root definition, one `-top-level` hit under the debug
render tree), and main's copies of the old tests in components/classic/
keep the GXT branches the old tests had; angle-bracket-invocation-test.js
imports `ClassicComponent` again for the two tests that use it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GXT smoke (contract + sharded smoke modules, each shard gated against the compat baseline), dual-build, gxt-native (modern-app boot, package, native suite against its baseline) and full compat workflows. The full and native suites run in parallel shards (5 and 4), and a combine job merges them and gates the merged run with baseline-gate.mjs: the verdict is "no test red that the baseline lists green, no baseline module missing, no timeout or harness error", so known reds do not fail a run. The setup action's optional Playwright install; the ember-source-gxt scenarios and the v2 app template for them, whose boot smoke covers build-time .gjs components (forwarded attributes, blocks, a class-body template, set() on a plain field, decorators without a global) and compiles with Glimmer's whitespace rules. The per-fix history is on lifeart:gxt-pre-squash-b1bf210aef. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test-results/gxt-baseline.json (compat, 10135/10376, 1037 modules) and native-baseline.json (native, 7190/7203, 684 modules), regenerated from the merged sharded runs of the GXT full and gxt-native workflows on this series rebased onto main; and docs-internal-gxt-packaging-design.md. Against the baselines before the rebase, every change comes from main or from the test files this series converted, and no test turned red: - main deleted the Enumerable and MutableEnumerable test modules, added `@each support`, `array computed macros deprecation` and (compat only) `DataAdapter releaseMethods`, all green, and changed the test or assertion counts of 17 existing modules in each suite (deprecation expectations, part of `EmberArray.@each support` moving to `@each support`, one more `Ember.A` test); - main renamed one known red, `custom ember array properties rerender when updated` in Helper Tracked Properties (its key now carries `@test` twice, so its name starts with `@test`); it is still red under compat; - the compat baseline gains the eight QUnit modules converted from vitest (`Compat: utils.ts` and its six children, `@glimmer/component-gxt: reactive identity`), all green. The second rebase onto main changes only main's five new tests of a cache's tag (emberjs#21664) in `@glimmer/validator: tracking > tracking cache`, which both suites run (QUnit also counts them in the parent module). Four are green. `it keeps its tag if it consumes the same tags in the next run` is a known red in both baselines: the GXT validators do not reuse a cache's tag when the cache consumes the same tags again, so an outer frame sees a new tag after each re-run. The third rebase onto main moves six classic-component test files into components/classic/ and renames their modules (` (classic component)`); their tests and results are unchanged. Main's rewrite of the positional-params tests of `Component Manager - Curly Invocation` to a native class whose manager's `updateComponent` is a no-op turns three of them into compat known reds: the compat custom-manager arguments do not update a native component's positional params unless `updateComponent` writes them (native passes them). The fixes folded in with this rebase turn 9 compat and 14 native known reds green, among them `custom ember array properties rerender when updated`. The note in the compat file lists the run; the known reds are the failing tests listed per module. A test that fails only because its expected error message is Glimmer's exact text is a known red: the GXT backends throw their own messages. The per-fix history is on lifeart:gxt-pre-squash-b1bf210aef. Folded in after the squash: the native baseline after the skip prune. The history of these follow-ups is on lifeart:gxt-followups. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Glimmer's validator now reuses a cache's tag when a re-run of the cache
consumes the same tags in the same order (`combineForCache`), and a frame
that consumed only a cache's tag ends with that tag. Both GXT validators
built a new combined tag on every run, so a frame that read such a cache
got a new tag each time ("it keeps its tag if it consumes the same tags
in the next run" failed in the compat and native suites).
- native: a cache's frame keeps its previous combinator when the frame's
tracker holds the same leaves in the same order. A frame keeps the last
combinator consumed into it when its tracker holds exactly that
combinator's leaves, in order (its frame reads only that cache). gxt
trackers still hold only leaf cells.
- compat: a cache keeps a tag of the tags its fn() consumed, reused while
they stay the same and in order. A manual frame that read the cache and
holds exactly those tags ends with it. The cache still forwards its
tags, not its tag, to the frames and gxt trackers reading it.
The reused tag has the same members as the one it replaces, so revisions
and invalidation are unchanged; only its identity is kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A component whose custom manager creates it with its arguments and does
not replace them in `updateComponent` (a native class with a no-op
`updateComponent`) kept rendering its first arguments after a change
("positional params are updated if they change" in its three variants).
The component reads its arguments through the live `named` / `positional`
objects of `buildCustomManagedArgs`, which memoize each read per render
pass. An argument change re-renders without a new render pass, so the
component's reads, and the recompute of a computed property over them,
returned the memoized first values. A computed property over
`args.positional.[]` / `args.positional.0` / `args.named.x` also stayed
valid: its chain tags are the tags of those keys of the live objects, and
nothing dirtied them. Glimmer's args proxy reads the argument references
and tags those keys with the references' tags.
On a change, the custom-managed arg effect now hands the values it read
to the memos, and dirties the tags of the changed keys that exist (an arg
propagation, as for other component arguments), before it dispatches the
manager hooks and recomputes the dependent properties. A manager that
replaces `args` in `updateComponent` behaves as before.
A gxt-backend test covers the named arguments of such a component, in a
plain getter and in a computed property, across two changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erts
In DEBUG, a `set()` or `@tracked` write during a render of a member that
a template of the same render read through a path now raises the
backtracking assertion with Glimmer's message: the components that were
rendering at the read and the template path ("`content` was first used:
- While rendering: -top-level x-outer this.wrapper.content"). That covers
`{{this.wrapper.content}}`, `{{@wrapper.content}}`, and an argument such
as `value=this.wrapper.content` that a template-only component renders.
Before, a child of a Glimmer component could write a member the parent's
template had rendered with no assertion, and a tracked member asserted
with "(unknown)" in place of the render tree.
The check (`checkBacktracking`) only knew the objects that classic
components render (`markTemplateRendered`), and it named the path after
a classic component's property. A Glimmer or template-only component
records nothing, and a template compiled at runtime reads a member with
no tap that reports the read.
Such a template reads `this.wrapper` through the proxy of its render
context and `.content` through the tracking proxy of the object that
read returned. The tracking proxy reads the member's cell, as Glimmer's
`getProp` consumes the member's tag. The render pass now records those
member reads in the frames where Glimmer consumes them (validator.ts:
inside a tracking frame, not untracked, not at commit). Each record
keeps the components on the parent-view stack, with a custom-managed
component named as it was invoked, and the path. `checkBacktracking`
asserts on a write of a recorded member when no classic component
rendered the object; the classic case keeps its message. compile.ts
still adds the template-only components to the tree. The read `set()`
makes of the value it replaces is not counted. The records are dropped
with the pass. All of it is DEBUG-only.
A member of a plain object (`{{this.person.name}}` with
`person = { name }`) is not covered: it is a plain property read that no
proxy sees.
Adapted in the rebase onto main 493985e, where a rendering test
renders through a Glimmer root labeled `{ROOT}`: the message names the
root as the other backtracking messages do (`renderRootLabel()`: `{ROOT}`
for a component root, `-top-level` for a classic one), and the render
pass that now brackets a component root's render holds the member
records. validator.ts imports both bridge members (`renderRootLabel` and
`installBacktrackingPart`).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both baselines are the summaries of the GXT full and GXT native runs of this series rebased onto main 493985e. - compat: 10195/10433 green across 1053 modules (was 10141/10377 across 1038). Main renders the template of a rendering test through a Glimmer root, moved classic-component tests into components/classic/ (new module names, the same results) and added tests. All of them are green except 4 known reds that main added: "the render stack of a backtracking message starts with -top-level" (a classic root's backtracking message), "{{outlet}} in a component asserts" (Route manager - {{outlet}} under a non-classic level) and the two loose raw-block tests of the jit suite's "Inherited from Handlebars (legacy behavior): raw blocks". Main's two new rehydration-stats tests of a curly Glimmer component are skipped as unsupported, as their classic-component siblings are. The backtracking assertion for a member that a Glimmer component's template read turns 3 known reds green: "when a shared dependency is changed during children's rendering" and its "(tracked)" variant (curly components), and "asserts when a shared dependency is changed during rendering, and keeps original context" (template-only components). 222 known reds remain (was 220). - native: 7187/7206 green across 684 modules (was 7192/7203). Main's 3 new tests are green. The 18 tests that main rewrote from classic components to a custom-managed component with positional arguments run again (their skips named the classic versions): 10 are green and 8 are known reds. Native does not update the positional arguments of a curried custom-managed component (4), renders nothing for such a component given positional arguments as an argument (2), and resolves a curried component invoked with arguments in a mustache as a helper (2). 19 known reds remain (was 11). No test that main left unchanged went from green to red. The smoke counts of the runner's README follow the new baseline: one known red, 274/275 tests and 1,834/1,835 assertions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lifeart
force-pushed
the
glimmer-next-fresh
branch
from
November 6, 2026 01:43
5d48546 to
e4f2cd0
Compare
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
In the Ember dialect a component's named args are emitted as
`$_args({ key: () => value }, …)`, where reactive values are zero-arg
getters the arg-access layer (ArgProxyHandler / the DEV $_args getter)
invokes on read. The layer historically invoked ANY function-valued arg,
so it could not tell a compiler-emitted reactive getter apart from a
function that is itself the arg value — a host-forwarded action or a
{{fn}}/helper bound result. Those got invoked on every read
(emberjs/ember.js#21340, "Components test: contextual components ::
renders with dot path and updates attributes": 4/8/15/22 instead of
1/1/2/3).
The compiler now wraps the reactive getters it emits in buildArgsObject
with `$_argThunk`, and the arg layer invokes ONLY branded thunks; every
other function value passes through unchanged. The marker is a plain
string-keyed property (like `$_nt`), minification-proof; `isArgThunk` is
exported so a host can retire its `fn.length`/`fn.prototype` shape
heuristic.
Gated to the Ember dialect (WITH_EMBER_INTEGRATION + IS_GLIMMER_COMPAT_MODE):
that is the only layer that forwards bare function values into gxt args,
so standalone gxt keeps byte-identical codegen and its
invoke-every-getter arg semantics.
- src/core/dom.ts: $_argThunk / isArgThunk; brand-only invocation in
ArgProxyHandler + the DEV $_args getter (gated).
- compiler: emit $_argThunk around reactive arg getters (reactiveGetter /
zero-arg arrow / $__cached memoized getter) in buildArgsObject.
- register ARG_THUNK in both symbol registries, GXT_RUNTIME_SYMBOLS.
- tests: emission + gate + runtime predicate + AOT auto-import guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
A dynamic modifier whose name is a `this.*` path was emitted as a plain member read (`$s.dyn`). On an EmberObject with a non-@Tracked `dyn` that read taps no tag, so the modifier formula is treated as const and a later `set(ctx, 'dyn', X)` never re-installs the modifier — it is never destroyed/created. Under EMBER_PROPERTY_TAGS the modifier-name path now goes through the same `$__tag(obj, "key")` chain as a value path (buildPathExpression → buildEmberMemberChain), so resolution is reactive. Standalone keeps the plain read. Unblocks the native emberjs/ember.js#21340 test "Basic Custom Modifier Manager: 3.22 :: destroys a dynamic modifier that was set after the initial render". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
The compiler reduced a thrown parse error to a one-line `message`
(`parseError.message.split('\n')[0]` + token/expected hints) and
discarded the original, so a host could only re-wrap the reduced string,
losing the `@glimmer/syntax` SyntaxError's code-frame and position. Both
the compiler and the Ember host pin the same `@glimmer/syntax`, so the
original error's message/code-frame/position already match what the host
would produce.
Preserve it on `CompilerError.originalError` (threaded through
`enrichError` from all three catch branches) so the host can rethrow it
unchanged when there is a single error. Synthesized diagnostics (unknown
binding, W-codes) leave it undefined.
Unblocks the emberjs/ember.js#21340 compile-time error group
(`compile errors` + `general syntax errors`, ~21 tests) once the Ember
host rethrows `originalError`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
`(mut this.foo)` / `(mut @bar)` pass the path as a string the host parses to build a setter, but a lexical JS head (`state.arg1`, a block param) cannot be resolved by name at runtime, so it was dropped and the host's `mut` asserted "You can only pass a path to mut". A lexical-head mut (head is a known binding, dotted tail) now lowers to the existing `(mut (get obj key))` machinery — `__mutGet(() => head, "rest")` — a parent getter + key the host writes through with `set(parent, key, v)`. The parent getter is buildValue's own read of the head, so it yields the real parent object whatever the binding's compiled shape, and a dotted tail is passed as __mutGet's dotted key. this./@ heads and non-compat builds are unchanged. Mirrored in the SubExpression (visitors/index.ts) and mustache (visitors/mustache.ts) handlers. Unblocks the emberjs/ember.js#21340 tests "Helpers test: {{fn}} :: can be used on the result of `mut`" and its "… with a falsy value" variant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
The `(mut <lexical path>)` lowering from the previous commit built its
`__mutGet` helper with
`helper('__mutGet', [parentGetter, literal(restKey)], range, pathRange)`,
omitting the `named` Map argument. `range` (a SourceRange) therefore landed
in the `named: Map<string, SerializedValue>` slot, so at serialize time
`value.named.values()` threw (reported as compile error E001) and the three
lexical-head lowering tests in mut-lexical-head.test.ts failed with a stray
errors entry. It is also a `tsc` type error (TS2345: SourceRange not
assignable to Map), which failed the `tsc &&` step of `pnpm dev:playwright`,
so the Playwright webServer could not start (exit code 2) — the same root
cause.
Pass `new Map()` as the named args, matching the sibling `(mut (get obj key))`
__mutGet calls, in both the SubExpression (index.ts) and mustache (mustache.ts)
handlers.
Part of the fix for the emberjs/ember.js#21340 tests "Helpers test:
{{fn}} :: can be used on the result of `mut`" and its "… with a falsy
value" variant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
DRAFT. The root template is built detached and inserted only after the whole template evaluates, so an in-element whose destination is a sibling rendered earlier in the same render (document.getElementById) sees null and the host's _emberInElement asserts. gxt already defers in-element to the mount hook for the native runtime, but the Ember compat path resolves the destination during build, so the fix must make the root render stream nodes into their parent as they are built. This commit adds the target-behaviour regression spec as todos; the streaming implementation is described in the PR and relies on CI / the emberjs/ember.js#21340 test "[integration] jit :: Application test: debug render tree :: in-element in tree" for validation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
…ction Adds `HOST_HOOKS.onCellRead(object, key)`, fired from the property-tag tap `$__tag` for every reactive read of an object/function head — the compiler-emitted equivalent of Glimmer's "consumed tags". A host snapshots the (object, key) pairs read during a render pass and raises the backtracking assertion when a later non-tracked set() dirties one of them — detection compat silently lost for Glimmer (template-only) components, whose reads were never recorded (manager.ts checkBacktracking only fired for template-rendered objects, which handleCustomManagedComponent never populated). Scope: this is the gxt-side SEAM only. It is hook-presence gated (a single branch, zero-cost when absent) and installed only by a DEBUG host, so it is a no-op for standalone gxt and every existing test. The snapshot + assertion logic lives Ember-side and is validated there; the three emberjs/ember.js#21340 backtracking tests that never see the assertion ("Components test: curly components :: when a shared dependency is changed during children's rendering", "Components test: dynamic components :: component helper emits useful backtracking re-render assertion message" and "Components test: template-only components (glimmer components) :: asserts when a shared dependency is changed during rendering, and keeps original context") go green once the Ember consumer lands and pins a gxt release carrying this hook. Fired before the getProp delegation so the read is captured whichever tag path runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
gxt rewrote `@index` to `@identity` at compile time (W003), so the list
keyed rows by VALUE and a reorder moved/re-created rows — `@index` yielded
the wrong index and nodes "changed" on reorder. Now:
- compiler (serializers/control.ts normalizeEachKey): emit `@index`
verbatim, drop the `@index -> @identity` rewrite + W003.
- runtime (control-flow/list.ts):
- setupKeyForItem: `@index` keys each row by its POSITION, so a reorder
leaves every key in place — the diff keeps (never moves) the row node
and rebindReusedRow updates its bound item.
- the index block param is a const formula equal to the row's fixed
position (not the bound item's current position).
- exclude `@index` from the property-name stale-key eviction (a
positional key has no `item['@index']`, which would wrongly evict
every reused row on reorder).
Standalone gxt has no body-proxy, so a reused row's displayed item only
updates through the host `rebindEachItem` seam (fired here); node identity
+ positional index are the standalone-observable core. tests: compiler
passthrough + runtime (nodes stay vs @identity moves, positional index,
rebind seam fires).
For the emberjs/ember.js#21340 tests "[integration] jit :: Updating ::
The each helper yields the index of the current item current item when
using the @index key" and the native "Syntax test: {{#each}} keys and DOM
identity :: @key keys items by position".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifeart
added a commit
to lifeart/glimmer-next
that referenced
this pull request
Nov 6, 2026
Native `{{#each-in obj key=KEY}}` forwarded `key=` but it was resolved
against the lowered `[key,value]` PAIR, so `@key`/path keys resolved to
`undefined` (rows collapsed) and `@index` degraded to `@identity`. Now:
- compiler (visitors/block.ts + serializers/value.ts): a non-identity
`key=` is forwarded to `$__eachIn(obj, "key")`, and the lowered `each`
keys by `@identity` on the returned pairs ($__eachIn owns the keying).
`@identity`/default stays the historic 1-arg `$__eachIn(obj)`.
- runtime (helpers/each-in.ts): `$__eachIn(obj, key)` resolves the key
against each ENTRY (Glimmer toEachInIterator / Ember keyEachInEntries):
`@identity` keys by the entry (value change → new pair → new row);
`@key`/`@index`/path key by the entry key / position / resolved path
value, retaining the pair and updating its value in place while the row
key is unchanged. Per-mode caches keyed by the row key directly (object
or primitive, by reference — no String() collapse) so distinct key modes
over one object don't clobber.
Standalone gxt observes the KEYING (which rows kept/recreated); the
retained row's displayed value re-reads through the host body-proxy
(Ember), validated there. tests: unit ($__eachIn per-mode stability +
in-place update), compiler (key forwarding), render (native: path key
retains the row node on same-id replace vs @identity recreates).
For the five native emberjs/ember.js#21340 tests of "Syntax test:
{{#each-in}} with the key option" (the default value key, key="@key",
key="@index", a path key, and key="@index" over a Map keyed by objects).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed like keywords
Tests of what Glimmer does with a template's lexical names, where the GXT
compat backend rewrote the template source with regular expressions before
it compiled it. The classic build runs them too: its results are the
expected ones.
Runtime `template()` (runtime-template-compiler-scope-test.ts):
- with `scope: () => ({ this: state })`, text such as `(this one)` stays
text, and a `this` path passed as an argument (`{{shout this.cls}}`)
reads `state`;
- with `eval`, a word of text is not looked up through `eval` (a global
getter of that name is not read), and a helper named in PascalCase
(`{{Greeting}}`) is in scope;
- a block keyword that the scope shadows (`{{#unless}}`, `{{#each}}`) is
an invocation of the scope's component: its `{{else}}` block, positional
arguments and named arguments are the component's (named arguments, not
HTML attributes). The positional arguments are read by a component with a
custom manager, since a template-only component takes none.
Block params named like keywords (block-param-names-test.js):
- outside the block that declares `component`, `{{component "foo-bar"}}` is
the keyword;
- inside it, the block param is read as an argument (`{{concat "got "
input}}`);
- a block invocation of a block param named `input` keeps its `{{else}}`
block and named arguments, and evaluates the block param's value once.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ads `this`
With `template(source, { scope: () => ({ this: state }) })` the template's
`this` is `state`. Every `this` path now reads it: an argument
(`{{shout this.cls}}`), a hash value, a block's value
(`{{#let this.x as |y|}}`) and a block-form tag (`<this.Foo>…</this.Foo>`).
Text such as `(this one)` stays text.
Root cause: template-function.ts renamed the `this` head in the template
source, with `/(\{\{[#/!]?\s*|\(\s*|<\s*)this\b/g` → `__gxtExplicitThis`.
A regular expression over the source cannot tell a path from text. It
rewrote `(this one)` in text, and it missed every `this` that does not
directly follow `{{`, `(` or `<`, which then read the rendering context. It
also missed the closing tag `</this.Foo>`, which no longer matched its
renamed opening tag.
Replacement: `gxtExplicitThisTransform` (ast-transforms.ts), an AST plugin
that `template()` passes to the compile. On the parsed template it turns
each `this` path head and each `this`-headed tag into the alias that the
scope binds to `state`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ree variables
`template(source, { eval })` takes from the caller's `eval` the template's
free variables: the heads of its paths and component tags that no block
param in scope binds, as `getTemplateLocals` of @glimmer/syntax finds them.
A word of text, an attribute name or a comment is no longer looked up, so a
global getter of that name no longer runs. A helper or component named in
PascalCase in mustache position (`{{Greeting}}`) is now found.
Root cause: seven regular expressions over the source collected the names.
The last one took every lowercase word (`/\b([a-z][a-zA-Z0-9_]*)\b/g`), text
included. The others needed a lowercase name, or a PascalCase one after `<`
or before `.`, so `{{Greeting}}` was not looked up and the strict-mode
template raised "not in scope".
Replacement: `gxtTemplateLocals` (ast-transforms.ts) walks the parsed
template with its block-param scopes (`walkGxtScopes`). A name that is not
a plain JS identifier (`has-block`, a reserved word) is not looked up.
The template is parsed by the GXT compiler's own parser: gxt does not export
it, but hands it to AST transforms as `env.syntax.parse`, and
template-function.ts takes it from there once. The compat build gets no
second template parser.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lock
Some block params are named like a keyword that the compat backend lowers by
name (`component`, `input`, `textarea`, `mount`, `outlet`, `link-to`), or
have a name that starts with `-`. Inside its block, such a block param is
now read as the block param everywhere: an argument
(`{{concat "got " input}}`), a sub-expression, a block invocation
(`{{#input}}`) and a tag. Outside the block, the name is the keyword again
(`{{component "foo-bar"}}` after the block).
Root cause: `compile()` (ember-template-compiler.ts) renamed these block
params on every compile, with `/\sas\s*\|([^|]+)\|/g`. It then rewrote
`{{NAME` across the whole template, with
``new RegExp(`(\{\{\{?)(?!#|/)(\s*)${name}(\b)`, 'g')``. Three problems
followed:
- the rename leaked outside the block, so `{{component "foo-bar"}}` read an
unbound name;
- `\b` also matched `{{input-foo}}`;
- only mustache heads followed the renamed block param, so
`{{concat "got " input}}` read an unbound name.
`precompileTemplate()` and `template()` did not rename at all.
Replacement: `gxtBlockParamNamesTransform` (ast-transforms.ts) renames the
block param and every read of it in its scope, on the AST. compile.ts runs
it for every runtime compile, before the user's AST plugins and the dialect
transforms, as the source rewrite ran before parsing. compile.ts gives the
dialect transforms the block-param names under their new names.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and blocks
The scope can shadow a block keyword (`if`, `unless`, `each`, `each-in`,
`let`, `with`, `input` or `textarea`). Then
`{{#each items name="b"}}A{{else}}B{{/each}}` invokes the scope's value as a
component, with its positional and named arguments, its block and its
`{{else}}` block, and evaluates each argument once.
Root cause: compile.ts (`if` to `with`) and `compile()` in
ember-template-compiler.ts (`input`, `textarea`) rewrote
`{{#NAME …}}…{{/NAME}}` into `<Alias …>…</Alias>`, with
``new RegExp(`\{\{#(${name})(\s[^}]*)?\}\}`, 'g')``. This had four effects:
- arguments became HTML attributes (`items=""`, and `name="b"` instead of
`@name`);
- `{{else}}` inside an element is a parse error;
- `[^}]*` stopped at a `}` in an argument;
- the pattern matched in comments too.
For a block param named `input`, `compile()` also turned
`{{#let EXPR as |input|}}` into `{{#let EXPR EXPR as |input Alias|}}`, which
evaluated EXPR twice.
Replacement: `gxtShadowedKeywordTransform` (ast-transforms.ts) renames the
head of each block keyword that the scope shadows to the keyword's alias,
unless a block param in scope rebinds the name. compile.ts binds the alias
to the same value. The positional arguments become the `__pos<i>__` /
`__posCount__` arguments that the GXT compiler passes for a curly
component's positional params, since it drops a lexical block's positional
params. The GXT compiler then invokes the alias with its block, its
`{{else}}` block and its named arguments. A block param named `input` or
`textarea` is renamed in its block (previous commit), so its block
invocation needs no copy of the `{{#let}}`. Both source rewrites are
deleted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GXT rendering backends for Ember (opt-in preview)
Summary
This PR adds
@lifeart/gxtas an opt-in, build-time rendering backend forember-source. It comes in two flavors:GXT_MODE=true(Vite dev and test pages),EMBER_RENDER_BACKEND=gxt(rollup dists)@glimmer/*rendering and reactivity modules are replaced bypackages/@ember/-internals/gxt-backend/.EMBER_RENDER_BACKEND=gxt-native(Vite and rollup)gxt-backend/native/. Ember tags are gxt cells, so there is one reactive system.The default build is classic and unchanged in behaviour. GXT code in classic packages sits behind the
__GXT_MODE__build constant, which the classic and native builds fold tofalse.bundle-size-check classicfails if live GXT bridge code reaches the classic dist. The classic suites give the same results as onmain, plus the tests this PR adds to shared test files, which also pass on classic. The classic dists are not byte-identical tomain's; the backwards compatibility section says why.Opting in is a build-time choice; this PR publishes nothing.
scripts/build-gxt-native-package.mjs --pack <dir>buildsember-source-gxt-native. An app installs it asember-source("ember-source": "npm:ember-source-gxt-native@<version>") and adds the Vite presetemberGxtNative()fromember-source/vite. It supports Vite only: no ember-cli and no@embroider/vite.scripts/build-gxt-package.mjsbuildsember-source-gxt(dist-gxt-package/). TheemberSourceGxtsmoke scenarios install it asember-source.Native scope. In scope:
@glimmer/componentand template-only components; strict.gjs/.gtstemplates and loose resolver templates; the router, outlets andLinkTo; helpers and modifiers, including custom managers;@tracked,@cachedand tracked collections; services,renderComponent, runtime compilation through@ember/template-compiler, and the debug render tree.Out of scope: classic
@ember/component,<Input>/<Textarea>/{{mut}}two-way bindings, ArrayProxy / ObjectProxy content reactivity,{{unbound}}, engines /{{mount}}, SSR, and.hbsfiles. Using a classic component or<Input>/<Textarea>throws. The native test run skips 286 tests, each with a reason inscripts/gxt-test-runner/native-skip.jsonor in the test source.Current status
The numbers are from the fork's CI on the tree of the head,
e4f2cd0e6d, with@lifeart/gxt0.0.86. The runs tested5228fb89ee, which adds to the head only the CI trigger lines and a scratch workflow that compares the classic dists withmain's. The CI section explains how these runs are made.Comparing each number with earlier descriptions would mix this PR's changes with
main's. Since the previous description's head5d4854653d, these tests changed:1a56745208: 3 compat tests: "when a shared dependency is changed during children's rendering" and its "(tracked)" variant (curly components), and "asserts when a shared dependency is changed during rendering, and keeps original context" (template-only components).mainadded or rewrote:mainmoved classic-component tests intocomponents/classic/(new module names, the same results), kept copies there of tests whose harness it changed, and added tests, such as eightInherited from Handlebars (legacy behavior)modules. All pass except 4 new known reds (see the known gaps) and the known red "renders with dot path and updates attributes", which now fails twice: as its classic copy and asmain's Glimmer rewrite.main's 2 new rehydration-stats tests are skipped, as their classic siblings are.mainrewrote 18 tests that native skipped as classic-component tests to a custom-managed component with positional arguments, under the same names. Their skips are dropped: 10 pass and 8 are known reds.main's 3 new native tests pass.So compat went from 10,141 / 10,377 to 10,195 / 10,433 tests and from 1,038 to 1,053 modules, and native from 7,192 / 7,203 to 7,187 / 7,206. No test that
mainleft unchanged went from passing to failing; the adaptations of the fourth rebase fix those thatmain's new harness and deprecations broke at first.Tests
test-results/gxt-baseline.json.baseline-gate: OK.[integration]modulesGXT …modules and 48 tests in the two modules converted from vitest (Compat: utils.tsand@glimmer/component-gxt: reactive identity), all added by this PR and all greenbaseline-gate: OK.mainhas 9,295 / 9,274 / 21 / 0; the 91 extra tests are added to shared test files by this PR.main: 9,131 / 9,089 / 42 / 0). Two of the 91 added tests are development-only.main: 9,288 / 9,263 / 25 / 0)main's new publint and attw check) pass.emberSourceGxtjobs (compat package boot and consumable)ember-source-gxt-nativeplus the npm@lifeart/gxt0.0.86 tarball, installed in a fresh app outside the repo: 15/15 acceptance checks; prod and dev each boot 17/17; the prod bundle is byte-identical to the harness measurement (74,660 br).main's counts are from its own CI run on493985e956(emberjs/ember.js run 41671740543). Its one red job, a smoke app whose browser did not connect to testem, is an unrelated flake. The differences, +91 (+89 in the production builds), are this PR's tests in shared test files, as on the previous base.The runner runs nested QUnit modules on their own as well as inside their parent module, so the compat and native totals count the tests of nested modules twice: 648 of the compat tests and 135 of the native tests.
Compat known reds and skips
Counting a nested module's tests once, 212 distinct tests fail:
node jitandserializationmodules). GXT renders browser DOM only.compile errorsand… keyword syntax errorsmodules, strict-mode names that are not in scope, and two keyword tests. GXT either accepts these templates or rejects them with its own message (see errors).baseline-gatereports 222 known reds, because it keys failures by module and test name, and two SSR parent modules list each failing test of their three child modules under one name.125 test rows are skipped. 21 of them are skipped on classic too. The other 104 are GXT-only skips, each with its reason in the test source:
GXT_UNSUPPORTEDinintegration-tests/lib/modes/rehydration/gxt-delegate.ts). The delegate fabricates no markers.integration-tests/lib/test-helpers/gxt-unsupported.ts): the list-update steps of{{#each}}swaps, serialization markers, and dynamic variables other thanoutletState.metaof a compile option, andvisit()with_renderMode: rehydration, which asserts under GXT.Bytes
The same modern app (
smoke-tests/modern-app) is built from each rollup dist (GXT native run 42054852391). The boot JS includes Ember, gxt and the app, compressed with brotli. Every variant passes 17/17 boot checks..gjs)Whole dist trees (every
.jsfile underdist/<mode>concatenated, then brotli; GXT dual-build run 42054851974), with their budgets inscripts/bundle-budgets.json:The budgets were last re-baselined at the measured size + 5 %, the method the file documents (
81060259b3, onlifeart:gxt-followups); each backend's_commentgives the measurements. The check fails above 5 % over budget. The head's trees are at 95.2 % to 96.1 % of their raw, gzip and brotli budgets.Since
5d4854653d, the prod trees grew by 171 (classic), 681 (compat) and 11 (native) bytes brotli, and the modern-app builds by 15 to 169. The classic code differs frommain's as before (see backwards compatibility), so its growth ismain's code; compat's ismain's code and the fourth rebase's adaptations. The backtracking commit changes only the dev trees: on5d4854653dit added 1,818 bytes to compat's, which grew by 3,731 in all.assert-native-distpasses for prod and dev: the native app closure contains no Glimmer VM module and no template compiler, and has a single reactive system. Its forbidden list,GXT_NATIVE_FORBIDDENinscripts/gxt-alias-map.mjs, also names the five compat Glimmer host modules ofgxt-backend. Of@glimmer/runtimeit allows onlylib/dom/sanitized-values,lib/dom/normalizeandlib/dom/props, Glimmer's own attribute code, andlib/debugger-callback, which imports nothing. Five entry points outside the app closure reach Glimmer VM packages: the@ember/template-compilerruntime-compilation entries,ember-template-compiler, and the@ember/-internals/glimmerindex.Runtime speed is not claimed beyond the krausest measurement below.
Memory (krausest, CI)
These numbers predate the squash and all four rebases, and were not measured again. Run 39096142262 built the krausest (js-framework-benchmark) app from the classic and native dists of the pre-squash commit
6b8675867f(tree9013968e8a, on the oldmaina9501d5d40), with npm@lifeart/gxt0.0.86; a scratch commit on top of it added the harness. The head's GXT code also has the lint fixes, the gated-code moves and the removal of the classic leftovers, which do not change GXT behaviour, and the later fixes and adaptations, which do and whose effect here is not measured. Medians of 24 reps, in MB (10⁶ bytes):With gxt 0.0.85, the same code measured 52.15 MB after creating 10k rows, 6.07 MB after clearing them and a leak of 0.657 MB per cycle (run 39081573945).
In run 39096142262, native's script time is lower than classic's on 8 of the 9 krausest ops, from −2.5 % on create10k to −75.8 % on update10th. clear1k is 8.9 % slower. create10k is close to parity: native was slower than classic there in run 39081573945.
How it was measured: one job on a GitHub-hosted
ubuntu-latestrunner, with Playwright's Chromium; each rep opens a fresh page per variant, in alternating order; every op is checked, and its DOM must equal classic's in the same rep. The heap isRuntime.getHeapUsage(used size) after gc; the leak is the slope over 5 create/clear cycles. The harness (scripts/krausest-bench.mjs,smoke-tests/krausest-app/and a workflow) is not part of this PR: it was moved out in3800cb4ae0, onlifeart:gxt-pre-rebase-68874e540b.What the 8 commits contain
How to review describes each commit. This section summarises by area the follow-ups and the 9 fixes folded into the 8 commits, the adaptations of the fourth rebase, and the 3 fixes on top.
SHAs in this section are commits of
lifeart:gxt-pre-squash-b1bf210aef, except in the parts marked after the squash (commits oflifeart:gxt-followups), third rebase (commits oflifeart:gxt-ember-easy-fixes, and79060b45c0oflifeart:gxt-ember-hard-fixes) and on top (commits of this branch). The fourth rebase parts are in commits 2–6, whose messages list them. Each fix commit message gives the root cause, and fixes that change behaviour come with tests that failed before them.e4f2cd0e6dtakes the baselines from the GXT Full and GXT native runs 42050816110 and 42050816407 of the same code. Everything below is compat unless it says native.Rendering correctness
c02e65b8f5,e49a0f17ba,0fefcbd515,ac92f3aa7a,2567a776db). Build-time.gjs/.gtstemplates do the same inside elements (6915dbdedf), and at their top level through gxt 0.0.84's content-binding hook (e463bd06f0).nullorundefinedis removed (972ddf69ee).classis an attribute, and classes merged through...attributesfollow Glimmer (c6355fb7b5). An attribute that names a property of its element is set through the property (2e4812f25c). A triple curly in an attribute renders its value (64bd42705d). A boundstyleis the style attribute with its text as written, not the text the CSSOM re-serializes, andnull,undefined,falseor''remove it (2b824a9b26, with gxt 0.0.85, compat and native).~strips whitespace, and an entity in an attribute value is decoded once (0e46f9b991, with gxt 0.0.85).sanitized-valuesin every path that writes a URL attribute, in development and production, in compat and native (f93c20f33e,ac68721b69,1ce514a0ab). Before, compat used outdated copies of Glimmer's lists, and production builds wrote attributes forwarded through...attributes,attributeBindings,LinkToor<Input>unsanitized.a0023cc485,42f32b7cf2,7c6547ed96).1a336f6b9c,3a5e2d1fc5).a5270a0181,d998d6eb75). A yielded curried value that changes renders its new component (51cdbcc1e1,6257592af4).(component …)in an argument or attribute position is a curried component (84dad24674). Invocation positionals are appended to curried ones (c0e84a86c4). A curried(modifier …)argument stays one value while its definition is the same (76a039ee5d).<this.x>(ea8a56928c,f1065adada,0904bf8e41,872f3ab467,d8ae7bbe7e). Third rebase: native{{get}}reads a property of a primitive, such as a string'slength, as Glimmer'sisDictallows (b9056d70f5).5e6bced0b5).{{else}},<:else>and<:inverse>are one block (a37eb299c5). A tag naming a block param invokes the yielded component (6438cd7a77). A block param in a quoted attribute is its value (83810390af).<@comp as |v|>passes its block and block params (47db2a5080). The whitespace between an invocation's named blocks is not a default block that replaces<:default>(cf1a6aa14c)..gjs/.gtscomponents. A class with a class-body<template>renders through its component manager (ba66721c72), as does a class with a registered template invoked lexically (7240def4a3). The nearest template of a class chain wins (76385ea3b9, compat and native). A<template>component invoked by name receives...attributes, renders its blocks, and does not get its attributes as arguments (0f0976c613,58984137b5,f4c40b7be2). Decorators in gxt-compiled files import decorator-transforms' runtime, so nodt7948global is needed (5100fa24dd,d874899199, compat and native).renderComponentruns the custom manager of a plain-object definition (06ca2f1fd6), so the[integration]suites, whose harness renders every loose template that way, render with a real context. A root updates in place instead of being re-rendered as a whole on any tag change (6f5611dab1), is a root of its owner's renderer (d78683b33d), renders before the cursor'snextSibling(7476e88553), accepts a<template>value (7c9095995c), uses ahasSubOwnermanager's owner (59a0e407ed), and creates SVG elements inside an<svg>(c4126ba773).main's rendering tests render each template as a root ofBaseRenderer#render. Such a root renders through the glimmer host'srenderRoot, at its cursor and withoutrenderComponent's clear and replace bookkeeping, inside a render pass, and with a classic root's context cells, so its function-valued and ArrayProxy-valued properties re-render when set. A root whose first render failed stays in its renderer until the renderer is destroyed (compat and native).{{#in-element}}follows its destination and leaves it with its block, as does a component inside it (555a6f15cf,d6c2ee0b9b).<svg>keeps the SVG namespace on update, and an SVG text child updates (073609e6a7).{{#each}}row renders the new object bound to its key (3109d6465f), and each object bound to it after that, also when the list grows at the same time (d049134601, with gxt 0.0.85).{{#each-in}}honourskey=(b608afded1).{{#each}}over a native iterable re-iterates when what it read changes (a392191922). An{{#if}}branch or{{#each}}row rendered on update resolves names in its template's owner (5d7fb77fd7).63cc148519). A modifier sees its element's content when it is installed (c0b147aa14), reads its current arguments (7e8bef68b4), updates in the owner its element rendered in (c81f8daf88), and is destroyed when its definition changes (9badc5318c).Component lifecycle
9b84fde952,fd04350576,81676cff0c).{{#if}}or{{#each}}reaches its modifiers. Modifiers are destroyed in the order their elements close (05b5302447,1083384678). Third rebase: so are native ones, which are queued and destroyed in that order at the end of the render pass or root teardown (fa74262259), instead of gxt's parent-first order.b5eda4885e) and classic components (c7ac744eb5,cf4f6349ba) were pooled per class, so an instance could be handed to another call site or{{#each}}row.406548a43e,ec4f1ae128).{{#each}}row that renders a component removes the component's nodes when it goes (eddfdcfa8a). A modifier is destroyed with its block however it was installed or updated (dc33bfa368). A content binding owns the helper it renders and destroys it when the value changes (841bbaba0a).sendCoreViewEvent: the listeners, then the method), somain's newEvented#triggerdeprecation does not fire for them; user code'striggercalls stay deprecated. A classic component destroyed with its block runswillDestroyElementwhile its element is in the document.Reactivity
5f939592a4). Writes indidUpdateanddidRenderrender until stable, up to the rerender limit, instead of stopping after three passes (03e0958b74).dirtyTagForreaches the DOM (d551ddebb1). It no longer reads the property it dirties (3c1b5da11a), and creating a property's tag no longer reads the property (b430e9d9d8). A content binding re-runs before the bindings its evaluation created (6ec3d03b1a).(hash)members, curried arguments, thefncallback, the arguments<Input>/<Textarea>do not use, the getters of a rendered component, a route template's controller getters, and a dynamic component's value (03d48b04c3,04b148cc76,9c5cc9b98d,a8029df1c2,50e2ddf5ee,9702662b7e,c338dbf8fe). Some eager reads remain; see the gaps. Third rebase: native{{hash}}is one object of stable identity with a lazy getter per member, so a yielded(hash x=this.model.x)updates, an unused member is not read, and a custom tag lets@computed('args.hash.key')follow it (20d2d745a1).(mut)and(readonly)values, and(mut @arg)writes through the argument (5256bf4be0). A dependent key through a component argument follows what the argument reads (c9376e3978). Template-only component arguments update whatever invoked them (5f3a7db10f,0f77b1e682). ArenderComponentroot argument is read as Glimmer'sgetPropreads it (958f1e8980).(fn …)result passed to a helper is not called at the argument site, because the results of internal helpers are marked as helper results, as those of user helpers are (ca628df3d3).updateComponentreads their new values (764861e503). Its reads were memoized per render pass, and nothing dirtied the tags of a computed property overargs.positionalorargs.named; the argument effect now seeds the memos and dirties the changed keys' tags before the manager hooks run.(mut)and(readonly)values without reading other properties of the value, so<LinkTo>renders a model that throws on unknown properties.createCache, and so@cached, re-computes when a gxt cell its function read changes, includingthis.argsof a Glimmer component (528cf966aa). A binding is evaluated once when it is created (d69fdb8eda). On top: a cache keeps its tag when a re-run consumes the same tags in the same order, and a frame that read only that cache ends with that tag, asmain's validator does since Reuse the combined tag of a cache that reads the same tags again #21664 (d06e8e1b65, compat and native). Revisions and invalidation are unchanged.cell()renders by the end of the run loop (6f5611dab1).set()on a plain field re-renders a.gjstemplate that reads it (ec57f0d104). Third rebase: a helper recomputes when an EmberArray argument changes in place: it is compared throughobjectAtandlength, as a native array is by its elements (78235805f4).ifwith no else value isundefined(c4668e4ecb);{{log}}logs again when its arguments change (132cabbc59); a track frame that consumed one tag ends with that tag (fbccd086b6).Errors
catchblocks, and more than 100 other catches that dropped the error or only logged it, were removed or converted, ingxt-backend/and in the GXT branches of classic packages. Each remaining catch rethrows (or handles one expected error, such as aReferenceErrorwhile scanning a template's implicit scope, and rethrows any other); or collects the error and throws the first one once the surrounding work has run; or, around a read that Glimmer would not make or where no caller exists (see the gaps), reports it in development withwarn(idember-gxt.dropped-read), which production builds strip, while the template's own read of the value still throws. A scan of everycatchingxt-backend/and in the classic files this PR touches finds none that drops its error unreported, apart from one inroute-managers/classic/manager.tsthat is identical onmain.dirtyTagForread the property it dirtied (3c1b5da11a); an argument value that is a function with parameters was called as an argument thunk (b5dbcb4534); the DOM sync at the end of a run loop dropped every error (59cd9c4dc3); a binding that threw re-ran in every later sync (27e195c3a4), and gxt 0.0.85 now ends such a pass itself, so the bindings it did not reach render on their next change (a6459d9380); LinkTo's class and href reactor leaked across applications (b02ed88554,6a88b0abdf).render,rerender(),runTask, a barerun()or autorun, or theset()that caused it (39d74cd176,0d68d0e89c,59cd9c4dc3,8a97df6031,36614af474,c6a667cb2c,119ab64625). A render throws only its own errors, not a stale error queued by an earlier one (18037c8e71). A template that the GXT compiler cannot compile throws instead of rendering nothing (4b363558f1). Third rebase: a custom modifier whose install throws keeps the output rendered before it: the error is collected and rethrown after insert, as Glimmer does (79060b45c0). A thrown value that is not anErroris rethrown at once, unchanged. Fourth rebase: so does a component root: an error of its commit phase keeps the render and is rethrown once the render is complete.0d54cc861d) and then removed (82bff4dd8a): they cost about 62 KB brotli in the development dist. Tests that differ from Glimmer only in error text, and tests that expect an invalid template to be rejected at compile time, are known reds.visit()with_renderMode: 'serialize'or'rehydrate'fails an assertion that GXT supports neither (d390108302).Debug render tree (Ember Inspector) and
{{debugger}}renderComponentrender is a root of its owner's renderer, andcaptureRenderTreelists it with live bounds (d78683b33d).05c9fc7adc). With gxt 0.0.84,<this.X>and<@X>nodes also get their path as the name (46841d4cef).{{#if}}block bounds and modifier nodes (Ember native: Glimmer block bounds for {{#if}} + Inspector modifier nodes lifeart/glimmer-next#266). Third rebase: native names arenderComponentroot{ROOT}, and a component bound through a strict-modescopeby its binding name, as compat does (a41caf005a).{{debugger}}calls Glimmer's debugger callback each time it renders, with the template's self (nullfor a template-only component) and agetforthis,@name, block params and paths (cc5b59558c, with gxt 0.0.85'sdebuggerhook, compat and native). The callback state moved from the VM opcode to@glimmer/runtime/lib/debugger-callback.ts. Third rebase: in a template-only component the callback getsnull: the template-only mark is found through the render proxy (2bc65d6641). One of its tests still fails (see the gaps).Routing
getRouteWrapper) renders its routes, and a classic route template gets the@outletargument (7040e46e9e).this(f921fb272b).345e2a6969).8a97df6031).Development assertions
81d10d84e9). The message text differs from Glimmer's.set()or@trackedwrite of a member that a template of the same render read through a path ({{this.wrapper.content}},{{@wrapper.content}}, orvalue=this.wrapper.contentrendered by a template-only component) asserts with Glimmer's message, which names the components rendering at the read and the path (1a56745208). Before, the check knew only the objects classic components render, and a Glimmer component's template recorded nothing. The render pass records the member reads the tracking proxy sees, in the frames where Glimmer consumes a tag. A plain object's member is not covered (see the gaps).{ROOT}and a classic root-top-level, asmain's do.ec796c6bff).{{on}}and{{fn}}assertions name the argument, as inWhile rendering: this.foo(92fda285c4, with gxt 0.0.85, compat and native).{{attrs.x}}fails Ember's assert-against-attrs assertion, with its message and source location (1beed434a9), not a plainError.Memory (native)
{}that the compiled template passes on every evaluation, one per call site (ecd9165aa4).installandupdateare prototype methods. Before, each modifier was an object with two closures of its own (19f633b32e).gxt-native | retained call-site shapes, has 4 tests.Test harness
@glimmer-workspace/integration-testsassertions ignore GXT's curried-component boundary comments and its trailing block anchor (1ea465f836,96c0216737).49746e696e,9d67d6fb55,2a65871b22,59cb554612).componenthelper does, and the GXT rehydration delegate renders the server side non-interactively, with its components and modifiers registered (597fbf26e5,a1f5f22b7d).9553e20b02).Compat: utils.ts(the same 44 cases) and@glimmer/component-gxt: reactive identity(4 tests) (5ec62d6896).48eb40aed0): now 275 tests and 1,835 assertions in 14 modules, with one known red.native-skip.jsondrops 45 entries that matched no test after the first rebase (ca5c72e5c9), and two entries get the new names of their tests, so that every entry matches a skipped test (432e968543).main's Glimmer root, under GXT throughgxtHarness.runRender, which keepsrunAppend's semantics (one render pass id, the DOM sync after the run loop, and its errors). The native test case exposesthis.root. GXT tests runmain's deprecatedA()andObjectProxy.createinsideignoreDeprecation, or expect the deprecation.CI
scripts/gxt-test-runner/merge-runs.mjsmerges the shards into the samegxt-summary.jsonandgxt-last-run.jsonas one unsharded run, whichbaseline-gate.mjsgates. Each workflow now takes about 6.5 minutes instead of 20–27 (16a3c98dbf).GXT Fullverdict comes frombaseline-gate.mjs, so known reds no longer fail it and file an issue every night (ddcb06fb46).Build and packaging
ember-sourceno longer depends on@lifeart/gxt. The repo pins gxt once, as a rootdevDependency, and the generatedember-source-gxtandember-source-gxt-nativepackages declare that exact pin as a dependency (b5f3a21daa).bin/benchmark/run.mjsis back tomain's 60 s per-sample timeout (0ce2abe736).pnpm lintand both roottscprograms (pnpm type-check, in both decorator modes) now covergxt-backendand@glimmer/component-gxt: ESLint went from 376 problems to 0 andtscfrom 497 errors to 0 (5174ffe5e1), mostly by mechanical fixes. The exceptions are targeted:ember-local/no-barrel-importsis off in the two trees, because the GXT builds alias exact barrel specifiers; there are 34 per-line ESLint disables and 33@ts-expect-errorlines, each with its reason, and no@ts-nocheck;tsconfig.jsongains 7pathsentries androotDir: ".". The published types still leave out both trees, and the supersededscripts/gxt-check.mjsand its two tsconfigs are removed.validator.ts'sdebug()now readsDEBUG, notprocess.env.NODE_ENV, aReferenceErrorin a browser.b6a0aaa131), soassert-native-dist,bundle-modern-app.mjs --assert-nativeand the native package's entry prune reject a native import of any of them by name.Gated code in classic files
glimmer/lib/base-renderer.ts,renderer.ts,templates/root.tsandcomponent-managers/unwrap-template.tsmoved, verbatim apart from imports and exports, intogxt-backend'sroot-template.ts,render-component.ts,classic-roots.tsandtemplate-adapter.ts(c741f0d35a,c604f8d668,f229c5009a,b1bf210aef). The four files reach it through one bridge slot,GxtGlimmerHost, whichglimmer-host.tsfills; each call site is a wholeif (__GXT_MODE__)block or a top-level ternary that checks the host is installed. They went from +3,316 to +338 added lines, with a byte-identical classicdist/prodafter each move.bundle-size-check classicalso fails on a livegetGxtGlimmerHost(orsetGxtGlimmerHost(call.template()of@ember/template-compiler(9b68659ccd), and the GXT parts ofRoute#refresh,Route#setupanddidTransition(17e4043436), ofnotifyPropertyChange(5c88a9bfe1) and ofgetRootViews/getChildViews(5a0630464d). The leftovers that changed the classic dist bytes are gone (cf7c4118d7,b147edb83b,bf5d10cf82,510da727c4,a9e6d684db,7edb8b8af2,cb3927dc51,9fcfd0faac,88d4488001,66fdf8a4fb): GXT-only imports, fields, exports and locals, two ungatedtemplate()wrappers, and the classic branch of 7 if/else forks. The classic package files went from +2,004 to +1,356 added lines.main's rendering tests callBaseRenderer#renderdirectly. Under__GXT_MODE__it now hands the render to the glimmer host, asrenderComponentdoes (9 more lines inbase-renderer.ts).CoreViewstaysmain's. The layout entry says what is left.The rebases onto
main54d1bf526d. Three conflicts, inpackage.json,pnpm-lock.yamlandarranged_content_test.js. An audit of all 456 hunks in the 82 classic package files removed two GXT comments without code in shared files and gated a skip of empty[integration]sub-modules.4019de38a7(38 commits, 26 files): two conflicts, inpackage.jsonandpnpm-lock.yaml. The classic audit found every PR hunk inmain's files gated or one of the classic changes listed under backwards compatibility.21a5f6c228(10 commits, 12 files): no conflict and nothing new to gate. Commit 5 updates the native allow-list and 12native-skip.jsonkeys for a test filemainmoved. The 9 fixes folded in change 13 files, all undergxt-backend/, which classic builds do not load.493985e956(44 commits, 77 files, 18 of which this PR also changes). Five conflicts, all in tests and test helpers:contextual-components-test.jsandmount-test.jsaremain's files with this PR's new test and whole-file skip;runtime-resolver-cache-test.jsandtemplate-factory-test.jstakemain's expectations with GXT branches, andmain's copies of their old tests incomponents/classic/keep the old GXT branches;rendering.tsismain's Glimmer-root harness plus the GXT steps. One clean merge broke:mainremoved aClassicComponentimport that two PR tests use, and it is back.main's [CLEANUP] Render tests through a Glimmer root component #21775 (every rendering test renders through a Glimmer root) and Deprecation enabled / URL updates for 7.5.0 #21745 (the 7.5 deprecations) broke about 386 compat tests at first; the adaptations above, which commits 2–6 list, fix them. The classic audit found this PR's added and removed lines in the 18 files equal to those on the old base apart from these edits, all__GXT_MODE__-gated or GXT-only but that import, and the classic dists differ frommain's as before.Each time,
pnpm-lock.yamlismain's lockfile plus this PR's 294 lines, with no line removed, and the baselines were regenerated from CI.gxt releases consumed
6114d128e4, baselines0ae49a5cad): native{{#if}}block bounds and Inspector modifier nodes, and compiler fixes (decorator-runtime imports,...attributesprecedence,{{component}}as an argument, block params,<@comp as |v|>, the nearest template of a class chain) that replaced Ember-side workarounds.26181318aa, baselines56a489699b): twelve compiler and runtime fixes, such as content bindings evaluated once and render-tree names for<this.X>and<@X>, with Ember-side fixes (fe2aff7af4,3b92022c9f,e9819caf94,bcc2c7ced1).e324bce683, baselines658d4f88f5): fixes to revalidation, attribute order,style, errors in branches and re-bound keyed rows, and three opt-ins for an Ember host: Glimmer's template text, argument labels and adebuggerhook. The Ember-side fixes are in the areas above.6b8675867f, baselines025c970ad5) cuts the memory a large render keeps: removed rows' opcode entries, cell-less modifiers, opcode arrays, the dynamic-content binding's scope, and two slots of cells and formulas. It needed no Ember change and changed no test result.Gaps listed in earlier descriptions that are closed
cell()staleness,@cachedoverthis.args,{{#each}}over Glimmer-component rows, and standalonetracked(value).dirtyTagForbefore the first render, a DOMNodevalue in content, the whitespace-only text that compat dropped, and the fixed debug label of internal argument references.{{debugger}}as a no-op (one of its tests still fails), and the elements rendered before an error during a component's construction, which now stay (2 tests; text and nested blocks still do not).{{#each}}row re-bound while the list grows, a component's yielded(hash)or(array)of its own tracked state, and six native known reds:{{element}}with modifiers, one style warning, and the{{fn}}and{{on}}assertions.92689181a5: 9 compat and 14 native tests with the 9 folded fixes, and the reds that came withmain's cache-tag and positional-params tests.5d4854653d: the backtracking assertion when a child writes a member that its Glimmer or template-only parent's template read (3 tests).ApplicationInstance#visit, the nightly verdict, the silent catch blocks, the benchmark timeout, the GXT trees' exclusion from lint and type checks, the vitest files with no runner, the README's isolation claim, the packaging doc's RFC citations, most of the gated code in classic files (see the layout), the leftovers in the classic dist, and the trees above their budgets.Known gaps and what is not supported
visit()with a serialize or rehydrate_renderModefails a development assertion; native does not check it.if/unlessarity, named-block rules, strict-mode names not in scope) can compile and render wrong or empty output. GXT's own compile errors are thrown, with GXT's text.fnare marked and passed through.{{#if}}or{{#each}}body is rendered once; a changed value creates an instance that is never rendered.{{#each … key="@key"}}andkey="@index", which key items by position, move rows instead of keeping their nodes and updating them (compat and native).getElementByIdduring render misses elements rendered before it.{{this.person.name}}, as a plain property read that compat does not see, so writing it in the same render does not assert (the dynamic-components backtracking-message test). This needs gxt's cell-read hook (feat(host-hooks): DEV onCellRead seam for compat backtracking detection lifeart/glimmer-next#275, unreleased) and a runtime-compile flag that emits property taps, which changes every runtime-compiled template.(helper …)is never destroyed.{{#each}}updates untracked items whose key did not change.(hash)member paths, a route context's sibling getters,{{#each}}backing fields, and argument effects. An error there is reported in development (ember-gxt.dropped-read).1a56745208also records two kinds of read that Glimmer does not consume, so it can assert where Glimmer would not: an argument that compat evaluates eagerly but no template uses, such asvalue=this.obj.x, and a JavaScript read through a tracking proxy inside a tracking frame, such as a getter that readsthis.args.obj.x. No test hits either.Error occurredmessage (1 test).{{fn}}tests on the result ofmutfail:mutasserts that it was not passed a path. The cause is not verified.{{debugger}}test fails, "basic debugger statement" in its template-only variant: the{{#if}}keeps its false branch on the third render, so the callback runs twice instead of three times. This is read from the CI failure; the cause is not verified.mainadded in the fourth rebase (4):@trackedfield that its Glimmer parent's template rendered ({{this.value}}) under a classic root, and compat does not assert.1a56745208records only member reads through the tracking proxy of an object a template path returned.{{outlet}}in a component under any route instead of asserting below a non-classic route.{{{{raw}}}}…{{{{/raw}}}}).{{#each-in key=}}(5) and{{#each}} @key(1);Evented.detect;mainrewrote to a custom-managed component with positional arguments: native does not update the positional arguments of such a curried component (4), renders nothing for one given positional arguments as an argument (2), and resolves one invoked with arguments in a mustache as a helper (2).__GXT_MODE__-gated; before the moves they gained about 5.0K.base-renderer.ts+162,renderer.ts+65,templates/root.ts+29 andunwrap-template.ts+11. template-compiler'stemplate.ts+16,route.ts+29 androuter.ts+47.tracked.ts(+231),property_events.ts(+140) andcomputed.ts(+125). They sit on hot paths (everyset,@trackedread and write, and computed-property cache miss), some run beforegxt-backendis installed, and they share module-local state with the classic code, so a bridge hook would add a dispatch to each path and still need the code as a fallback.property_set.ts,tags.tsandproperties.ts; the router'sstashParamNamesand outlet-update skip, which use router locals; the run loop's flush budget,afterRenderwrapper and end-of-loop sync (+71); and small hooks inenvironment.ts,helpers/element.ts,object/core.ts,promise-proxy-mixin.ts,application/instance.tsand the classic route manager.@glimmer/managerintemplates/root.ts. Removing it would not change the classic prod dist, but it breaks the native rollup build with a circular dependency.Every known red is listed in
test-results/gxt-baseline.jsonandtest-results/native-baseline.json. Every skip has its reason in the test source or innative-skip.json.Dependencies
@lifeart/gxt0.0.86, exact pin, published. The branch has consumed 0.0.82 to 0.0.86 (Ember native: run Ember without the Glimmer VM (tags = gxt cells, host hooks, Glimmer semantics) lifeart/glimmer-next#265–270, all merged). feat(args): brand compiler-emitted reactive arg thunks ($_argThunk) lifeart/glimmer-next#272–277 are open and unreleased, and this PR does not use them. gxt is pinned once, as a rootpackage.jsondevDependency, plus thesmoke-tests/v2-app-gxt-templatefixture. Classicember-sourcedeclares nothing about it. CI installs it from npm. TheGXT upstream contractjob checks that the 89 named importsgxt-backend/takes from gxt exist in it, across 4 entry points.playwright^1.59.1 androllup-plugin-visualizer^7.0.1. Therolluprange moves from ^4.57.1 to ^4.60.2, with no lock change: main's override still resolves 4.61.0. The lockfile changes by +294 / −0 lines.CI
On emberjs/ember.js, only
PR Title Linthas run for the head; the other workflows are waiting for approval to run. The results above come from the fork, lifeart/ember.js:maina9501d5), ran the PR workflows up to0de3263761and is now closed..github/workflows/), deleted after the run. For the head's tree, the scratch branch wasci-scratch-rebase4-f,e4f2cd0e6dplus that commit (5228fb89ee), and the runs are CI 42054852312, GXT Full 42054852478, GXT native 42054852391, GXT Smoke 42054852337 and GXT dual-build 42054851974..github/workflows/scratch-dist-diff.yml(run 42054852136, one job), which builds the classic dists ofmain, ofmainplus commit 1 and of the head, and of the previous head and its base plus commit 1, and compares their lines with chunk names normalized (see backwards compatibility).Perf script still works,Notify DiscordandPublish alpha from default branchare skipped on a push, andPublish channel to s3fails because the fork has no AWS credentials. The other 64 jobs passed;Basic Testpassed on the re-run of the failed jobs, after its browser did not connect to testem in the first attempt. All 20 jobs of the four GXT runs passed.The workflows:
CI(classic): the jobs are unchanged. The twoemberSourceGxtsmoke scenarios build the compat package first.GXT Smoke(pull requests): the upstream contract check, and 14 compat modules in 4 shards, each gated bybaseline-gate.mjs --subsetagainsttest-results/gxt-baseline.json.GXT dual-build matrix(pull requests): rollup builds classic, gxt and gxt-native;bundle-size-checkfails above 5 % over budget, and for classic also on live GXT bridge code;assert-native-distchecks the native builds.GXT native(pull requests): the modern app from the classic and native dists, strict and loose, with boot checks, the VM-free assertion and bytes; the native package and its consumer acceptance; and the native suite in 4 shards, merged and gated againsttest-results/native-baseline.json.GXT Full (nightly): the compat full suite in 5 shards, merged and gated the same way. It runs on schedule or dispatch only, and files an issue when the merged gate fails.Baseline gating.
runner.mjsexits 1 whenever any test fails, known reds included, so it cannot be the verdict on its own.baseline-gate.mjspasses when there are no timeouts and no harness errors, every module in the baseline ran, every failing test is listed as failing in the baseline, and the runner wrote its summary. Known reds that start passing are reported, with a prompt to regenerate the baseline, and never fail the gate.How to review
The diff against
main(493985e956) is 302 files, +101,333 / −1,021. Of the added lines, 13,925 are the two baseline JSON files and 294 are the lockfile.The 8 commits, in review order:
8ed8cd63a6build and tooling for the GXT render backends (19 files): Vite, rollup and babel flavors, the alias map and the native forbidden list (scripts/gxt-alias-map.mjs), build constants, the gxt pin, the lockfile, bundle budgets, and the lint and type-check settings for the GXT trees.188b506a54the GXT compat backend (67 files):gxt-backend/withoutnative/, including the glimmer host modules, the code moved out of template-compiler, routing, metal and views utils,@glimmer/component-gxt, the backend's own tests, the 5 compat fixes of the third rebase, and the compat adaptations of the fourth.7113aef3a1the native layer and its package (94 files):gxt-backend/native/, the package script and Vite preset,assert-native-dist, the modern-app harness, the 4 native fixes of the third rebase, and the native adaptations of the fourth.9078e4047eGXT hooks in the classic packages (30 files): every change to classic package source. Each hunk is__GXT_MODE__-gated code or a GXT-only declaration, apart from the{{debugger}}callback move and the one leftover import listed under known gaps.be2242505btest infrastructure for the GXT backends (32 files): the runner, shard merge and baseline gate,index.html/native.html, internal-test-helpers, and the integration-tests GXT delegate.47108b67c0GXT-specific tests and test changes (32 files).21233844d9GXT workflows, smoke apps and scenarios (25 files).4614753b53test baselines and the packaging design doc (3 files).On top of them:
d06e8e1b65a cache keeps its tag when it consumes the same tags again (2 files: the compat and native validators);764861e503a custom-managed component reads its changed arguments (2 files:manager.tsand a gxt-backend test);1a56745208a write of a member a Glimmer component's template read asserts (3 files:gxt-bridge.ts,manager.tsandvalidator.ts);e4f2cd0e6dregenerate the test baselines (3 files: the two baselines and the runner README's smoke counts).Each commit message describes what the commit holds and names its main fixes and their causes. Only the head's tree,
e4f2cd0e6d, was built and tested for these numbers; the PR is meant to be reviewed and merged as a whole, and the commits before it may not build on their own.Suggested order:
metal/lib/tracked.ts,property_events.tsandcomputed.ts,runloop/index.ts,components/abstract-input.ts,object/core.tsandrouting/router.ts.application/instance.tsismain'svisit()code apart from the GXT assertion.{{debugger}}callback state. It moves without a gate from the debugger opcode into@glimmer/runtime/lib/debugger-callback.ts, so that the GXT backends call the same callback; the opcode reads it from there (cc5b59558c).rollup.config.mjs,vite.config.mjsandscripts/gxt-alias-map.mjs.compile.tsandmanager.ts(about 14.5K lines each) are most of it; read them by their section headers.Run locally
The runner also accepts
--smoke(the 14 CI modules, withbaseline-gate.mjs --subset),--shard i/N(CI merges the shards withmerge-runs.mjsand gates the merged run),--filter <text>and--auto-serve; seescripts/gxt-test-runner/README.md.Backwards compatibility and risk
main's counts on493985e956, plus the 91 tests this PR adds to shared test files (89 in production builds), which pass on classic too. Classic type checks pass on every supported TypeScript version.__GXT_MODE__-gated and fold away at build time.bundle-size-check classicrejects live GXT bridge code in the classic dist.main's. Measured on493985e956by the dist-diff run 42054852136:shouldPrintComment), so the prod tree is compared withmainbuilt with commit 1 only, which adds that rule and leaves the dev tree's lines unchanged: 1,206,007 bytes raw and 204,064 brotli, against this PR's 1,205,816 and 203,993.main's plus two changes: the{{debugger}}callback module (cc5b59558c), from which the opcode reads the callback (getDebuggerCallback()), which is intended, and one alias line,const template = templateFactory;, because an ES module cannot re-export a binding conditionally. The other differences are import and export lines: GXT-only static imports in metal change the module graph, so Rollup puts metal'schain-tagsmodule in theobserverschunk (92 shared chunks instead of 93).main's: the same code differences, an empty statement left by a folded__GXT_MODE__gate inComponentRootState, GXT comments in kept code, and@ember/componentdocs thatmain's dev tree does not have.5d4854653dhad the same code differences on its base, line for line; in dev, three comment lines differ, reworded in the fourth rebase.main.ember-source.@lifeart/gxtis a rootdevDependency, so a classic install does not pull it. The generatedember-source-gxtandember-source-gxt-nativepackages declare the exact pin as a real dependency.{{debugger}}callback state. For apps that opt in, the risk is that of a preview, with the gaps listed above.🤖 Generated with Claude Code