Repository navigation
Conversation
📊 Size reportTarball size — dist/dev 0.3%↑
Show files (21 files)
dist/prod -38.7%↓
Show files (187 files)
smoke-tests/v2-app-template/dist 0.2%↑
smoke-tests/v2-app-hello-world-template/dist 0%↑
🤖 This report was automatically generated by wyvox/pkg-size |
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. - `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: the `@lifeart/gxt` 0.0.82 dependency, playwright for the GXT browser tests, the bundle-size check and budgets, the compat package assembler (scripts/build-gxt-package.mjs), the opt-in gxt-backend checker, and lint / tsconfig / prettier / yuidoc exclusions for gxt-backend. On top of main's build: the classic test build keeps main's `treeshake: false`; GXT test pages isolate a test module the gxt compiler cannot compile (a failing synthetic module instead of a page that fails to load); the stable-decorators type check excludes gxt-backend like the main one; `@glimmer/component` resolves to its source in the GXT configs too; the published types (types/publish.mjs now fails on tsc errors) leave out gxt-backend and @glimmer/component-gxt and see the `__GXT_MODE__` declaration. 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;
- 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;
- 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.
Also @glimmer/component-gxt and the backend's own QUnit tests (__tests__).
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), 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. 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: the renderer's GXT root render (ClassicRootState), its force-rerender / root-tag bookkeeping, and the GXT `renderComponent` (base-renderer); a VM-free renderer state under GXT; the GXT root template (templates/root.ts); GXT-aware `template()`, `unwrapTemplate`, internal components, Input, element helper, scheduleRevalidate. - metal: the single gxt cell behind `@tracked`, the GXT re-render trigger in `notifyPropertyChange`, the CP re-entrancy guards, `set` hooks; barrel imports of @glimmer/validator where the rollup alias map needs them. - 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. - runtime / object / runloop / application / views / template-compiler: smaller GXT hooks (observer flush budget, proxy unknownProperty during renders, settled PromiseProxy fast path, rehydrate DOM handling, view-tree rebuild, GXT `template()`). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- scripts/gxt-test-runner: the Playwright + QUnit runner (per-module isolation, baselines, smoke list, native allow-list and skips, contract tests, baseline gate; the gate's `--subset` mode compares only the modules a smoke shard ran); - 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, GXT artifact stripping in equalTokens / equalInnerHTML, GXT run helpers; the router test case exposes its root element to GXT; - integration-tests: the GXT rehydration delegate (two plain GXT renders; serialization-dependent tests skipped with a reason) on main's `RenderDelegate` / `RenderHandle` interface, GXT `<!---->` stripping, and no empty component sub-modules. 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), GXT skips (hot reload, `{{mount}}`), new regression tests
(has-block-params in nested blocks, the (hash)/(array) identity tests, which
moved to helpers/gxt-hash-array-identity-test.js because main rewrote
array-test / hash-test as .gjs), and renames that keep the names `array` and
`element` out of module scope in the single-chunk GXT test bundle.
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: main rewrote tests in six smoke modules, and their new versions are known reds there), dual-build, gxt-native (modern-app boot, package, native suite against its baseline) and nightly full workflows; the setup action's optional Playwright install; the ember-source-gxt scenarios and the v2 app template for them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test-results/gxt-baseline.json (compat, 8703/9828, 954 modules) and
native-baseline.json (native, 7061/7103, 672 modules), regenerated on this
series from one local full run each; and docs-internal-gxt-packaging-design.md.
Every status change against the pre-rebase baselines is a test main added
or rewrote: Glimmer components instead of classic ones, .gjs conversions,
route managers, standalone tracked(), {{#each-in}} key=, {{on}} listener
order, and main's rewrite of the @glimmer-workspace integration harness,
which now renders the [integration] suites through Ember's renderComponent
(GXT under GXT_MODE) instead of the Glimmer VM. The notes in the files list
the known reds and the three [integration] modules that hang under GXT.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lifeart
force-pushed
the
glimmer-next-fresh
branch
from
October 13, 2026 09:08
68874e5 to
8679ee1
Compare
…ever Three compat `[integration]` modules hung the renderer once main's harness rendered through `renderComponent` and registered every helper as a public `setHelperManager` definition. Two loops, both in compat only: 1. A helper's value read wrote reactive state. On every evaluation of the formula that renders a cached custom-manager helper, `$_maybeHelper_ember` assigned the bucket's args, and the args setter dirtied a tag that the formula had consumed through the helper's cache: with no dedup when an argument was a function, and a forced cache miss after any classic dirty (which the assignment itself caused), each evaluation scheduled the next (`<Bar @content={{(foo)}} />`, `<Bar @content="{{foo}}" />`). The read also wrote the result cell. Helper args now follow Glimmer's args proxy: `positional` / `named` are accessors over plain storage; an assignment compares slot by slot (`Object.is`, functions by identity) and only a real change bumps a helper-args tag, which only the shim's caches and track frames see (no GXT cell, no classic bridge, no reactor, no scheduling). `getValue` stays the helper's cache, so compute re-runs only when an argument or a tag it consumed changed; the read path no longer keeps an arg-serialization dedup and never writes the result cell. 2. A `(hash)` / `(array)` argument with a `(component)`, `(hash)` or `(array)` member got a new identity on every read after any change: gxt's `cachedHelper` re-runs a factory that read no cell (every `(hash)`) on each revision change, and those members defeat its shallow-equal check. Each sync then saw an argument change; its write-back fired the `renderComponent` root's classic reactor, which re-rendered the root in the middle of the sync's pass over the component args, and the pass went on into the new components (`<X @stuff={{hash Foo=(component "foo")}} />`). `$__cached` now has reference semantics (re-run only when a cell the factory read changed, constant otherwise, previous identity kept when shallow-equal). The sync's arg write-back and the arg-dirty effect run as arg propagation (`withArgPropagation`), whose classic dirties fire no classic reactor: GXT is already propagating that change. Tests: gxt-backend/__tests__/helper-args-test.ts (14 tests; 11 hang or fail without the fix). The 5 hanging tests of the three modules now pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ment The per-slot `Object.is` comparison of the previous commit kept a helper's cached value when an array argument was mutated in place (`pushObject`): the array is the same object, and the old JSON arg-serialization dedup that used to notice the new contents is gone. In Glimmer, reading an array argument consumes the array's `[]` tag, so such an array is a changed argument. A slot now also records an array's elements and is unchanged only when they are still the same (`Object.is` each). Test: helper-args-test.ts `an Ember array argument changed in place re-computes the helper` (passes before the previous commit, fails with it alone, passes now). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Regenerated from one local compat full run on 996d0c9 (runner.mjs --full, no timeouts, no harness errors, no green->red against the previous baseline): 8764/9928 tests in 959 modules (was 8703/9828 in 954). - The three `[integration]` modules that hung the renderer are now in the baseline: `jit :: Components :: Glimmer` 20/42, its `> [integration] Glimmer` sub-module 20/38, `argument-less helper paren-less invoke` 2/5 (the five tests that hung now pass; the other reds are the known harness conformance gaps). - New modules: `GXT helper args: renderComponent roots settle` 10/10, `GXT helper args: helper re-computation` 5/5. - red -> green: Custom Helper test :: works with strict-mode; Helper Tracked Properties :: class based helpers are autotracked, nested tracked properties rerender when updated; [integration] jit :: strict mode: dynamic template values :: Can pass curried helper as argument and invoke dynamically. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`for…of` over the reference's bookkeeping Map goes through gxt's reactive Map.prototype iterator patch, which registers the Map with the reading formula; the rest of gxt-backend iterates such Maps with `forEach` for that reason. No behaviour change: the three [integration] modules, the new tests and the `--filter "[integration]"` / `--filter Helper` runs give the same results as the baseline generated from 996d0c9. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he same `<X @f={{fn this.greet "x"}} />` re-rendered forever in a renderComponent root. The compat compiler emits the argument as a getter that calls `$__fn` on every read, and `$__fn` returned a new function each time. The manager compares an argument by identity, so every sync saw a change: it wrote the value back to the component and dirtied the argument, which scheduled the next sync. Glimmer's `fn` is one function per call site; the function reads the callback and the partial arguments when it is called. Now: - an `(fn …)` value records the callback it binds and calls through a replaceable implementation (utils.ts `createFnHelperValue`); a `(mut this.x)` cell records what it writes, so two cells of one call site bind the same callback; - a component argument reads through a slot per component and argument name, shared by the instance's arg getter, the args proxy and a re-render's new getter. The slot keeps its `(fn …)` value while a new evaluation binds the same callback, and takes that evaluation's implementation, so a call still reads the current callback and arguments (`keepFnHelperValue`). Reading an argument writes no reactive state. Tests in helper-args-test.ts: three renderComponent-root hangs (direct, with a changing partial argument, with `(fn (mut …))`), one function per call site for a template-only and a classic component, a changed callback, and a stashed function reading its current arguments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e run loop
`{{join-words this.list}}` (and `{{this.list.length}}`, and `{{#each}}`
over the array) kept the old value after `runTask(() => list.push('c'))`.
The formula that read the array does subscribe to the array's collection
cell, but nothing rendered the change: `dirtyCollection` invalidated the
cell and scheduled a `syncDom()` microtask, and before that microtask ran,
the run loop's DOM sync (`_gxtSyncDomNow`) — taking its non-property
branch, since no property change was recorded — cleared gxt's pending
invalidations as initial-render noise, the collection cell's with them.
Glimmer: dirtying a tag schedules a revalidation, rendered when the run
loop ends. A trackedArray change now does what a `@tracked` write does
(glimmer-tracking.ts `trackedSet`): it records a pending state change
(only the pending sync while a render is in progress) and schedules a
revalidation, besides the coalesced flush (validator.ts
`_scheduleNativeCollectionFlush`).
Scoped to the native-cell collection (trackedArray's `collection` tag).
The storage-wrapped collections (trackedMap/Set/Object, `cell()`) go
through `dirtyTag` and keep their behaviour: recording their changes too
made a `renderComponent` root compute a helper over a trackedObject four
times (the classic reactor re-renders the root, then the sync re-ran the
previous render's formulas), a regression of the Helper Managers
"plain functions entangle with any tracked data" test.
Tests in helper-args-test.ts: a trackedArray helper argument, a
trackedArray component argument, and length / plain-function /
custom-manager reads of one trackedArray changed by push and splice.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…text
A helper inside a template-only component received the component's render
context as its named arguments (`{ $fw, args, attrs }`), and one inside a
block of it the block's context, inside a Glimmer component the instance's
own arg properties. `$_maybeHelper_ember` guessed which of its third and
fourth arguments was the hash and which the context from the third one's
shape, and the shape checks missed those contexts.
gxt's emission decides it: a call by NAME passes the render context last
(`(name, positional, ctx)`, or `(name, positional, named, ctx)` with named
arguments); a call of a BINDING passes `(definition, positional, named)`;
Ember-side callers pass all four. So with four arguments the third is the
hash, a name's third argument is its context, and a binding's third
argument is a context only after compile.ts's scope rewrite turned a name
call into a binding call, recognised by gxt's component id
(`COMPONENT_ID_PROPERTY`), which every render context carries and a hash
never does. The strict-mode `$_maybeHelper` shadow now forwards the call's
own arguments, so their number survives it.
Glimmer: a helper's named arguments are exactly the call site's
`key=value` pairs. New helper-call-site-test.ts: a custom-manager and a
function helper in a template-only component, in its blocks, in a Glimmer
component, and a strict-mode scope helper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Owner-registered helpers (a `setHelperManager` definition, a classic
`Helper` class) were cached per helper NAME: two sibling
`{{counting "a"}}{{counting this.b}}` shared one instance (`[a1][b2]`),
every `{{#each}}` row shared it, and tearing down one call site's
`{{#if}}` branch did not touch the others' state because they were the
same object.
Glimmer creates one helper instance per call site and render scope. The
compat call (`$_maybeHelper("counting", ["a"], this)`) carries the render
scope (`this`, or a block's context) but nothing that tells two call
sites in one scope apart. Now:
- an AST transform (ast-transforms.ts `gxtHelperCallSiteTransform`) gives
each helper call site a process-unique id, passed as a `__gxtSite`
named argument. It marks only calls gxt emits as `$_maybeHelper` by
NAME with at least one positional argument (not keywords, block params,
scope values, or zero-argument / named-only calls, whose emission must
stay exact);
- `$_maybeHelper_ember` takes the id out of the named arguments and keys
the helper bucket / classic instance by (render scope, site), in a map
whose entries are destroyed when gxt destroys the scope (an `{{#if}}`
branch, an `{{#each}}` row, a component); calls without an id keep the
name-keyed cache.
New tests in helper-call-site-test.ts: two sibling call sites keep
separate state, destroying an `{{#if}}`'d call site leaves the other,
`{{#each}}` rows get one instance each, a custom manager creates one
helper per call site. Known gaps (no site id): templates compiled at
build time by gxt's own plugin, binding calls (gxt passes no render
scope with them), zero-argument and named-only calls.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Regenerated from the compat full run on 430658f (961 modules, 8804/9948, no timeouts, no harness errors, no green->red against the previous baseline). Against it: +20 tests in the two extended GXT helper args modules and the two new GXT helper call sites modules, all green; 20 red->green elsewhere: - Components test: contextual components (classic component): GH#14508 rest positional params passed as a named parameter (2); - Components test: curly components (classic component): can use hash parameter instead of arbitrary positional param [GH emberjs#12444]; - Helper Tracked Properties: array properties / getters / tracked properties rerender when updated (3); - Syntax test: {{#let as}}: can access alias of an array; - The <LinkTo /> and {{link-to}} components: it works in an #each'd array of string route names (2); - [integration] jit :: initial render (client): 9 helper / literal tests; - [integration] jit :: trackedArray() (rendering): {{each}} works with new items / when updating old items. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… first
gxt's `cellFor(obj, key)` without `skipDefine` defines its own cell
accessor on `obj`. Over a `@tracked` key (an accessor on the class
prototype) that accessor shadows the tracked one: later writes
(`obj.key = value`) update only gxt's cell, the tracked setter's
notification never runs, the run loop's DOM sync records no change and
the DOM stays stale, with no error. Two callers did that:
- the re-render trigger's render-context fan-out (compile.ts SyncCore)
updated the cell of a context whose target is the written object
itself with `skipDefine` false, so the first write to a component's
own tracked property shadowed it (the second click of a counter
rendered nothing, for a classic and a Glimmer component alike);
- a template's block-param path read (`{{item.name}}` in an `{{#each}}`
row, compiled to `$__cellFor(item, "name")`) shadowed the tracked
property of the row's item during the first render, so no direct
write to it ever rendered.
`isTrackedAccessor(obj, key)` (manager.ts, memoized per prototype for an
inherited accessor) tells both callers that the key is tracked: the
fan-out updates the cell without defining, and `$__cellFor` reads the
key through its accessor, which reads and subscribes to the same cell.
Both `@tracked` implementations of the compat build (metal's and
gxt-backend/glimmer-tracking.ts) had the bug the same way.
The new `__tests__/glimmer-tracking-test.ts` asserts the DOM after
repeated writes in each setting (classic / Glimmer / template-only
components, helpers, `{{#each}}` rows, arguments, backtracking, action /
next() / microtask writes, `@cached`, inheritance, non-component
objects) and ports the classic tracked tests with `@glimmer/tracking`
imports. Before: 6 of the 13 settings tests fail (stale DOM after a
write, no error); after: all pass. The ported modules pass before and
after.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ompat build
The compat build aliased `@glimmer/tracking` (and its
`primitives/cache`) to gxt-backend/glimmer-tracking.ts, a second
`@tracked` / `@cached`: a `trackedData` storage cell plus
`consumeTag(tagFor(obj, key))` on read; `dirtyTagFor`, `setPendingSync`
and `triggerReRender` (skipped while rendering) on write; the accessors
marked `__isTrackedGetter` / `__isTrackedSetter` for manager.ts. Ember's
own classes, the native build and upstream's `@glimmer/tracking` use
metal's `tracked`, whose GXT path keeps the property in its one gxt cell
and notifies through `notifyPropertyChange` (the re-render trigger, which
records the pending property-change sync, plus `dirtyTagFor` of the key
and SELF_TAG, observers and `PROPERTY_DID_CHANGE`).
- scripts/gxt-alias-map.mjs: the two `@glimmer/tracking` rows are gone,
so both builds resolve the in-repo package: metal's `tracked` /
`cached`, and the cache primitives from `@glimmer/validator/lib/tracking`
(collapsed onto the validator shim, as before).
- glimmer-tracking.ts is deleted.
- manager.ts: `_isTrackedKey` checks only the TrackedDescriptor metal
records in meta. The other three marker checks (the plain-getter
collision check, the createRenderContext cell install, the render
proxy's getter cache) already excluded every accessor with a setter,
and a `@tracked` accessor always has one; the marker clauses go.
- @glimmer/component-gxt re-exports metal's `tracked` / `cached` and the
validator shim's `createCache` / `getValue`; its identity test follows.
Its ambient declarations move from gxt-backend-shims.d.ts to a `.ts`
script: types/publish.mjs copies every hand-written `.d.ts` into the
published types, where a second declaration of metal's real modules
clashed (TS2451 on `cached`).
- Comments that named glimmer-tracking.ts are updated
(compile.ts, ember-gxt-wrappers.ts, validator.ts, bundle-modern-app's
byte category).
What `@glimmer/tracking` gains in compat: the TrackedDescriptor that
`get` / `set` and the component manager see, field initializers called
with the instance as `this`, the `equals` option, the classic-class
`tracked({ value })` form and the array (`[]`) consumption of a tracked
array value. The last module of glimmer-tracking-test.ts covers them
(red before, green after); the DOM-after-write modules are green before
and after. Metal is unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Regenerated from the compat full run on c16139a (966 modules, 8846/9990, no timeouts, no harness errors, `baseline ok` against the previous baseline); 018eac1 differs from it only by renaming @glimmer/component-gxt's type declarations, which the test page does not load. Against the previous baseline: +42 tests in the five new `GXT @glimmer/tracking` modules, all green; no red->green and no green->red elsewhere. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The classic ember-source dist never imports @lifeart/gxt, yet the package listed it in `dependencies`, so every classic install pulled gxt and its own dependencies (@babel/preset-typescript, @glimmer/syntax, content-tag, decorator-transforms). - package.json: @lifeart/gxt moves to `devDependencies`. That entry is the repo's single exact pin. ember-source declares no peer or optional dependency on it: nothing in the published package uses it. - scripts/build-gxt-package.mjs: ember-source-gxt takes the pin from the root devDependency and declares it as a real, exact dependency. It fails if the root lists gxt in dependencies, peerDependencies or optionalDependencies. - scripts/build-gxt-native-package.mjs: ember-source-gxt-native pins the root devDependency instead of the installed version. An npm-installed gxt that differs from the pin fails the build; a local gxt build still only warns. - gxt-native.yml packs the gxt named by the devDependency. - packages/@ember/-internals no longer declares gxt. It is private, and its gxt-backend sources resolve the root node_modules copy (Node, vite and tsc all land on the same store path). The vite configs alias that root copy, and rollup keeps gxt external. - The gxt-consumable smoke test now asserts that the compat package depends on exactly the root pin, and not as a peer or optional dependency. - The packaging design doc no longer claims that classic needs gxt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…efinition
The GXT renderComponent treated every definition that is not a class as
template-only. A plain object with a custom component manager
(setComponentManager) therefore rendered with `this` = {}: the manager's
createComponent and getContext never ran. The @glimmer-workspace
integration harness renders every loose template that way, so `{{this.x}}`
was empty in those suites.
The custom manager is now looked up on any definition (own or on the
prototype chain, as getInternalComponentManager does). A definition with
one renders the context its manager creates, with lazily read named args,
calls didCreateComponent after the render when the manager asks for
asyncLifecycleCallbacks, and destroyComponent when the render is destroyed
when it asks for a destructor. The owner is added to such a context only
when it has none, and not as an enumerable property.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… real one The "LOCAL gxt build" warning fired whenever node_modules/@lifeart/gxt was a symlink. pnpm installs every package as a symlink into its virtual store, so the warning also fired for the npm release. A symlink now counts as a local gxt only when it resolves outside node_modules/.pnpm (pnpm link, a link: dependency, a manual symlink to a checkout); a dist with build meta still counts as local wherever it lives. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s of plain-object definitions
The integration harness renders loose templates through renderComponent with
a plain-object definition and a custom manager. Running that manager makes
`this` the real test context: 280 more rows pass (9139/9992).
Five tests that only passed because `this` was `{}` (their code never ran)
are now known reds with real causes: the three "Element modifier with hooks"
/ "didUpdate is not called when params are constants" tests (a context write
re-renders the whole root, so the modifier re-installs instead of updating),
and the two `false` / `null` class-name tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The gxt-backend render-error queue is global. Several sites queue an error and also throw it, and the renders that flush the queue (renderComponent and a classic root's first render) threw whatever it held. So an error the caller had already caught stayed queued, and the next unrelated render threw it: in the @glimmer-workspace suite a test died with the error of the test before it. The Ember test harness hid this by clearing the queue itself. - Every queued error records when it was captured. A render takes a mark when it starts; when it completes it throws the first error queued since the mark, and when its own error propagates to the caller it drops the errors queued since the mark. Errors queued before the mark stay queued. - renderComponent also flushes its errors when it renders a component that has no template, through the component manager. - A renderComponent call that throws stops its classic-tag reactor. The caller gets no result to rerender or destroy it, and the reactor would render the failing template again on the next tracked write anywhere and queue its error inside another render. A classic root whose render failed stops rendering too. Test: "Strict Mode - renderComponent (direct)" renders a strict template with a helper used as a component (which throws), then renders a component that writes tracked state into another element; the second render must not throw the first render's error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…st render The test checks that the next render does not throw the first render's error, not which error the first render throws. Under gxt-native the helper-as-component render throws a TypeError instead of Glimmer's "Expected a dynamic component definition" message, so the test no longer matches the message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er template checks
Compat full run: 9440/10078 (was 9567/10079), 982 modules, no timeouts, no
harness errors, nothing turned green. The 2 tests of the removed
`GXT correctness: compile-time template checks` module are gone; the
`compile pipeline errors` module has 1 new test (the template compiler
throws a parse error), green.
126 tests turned red. Every one expects an invalid template to be rejected
at compile time with Glimmer's error; they were green only while the checks
ran. None is a regression of a valid template.
(a) Message-only difference, known red (25): the template still throws,
with the GXT compiler's (or GXT's not-in-scope) message instead of Glimmer's
`SyntaxError` and location.
- `[integration] jit :: compile errors` (11): end tags for void elements,
end tags with attributes, mismatched / unclosed / unmatched tags, the
unquoted attribute, and the 5 "error line numbers include ..." tests.
- `[integration] jit :: general syntax errors` (10): the 7 "Block params in
HTML syntax" tests, `../`, `./`, mixing `.` and `/`.
- `[integration] jit :: strict mode: general properties`:
`{{component.foo}}` throws (append position).
- `Components test: curly components` "block with properties on attrs is
asserted against", "non-block with properties access via attrs is
asserted against", and `(classic component)` "using attrs for positional
params is asserted against": `{{attrs.x}}` throws "attrs was not in
scope" instead of the `{{attrs}}` assertion.
(b) An invalid template the GXT compiler accepts, known red (100): the
accepted trade-off of dropping the checks. Nothing is thrown at compile
time (production builds already behaved this way).
- `[integration] jit :: \`<keyword>\` keyword syntax errors` (43): keywords
used in a position they do not support ("non-append / non-block /
non-expr / non-modifier keywords cannot be used as ..."): `component` 1,
`debugger` 3, `each-in` 3, `each` 3, `has-block-params` 2, `has-block` 2,
`helper` 2, `if` 1, `in-element` 3, `let` 3, `log` 2, `modifier` 2,
`mount` 3, `mut` 2, `outlet` 3, `readonly` 2, `unbound` 2, `unless` 1,
`yield` 3.
- `[integration] jit :: if/unless (if)` and `(unless) keyword syntax
errors` (11 + 11): `if` / `unless` arity and named arguments.
- `[integration] jit :: named blocks syntax errors` (10): every named-block
rule (in "Named blocks must start with a lower case letter" one sub-case
still throws a GXT error, the others compile).
- `[integration] jit :: yield keywords syntax errors` (8): `yield` /
`has-block` / `has-block-params` arguments.
- `[integration] jit :: strict mode: static template values` (9) and
`strict mode: general properties` (4: "Undefined references is an error",
`{{component}}` with a string in append / block / expression position):
strict-mode references that are not in scope.
- `Strict Mode <-> Loose Mode - renderComponent :: strict-mode components
cannot lookup things in the registry` and `` `action` is not a keyword ::
in strict mode, an unbound `action` is not in scope ``: strict-mode names
that are not in scope (a strict-mode helper call still throws when it
renders).
- `[integration] jit :: argument-less helper paren-less invoke` (1) and
`dynamic modifiers in resolution mode` (1): the loose-mode resolution
errors.
(b), consequence, known red (1): `Strict Mode <-> Loose Mode -
renderComponent :: rendering multiple times to adjacent elements` fails
with a global failure: the render-time "`a-helper` ... was not in scope"
error of the previous test's template (above), whose un-awaited render
throws after that test has ended. Its own template is valid.
(c) Regressions: none. The `{{this.attrs.x}}` deprecation, which the checks
had emitted, is kept by the previous commit (4 tests stay green).
The native suite does not load the compat compile pipeline; its baseline is
unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…th its block
Glimmer asserts an `{{#in-element}}` destination reference: when it
changes, the block is removed from the old destination and rendered into
the new one. When the block is destroyed (its `{{#if}}` branch goes, its
render is destroyed), its content leaves the destination. Now that a
renderComponent root updates in place, the compat `$_inElement` ran only
once: a new destination was never rendered into, and the content stayed in
the old one, also after its block was gone.
`$_inElement` now reads the destination in a tracking frame of its own and
keeps the block's render (its nodes and the effects of its text) per
destination. An effect owned by the block's context subscribes to the
cells that read consumed; when the destination changes, it removes the
render from the old destination and renders the block into the new one. A
destructor on the block's context removes the render when the block is
destroyed. A later `$_inElement` call that renders into the same
destination (a classic root rendering again) replaces the earlier render
and takes over following the destination.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t rendered in Glimmer resolves a template's modifiers once, in the owner the template renders with, and an update calls the installed modifier's updateModifier. The compat build resolves a modifier by name each time its formula runs, and an update runs that formula outside of any render: the ambient owner is another render's, or none. A modifier registered by name then resolved to nothing on update; its formula had called the destructor of the installed modifier and installed nothing, so updateModifier / didUpdate never ran. `$_maybeModifier` now records, per element, the owner of the render that first evaluated one of its modifiers, and evaluates the element's modifiers in that owner while it is alive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…stroyed When the modifier an element invocation renders changes to another definition (an argument swap, a curried swap, `(if a b)`), or to none, Glimmer destroys the old modifier and installs the new one on the same element. The compat build marks a modifier pending destroy when gxt calls its destructor before re-running the invocation, and the update path takes it back when the same modifier runs again. The end-of-sync flush then skipped every pending modifier whose element was still in the document, so a modifier replaced on a live element was never destroyed: its destructor / destroyModifier never ran. The flush now destroys every modifier that no evaluation of its invocation took back during the sync, whether its element was removed or not. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A modifier manager gets an args object from `createModifier` (and `installModifier`); in Glimmer it is a proxy over the argument references, so a manager that keeps it reads the current values when `updateModifier` runs. The compat build gave the manager an object over the values of the evaluation that installed the modifier, and a fresh one to `updateModifier`; a manager that reads the args it kept (the functional-modifier manager of the integration tests re-runs its function with them) saw the install-time values forever. The args objects now read the invocation's arguments from a source that the update path points at each evaluation's arguments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ves in its template's owner
Glimmer renders a block in the owner of its template, also when an
update renders one of its branches or rows again. The compat build
renders that branch or row during the DOM sync, outside of any render,
where the ambient owner is another render's, or none. A modifier
resolved by name in it then found nothing: an `{{#if}}` toggled back on
rendered its element without the modifier, and didInsertElement never
ran.
`{{#if}}` and `{{#each}}` now keep the ambient owner they were created in
and render their branches, rows and `{{else}}` in it while that owner is
alive, as they already keep their namespace and parent view.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s in its text node
Glimmer updates a text binding by changing its text node's value, so the
node is the same before and after an update. For an element whose only
content is a mustache (`<p>{{this.value}}</p>`), gxt updates the text
through its DOM api's `textContent`, which sets the element's
`textContent` and replaces its children: the text node was a new node
after every update.
The compat renderers now use a DOM api whose `textContent` changes the
value of the element's text node when it is the element's only child.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and modifiers
Compat full run: 9453/10083 (was 9440/10078), 982 modules, no timeouts,
no harness errors, no green->red.
8 tests turned green:
- `[integration] jit :: #in-element` "Updating remote element" and
"Inside an '{{if}}'", and `Updating` "helpers passed as arguments to
{{#in-element}} are not torn down when switching between blocks": an
`{{#in-element}}` renders into its new destination and its content
leaves the old one, also when its block is destroyed.
- `Updating Element Modifier` "Updating a element modifier": a modifier
resolved by name updates when its arguments change.
- `Updating Element Modifier` "Destructor is triggered on element
modifiers": an `{{#if}}` branch rendered again resolves its modifiers.
- `strict mode: dynamic template values` "Can use a dynamic modifier with
a changing definition" and its curried form: the replaced modifier is
destroyed.
- `Updating` "a simple implementation of a dirtying rerender": the text of
a single-mustache element updates in its text node.
5 new tests in `GXT in-place updates`, all green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…efinition is the same `<Foo @value={{modifier this.foo "x"}} />` curried a new modifier on every read of the argument: the compat compiler re-evaluates the call each time. The sync reads every argument and writes a changed value back, so each sync saw a new `@value`, wrote it, re-ran the element's modifier (whose manager then updated it, rewriting the element's content) and scheduled the next sync. The renderer never settled. Glimmer's curry reference returns the same curried value while the definition it curries stays the same; the curried arguments are references the modifier reads when it installs or updates. A curried modifier now records the definition it curries and installs through a replaceable implementation (utils.ts `createCurriedModifierValue`). An argument slot that holds one keeps it when a later evaluation curries the same definition, and takes that evaluation's implementation (`keepArgValue`, which also covers the `(fn …)` values the slots already kept). A rerender without a change leaves the modifier alone; an argument change still updates it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`(modifier @modifier "world")` with `@modifier` undefined fell through to gxt's own helper, which returns a new function that calls the missing definition. An argument holding it was truthy and changed on every read: each sync wrote it back and scheduled another one, even for a component whose render was already torn down, and the extra sync left the next render's update one sync late. Glimmer's curry reference is `null` for a missing definition, and an element modifier of that value installs nothing. The `(modifier)` keyword now returns `undefined` for a `null` / `undefined` definition. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Compat full run: 9461/10085 (was 9453/10083), 982 modules, no timeouts, no harness errors, no green->red. 6 tests turned green: - `[integration] jit :: dynamic modifiers in resolution mode` "Can pass curried modifier as argument and invoke dynamically", and its "(with args)", "(with args, multi-layer)", "(with named args)" and "(with named args, multi-layer)" forms: a curried `(modifier …)` argument keeps its value while its definition is the same, so a rerender without a change leaves the modifier and its element alone. - `[integration] jit :: strict mode: dynamic template values` "Calling a dynamic modifier using if helper": a `(modifier)` of a missing definition is no modifier, so the argument of a finished render no longer changes on every sync and delays the next render's update. 2 new tests in `GXT in-place updates`, both green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A content mustache inside an element (`<div>{{this.value}}</div>`,
`<div>a{{this.value}}b</div>`, the same inside `<svg>`) rendered every value
as text: a SafeString showed its HTML escaped, a DOM node or fragment showed
`[object HTMLParagraphElement]`. A SafeString was rendered as HTML only when
it was the value at creation, and the text of a mustache inside an SVG
element or a foreignObject never updated. A DOM node in `{{{this.value}}}`
rendered its String() as HTML.
In Glimmer, dynamic content renders what its value is after every change:
text (updated in its text node), the HTML of a SafeString (parsed in the
element it is in, so it is SVG inside an SVG element), a DOM node, or the
nodes of a fragment. A change of kind, of the node or of the HTML replaces
the content where it is.
The content mustaches of an element are now rendered as that dynamic
content: the function children of an HTML element, the mustache that is an
element's only content, and the children of SVG, MathML and custom elements.
The binding is evaluated once when it is created, when gxt renders the child,
so children are still evaluated in order after the element's attributes. A
block's render result passed as a child is rendered by gxt as before. The
content of a component definition value renders the component, as it already
did at the top level of a template, which now shares the same binding. A
DOM node in triple curlies is inserted as it is.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ue is
A content mustache at the top level of a template (`[{{this.value}}]`)
kept the kind of its first value: text that changed to a SafeString showed
the HTML escaped, a SafeString that changed to text was parsed as HTML, and
after a change to a DocumentFragment the binding stopped updating.
It now renders through the same dynamic content binding as the content of
an element, which renders what the value is after every change (text in its
text node, the HTML of a SafeString, a DOM node, a component). The binding
is still evaluated once when it is created. A DOM node that is the first
value is still inserted as it is, since a component render result reaches
this position as a node too.
The text of an object value is now taken in the tracking frame of the
binding's evaluation, so a binding depends on what the object's `toString`
reads, such as the value of a yielded block parameter (`{{yield this.bar}}`
rendered as `{{bar}}`).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A helper invoked in content position by name (`{{format-html this.text}}`,
`{{const-foobar}}`) rendered its result with String(): a SafeString showed
its HTML escaped and a DOM node showed `[object Text]`. In Glimmer a
helper's result is dynamic content like any other value.
The result now renders through the dynamic content binding: text, the HTML
of a SafeString, a DOM node or a component, rendered again when what the
helper read changes. The helper is still evaluated once on creation, and the
binding is still torn down with the enclosing branch.
The fragment the binding returns now carries its current nodes as its
rendered nodes, so a gxt block that registers it as one of its nodes removes
what it renders when the block is destroyed. Trusted HTML rendered inside an
SVG or MathML namespace before its element exists is parsed in that
namespace.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lled
The mustache that is an element's only content (`<div {{replace}}>Nope</div>`,
`<p {{peek}}>{{this.value}}</p>`) became the element's children when it was
made dynamic content. gxt installs an element's modifiers while it creates
the element, before it renders the element's children, so the modifier saw
an empty element, and a modifier that rewrote the element's HTML got the
content appended after its own (`[Hello, world!Nope]`).
In Glimmer a modifier is installed after the element's content is rendered.
When the element has modifiers, its content is now rendered into it by an
entry ahead of the modifiers, as the text was before, so a modifier sees it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… value is Compat full run: 9495/10097 (was 9461/10085), 983 modules, no timeouts, no harness errors, no green->red. 22 tests turned green: - the 18 `[integration] jit :: Updating - Content` tests (double and triple curlies in the HTML, SVG foreignObject and SVG contexts): a content mustache renders text, the HTML of a SafeString or a DOM node and renders what its value is after every change; a DOM node in triple curlies is inserted as it is; - `[integration] jit :: Updating` "updating a curly with a safe and unsafe string"; - `[integration] jit :: Updating` "double curlies with const SafeString" and "double curlies with const Node": the result of a helper invoked by name renders what it is; - `Components test: curly components` "static arbitrary number of positional parameters": a component invoked by name with positional arguments as the only content of an element renders the nodes it returns. 12 new tests in `GXT dynamic content`, all green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s value is
In a .gjs/.gts template, a `{{value}}` in an element (`<p>{{this.value}}</p>`,
`<p>a{{this.value}}b</p>`) rendered every value as text: a SafeString showed
its HTML escaped, a DOM node or fragment showed `[object HTMLElement]`, and
inside SVG the HTML of a SafeString was not rendered at all. A block parameter
holding a SafeString or a node (`<Show @value={{this.value}} as |v|><p>{{v}}</p>`)
rendered as text too. Runtime-compiled templates already rendered Glimmer's
dynamic content there; build-time templates call another `$_tag`, which handed
the content to gxt's text binding.
That `$_tag` now creates a plain element with its content mustaches rendered as
dynamic content, as for a runtime-compiled template: text updated in its text
node, the HTML of a SafeString, a DOM node, or the nodes of a fragment, and a
change of kind replaces the content in place. gxt picks an element's namespace
from its render context, so a mustache among the children holds its place
while gxt creates the element and renders there once the element exists: the
HTML of a SafeString in an SVG element is SVG. The content is still rendered
after the element's attributes, and ahead of its modifiers when it is the
element's only content, so a modifier sees it when it is installed. A block
parameter is a gxt cell; its value is read where the content is rendered. An
element whose tag is dynamic (the `(element)` helper) keeps gxt's rendering of
its children, which gxt renders again when the tag changes.
A `{{value}}` at the top level of a template or of a block still renders as
text in a build-time template: gxt renders it with its own text binding, which
has no host seam outside the gxt-native build. The new tests for those two
positions are skipped in compat; classic and gxt-native run them, and the
module joins the gxt-native suite.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…through the property
`<button name={{false}}>` rendered `<button>`: `name` is an attribute to gxt,
and `false` removes an attribute. Glimmer sets a bound attribute that names a
property of its HTML element through that property (`normalizeProperty`), so
the attribute is the text the property reflects: `name="false"`, as `id`,
`title` or `type` reflect any value. A bound attribute that is no property of
its element (`data-*`, `aria-*`, `name` of a `<span>`) is an attribute, where
`true` is the empty string; compat wrote `"true"`. A bound `selected` of an
`<option>` (and `value` of an `<input>`, `checked` of a checkbox) also got an
attribute next to the property in a runtime-compiled template, so
`selected={{true}}` rendered `<option selected="true">`.
What an attribute write leaves on the element is now one rule, shared by both
builds' attribute hooks: null and undefined remove the attribute, text is the
attribute's text, any other value of a property of the element is assigned to
the property and the attribute is what it reflects, and any other value of an
attribute follows Glimmer's normalizeValue. A runtime-compiled template no
longer adds an attribute for a bound value gxt set through a property of the
element; a name that is no property of the element is still written as an
attribute. The gxt-native build may now ship Glimmer's normalizeProperty, a
DOM module with no VM dependency.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aches and bound attributes that are properties
Compat full run: 9545/10108 (was 9495/10097), 985 modules, no timeouts, no
harness errors, no green->red. 39 tests turned green: a bound attribute that
names a property of its element is set through the property
(`<button name={{has-block}}>` renders `name="false"`: the parameterized
has-block and has-block-params `(prop, ...)` tests of the Curly, Glimmer and
Dynamic suites and their parent modules), and a bound `selected` of an option
is its selectedness, with no attribute (Updating: `<option selected>` is
normalized and updated correctly; initial render (client): Dynamic selected
options, Dynamic multi-select). 11 new tests, green or skipped: the two new
`<template>` modules (content mustache, bound attributes) and two yielded block
parameter tests in GXT dynamic content. The 2 skipped tests are a content
mustache outside an element in a build-time template, which gxt still renders
with its own text binding under GXT_MODE.
Native full run (the GXT native CI job of the fix commit): 7141/7185 (was
7132/7176), 683 modules, no green->red, no timeouts: the 9 tests of the two new
`<template>` modules pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ver invoked it
gxt's `$_args` hands a component each argument as a thunk value
(`{ value: () => this.greet }`). A template-only definition invoked
through a path (`<this.Apply @value={{this.greet}} />`), with
`{{component this.Apply …}}`, or curried and then invoked, was rendered
with that object as it was, so `@value` was the thunk. A content
mustache calls a function and showed the value, but a modifier argument
used in modifier position (`<div {{@value "x"}}>`) was never installed,
a helper argument called as `{{@value "x"}}` never ran, and an `{{on}}`
handler argument returned the handler instead of calling it.
The template-only render now gives the template an args object where
such a thunk is a getter that reads it, through the argument's value
slot, as the args proxy of a component rendered by name reads it. The
two copies of the template-only render become one function.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GXT `renderComponent` path defined every root argument (the args handed to a manager-rendered root, a custom manager's named args, a component's `this.args`, a template-only root's context and its `@` args) as a getter that read the classic-validator bridge cell before the value. Every classic tag change bumps that cell, so a binding that read `@foo` ran again on any change anywhere, such as a write to another argument of a `trackedObject`, and once more in the first sync after the render. Glimmer reads a root argument as `childRefFor` of the args' const reference, the environment's `getProp`: the value, and the key's tag. The getters now read through gxt-backend's `gxtLib.readProperty`, which reads the property once and consumes its gxt property tag, as gxt's `$__tag` does (the validator shim's `tagFor` would create a formula over the property, which reads it a second time). A binding depends on that argument only: a `trackedObject` write to it, or `set()` on a plain args object, still updates it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…it reads changes
A component rendered by name gets its arguments as getters. The args
proxy kept each one in a cell that the sync's arg-cell pass updates
from the getter, and that pass keys its entries by component instance.
A template-only component has no instance, so every template-only
render replaced the previous one's entry: only the last template-only
component rendered (the last `{{#each}}` row, or the second of two
side by side) saw an argument change. In `{{#each this.rows as |root|}}
<FooBar @value={{root.value}} />{{/each}}`, writing the first row's
tracked `value` left it stale; a Glimmer component in the same place
updated.
A component without an instance now reads such an argument where its
template uses it, through the argument's value slot, as Glimmer reads
an argument reference: the binding that reads `@value` depends on what
the argument's expression reads. Template-only components no longer
have entries in the arg-cell pass, so a destroyed one is no longer read
on every sync either.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oot arguments read their values Taken from a GitHub Actions GXT full run on this code. 9556/10118 green (was 9545/10108), 987 modules, no timeouts, no harness errors, no green->red. `#in-element: Inside a loop` turned green; 10 new GXT tests (component arguments, renderComponent root arguments) are green. The native suite is unchanged (7141/7185). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
runner.mjs exits 1 whenever any test fails, including the tests the baseline already lists as failing, so the scheduled full run reported a failure on every night even when it said "baseline ok". The verdict now comes from baseline-gate.mjs, as in the native workflow: no test red that the baseline lists green, no baseline module missing, no timeout or harness error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… "inverse"
Glimmer gives a component's `{{else}}` block, `<:else>` and `<:inverse>`
one name, so `{{yield to="else"}}` and `{{yield to="inverse"}}` both
render it. The GXT compiler names a curly component's `{{else}}` block
`inverse`, and its `$_slot` finds a block by its exact name: a curly
component's `{{yield to="else"}}` rendered nothing. The compiler also
built a `{{#component}}` block's component from the main block alone, so
the `{{else}}` block of a dynamic invocation was dropped: neither yield
rendered it and `(has-block "inverse")` was false.
A dialect AST transform now gives the block the one name `inverse`:
`{{yield to="else"}}` yields to `inverse`, `<:else>` is `<:inverse>`, and
a `{{#component}}` block with an `{{else}}` block passes the two blocks
as `<:default>` (with the block params) and `<:inverse>` named blocks.
A bare `@arg` component keeps its block: its `<@arg>` invocation has no
named blocks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In Glimmer a tag that names a block param in scope invokes the param's value as a component, whatever its case: `<Yielder as |comp|><comp /></Yielder>` renders the yielded component. The block-params transform rewrote a param used as a tag only in the dotted form (`<comp.Foo>` reads the yielded `comp`), so `<comp>` was left as a plain element and rendered `<comp></comp>`. It now rewrites the param tag itself to the same yielded-value read, `<this.$_bp0>`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The quoted-attribute transform turned every bare `{{name}}` in a quoted
attribute value into a helper call `{{(name)}}`, so that a helper named
there resolves. A block param is a local variable, not a helper name,
and Glimmer reads it as the variable. As a helper call its value went
through helper resolution and a falsy one was lost:
`class="index-{{index}}"` in an `{{#each}}` row rendered `index-` for
the first row while `{{index}}` rendered 0.
The transform now keeps a mustache naming a block param in scope as it
is. It tracks block params as Glimmer scopes them: a block's params cover
its body, an element's params its children, not its own attributes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Taken from a GitHub Actions GXT full run on this code. 9584/10125 green (was 9556/10118), 989 modules, no timeouts, no harness errors, no green->red, no module with fewer passing assertions. 11 tests turned green: yield to "else" (curly components, yield Curly and Dynamic), yield to "inverse" (Dynamic), the five Dynamic parameterized has-block (..., else) when else supplied tests, and has-block within a yielded curried component invoked with angle brackets (truthy, falsy). The 7 new GXT tests (else blocks, block params) are green. The two each-index Updating tests stay red on other assertions (`key='@index'` keying, a keyed row reused for a new object), with 31 more of the module's assertions passing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Owner
Author
|
Closing: CI for this branch now runs on scratch branches before each push, so this tracking PR is no longer used. |
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.
Fork-internal PR used only to run the full CI (classic Basic Test, variants, lint, types, GXT workflows) on the
glimmer-next-freshbranch of emberjs#21340. Baseci-base-gxt= the merge-base with upstream main, so CI tests the branch exactly. Do not merge.🤖 Generated with Claude Code