feat: add Lit adapter at full matrix parity#7
Conversation
Ship @stainless-code/lit-layers with controllers, @lit/context, light-DOM CEs, docs, skill, and recipes so Lit/WC apps match other adapters.
Reactive CE props, shadow stack-provider, createStackHook DOM tests, and docs parity so the seventh adapter matches Lit idioms and the public site.
Landing parity/hero counts include Lit; StackOutlet rebinds on context client swap; unit test covers validated useLayer parsed payload.
Lit ships as a first-class adapter; keep the comparison matrices honest.
LayerController deferred StackControllers until a single resolveClientLazy callback, cutting three ContextConsumers to one for lazy useLayer hosts.
Defer #states until one resolveClientLazy callback, bindClient + requestUpdate so outlet() no longer depends on StackController coincidentally re-rendering.
Core skill, glossary, CONTRIBUTING peers, and architecture DOM-harness notes were still six-adapter shaped after the Lit package landed.
StackController ignored later context pushes once bound; share one resolve for useAppStack, cover upsert/void/id dismiss, and sync Lit into issue templates and architecture language.
Pass explicit createStackHook clients into StackController so open and states cannot diverge; share resolve on useStackHandles; add headless DOM coverage and close remaining Lit docs roster gaps.
Pin StackOutlet instance stability across actionStatus updates and add Lit to headless-rendering / nested-overlays guide surfaces.
Delete+lift remaining Lit locks into architecture, drop roadmap pending rows, and record the new @stainless-code/lit-layers package for release.
🦋 Changeset detectedLatest commit: 9aae20e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Review limit reached
Next review available in: 39 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughThis PR adds ChangesLit adapter integration
Estimated code review effort: 5 (Critical) | ~120 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Split Lit out of the React-shaped adapter-hooks matrix, fix nested group init, restore verbatim experimental copy, and slim StackController JSDoc so the design note lives on the API surface.
Lift the layerClientContext export rationale into JSDoc and keep the harden ledger as a one-line suppress.
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
packages/lit/tests-dom/layer-handles.test.ts (2)
149-167: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueMove custom element definition to the module scope.
Defining custom elements inside an
itblock risks triggering aDOMException(registry collisions) if the test suite is ever re-run in watch mode without a complete JavaScript environment teardown.Move the
StackProbeclass and thecustomElements.definecall outside thedescribeblock, alongsideConfirmDialogandLayerHost.♻️ Proposed refactor
- class StackProbe extends LitElement { - `#stack` = useStack(this, { - stack: "confirm", - select: (s) => s.length, - }); - - createRenderRoot(): this { - return this; - } - - get count(): number { - return this.#stack.current; - } - - render() { - return html`<span>${this.#stack.current}</span>`; - } - } - customElements.define("test-stack-probe", StackProbe);Note: Place the extracted class definition at the root module scope of this file.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/lit/tests-dom/layer-handles.test.ts` around lines 149 - 167, Move the StackProbe class and its customElements.define call from the test block to the module scope, alongside ConfirmDialog and LayerHost. Keep its useStack configuration, createRenderRoot, count getter, and render behavior unchanged, and ensure the tests reference the module-scoped custom element.
89-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
afterEachfor robust DOM cleanup.If a test assertion fails, execution immediately exits before reaching the manual
document.body.removeChild(...)cleanup step. This can leave orphaned elements in the DOM, polluting subsequent tests.Consider importing
afterEachfromvitestand using it to reliably clear the DOM, which allows you to safely remove the manual cleanup lines at the end of every test.♻️ Proposed refactor
-describe("Lit adapter — useLayer lazy client", () => { +describe("Lit adapter — useLayer lazy client", () => { + afterEach(() => { + document.body.innerHTML = ""; + }); + it("dispatches one layer-client context-request on the useLayer host", async () => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/lit/tests-dom/layer-handles.test.ts` around lines 89 - 90, Use Vitest’s afterEach in the “Lit adapter — useLayer lazy client” test suite to reliably clean up DOM elements even when assertions fail. Move the shared document.body cleanup from each test’s manual teardown into the afterEach hook, then remove the redundant per-test cleanup lines.docs/architecture.md (1)
39-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReorder table columns to match the canonical framework list.
As per coding guidelines, the adapter list order for every multi-adapter listing should be:
core/vanilla → react → preact → solid → angular → vue → lit → svelte. The current matrix hasVueprecedingSolidandAngular.♻️ Proposed refactor
-| Capability | React | Preact | Vue | Solid | Angular | Lit | Svelte (runes) | Svelte (store) | -| ------------------------------------------------------ | :---: | :----: | :-: | :---: | :-----: | :-: | :------------: | :------------: | -| Primitives + core re-export | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `StackOutlet` (built-in host) | ✅ | ✅ | ✅ | ✅ | ✅¹ | ✅ | —⁴ | —⁴ | -| `StackSubscribe` (render-prop / scoped slot) | ✅ | ✅ | ✅ | ✅ | —² | ✅ | —⁴ | —⁴ | -| `useStackHandles` (headless `{ states, getCall }`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | —⁵ | —⁵ | -| `useMutationFlow` (async-action helper) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `useLayerGroup` (nested-stack hook) | ✅ | ✅ | ✅ | ✅ | ✅³ | ✅ | ✅⁶ | ✅⁶ | -| `createStackHook` (`useAppStack`/`AppHost`/`AppLayer`) | ✅ | ✅ | ✅ | ✅ | ✅³ | ✅ | —⁴ | —⁴ | +| Capability | React | Preact | Solid | Angular | Vue | Lit | Svelte (runes) | Svelte (store) | +| ------------------------------------------------------ | :---: | :----: | :---: | :-----: | :-: | :-: | :------------: | :------------: | +| Primitives + core re-export | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `StackOutlet` (built-in host) | ✅ | ✅ | ✅ | ✅¹ | ✅ | ✅ | —⁴ | —⁴ | +| `StackSubscribe` (render-prop / scoped slot) | ✅ | ✅ | ✅ | —² | ✅ | ✅ | —⁴ | —⁴ | +| `useStackHandles` (headless `{ states, getCall }`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | —⁵ | —⁵ | +| `useMutationFlow` (async-action helper) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `useLayerGroup` (nested-stack hook) | ✅ | ✅ | ✅ | ✅³ | ✅ | ✅ | ✅⁶ | ✅⁶ | +| `createStackHook` (`useAppStack`/`AppHost`/`AppLayer`) | ✅ | ✅ | ✅ | ✅³ | ✅ | ✅ | —⁴ | —⁴ |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/architecture.md` around lines 39 - 49, Reorder the capability matrix columns in the architecture table to the canonical sequence: core/vanilla, React, Preact, Solid, Angular, Vue, Lit, Svelte. Update the header, separator, every capability row, and associated footnote alignment without changing any capability values or footnote text.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/docs/content/adapters/lit.mdx`:
- Around line 31-40: The Lit examples create duplicate ContextProvider
controllers on reconnect by calling provideLayerClient in connectedCallback. In
apps/docs/content/adapters/lit.mdx lines 31-40, move provideLayerClient(this) to
a class field on MyShell and remove the connectedCallback initialization. In
apps/docs/recipes/confirm-dialog/lit.ts lines 89-93, likewise initialize the
provider as a class field and lift defineStackElements() to module scope.
In `@packages/lit/src/index.test.ts`:
- Around line 377-396: Update the useMutationFlow success test to await the
promise returned by run(...).orEnd("ok") instead of tracking the locally
assigned resolved flag. After it completes, assert flow.pending is false and
verify the host layer was removed, using the existing host/layer inspection API.
In `@packages/lit/src/index.ts`:
- Around line 1192-1245: Update the client/context handling in `#init`, updated,
and connectedCallback to track the latest context client separately from the
explicit client. Ignore context callbacks while .client is set, preserve
explicit-client precedence during provider swaps, and when .client is cleared
rebind to the most recent context client.
- Around line 1620-1624: Update defineStackElements to validate existing
registrations: when customElements.get(tag) returns a constructor, compare it
with the expected ctor and throw a descriptive integration error if they differ;
retain definition for unregistered tags and accept registrations already owned
by the same ctor.
- Around line 804-817: Update the MutationRun implementation returned by run so
concurrent orEnd invocations cannot prematurely clear shared state or call
this.#call.end while another operation remains active. Either reject a new
invocation while one is in flight, or track active runs with a count and only
reset `#pending`, update the host, and finalize the call after the last run
completes; preserve cleanup for rejected or failed operations.
- Around line 1520-1533: Update the open flow in the method containing `#open`,
`#opened`, and `#pendingOpen` so synchronous throws from this.#client.open and
asynchronous promise rejections both reset `#opened` to false and `#pendingOpen` to
null, preserving retryability. Attach rejection handling to the pending promise
so failures do not become unhandled rejections, while retaining the existing
successful-response guard and onResolved behavior.
- Around line 1440-1458: Update AppLayerController so assignments to payload,
options, and onResolved synchronize the currently mounted layer whenever it is
open, using accessors or an explicit update method rather than plain fields.
Ensure options.key changes do not alter the key used to dismiss the
already-owned layer; retain and use the original mounted-layer identity for
later dismissal.
- Around line 1473-1477: Update the controller’s layer lifecycle around the
client-resolution branch and the close/disconnect paths to retain the exact
client and unique layer identity returned when this controller opens its layer.
When the context client changes, dismiss the previously owned layer before
replacing `#client`, then reopen or resynchronize on the new client as
appropriate. Replace stack.find(key)-based dismissal with operations targeting
only the stored layer instance, and clear the stored ownership state when the
layer is closed or disconnected.
- Around line 1551-1555: Update StackHook.AppLayer and BoundAppLayer so their
constructors/classes remain generic over payload and response types instead of
collapsing them to unknown; ensure new AppLayer(this, { payload, onResolved })
preserves typed P/R inference through the returned hook, and add the
corresponding assertion in inference.test-d.ts.
In `@packages/lit/tests-dom/stack-outlet.test.ts`:
- Around line 209-230: Strengthen the “stops updating after disconnect” test
around provider.remove() by opening the “After disconnect” layer, waiting for
any relevant update completion, and asserting the detached outlet does not
render that content. Keep the existing no-throw assertion, but verify the
outlet’s rendered output remains unchanged after disconnection.
---
Nitpick comments:
In `@docs/architecture.md`:
- Around line 39-49: Reorder the capability matrix columns in the architecture
table to the canonical sequence: core/vanilla, React, Preact, Solid, Angular,
Vue, Lit, Svelte. Update the header, separator, every capability row, and
associated footnote alignment without changing any capability values or footnote
text.
In `@packages/lit/tests-dom/layer-handles.test.ts`:
- Around line 149-167: Move the StackProbe class and its customElements.define
call from the test block to the module scope, alongside ConfirmDialog and
LayerHost. Keep its useStack configuration, createRenderRoot, count getter, and
render behavior unchanged, and ensure the tests reference the module-scoped
custom element.
- Around line 89-90: Use Vitest’s afterEach in the “Lit adapter — useLayer lazy
client” test suite to reliably clean up DOM elements even when assertions fail.
Move the shared document.body cleanup from each test’s manual teardown into the
afterEach hook, then remove the redundant per-test cleanup lines.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a09dc40f-f653-4873-9164-4f30b67ac192
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (53)
.agents/skills/docs-voice/SKILL.md.agents/skills/harden-pr/LEDGER.md.agents/skills/improve-codebase-architecture/LANGUAGE.md.changeset/lit-adapter.md.github/CONTRIBUTING.md.github/ISSUE_TEMPLATE/1_bug.yml.github/ISSUE_TEMPLATE/2_feature_adapter.yml.size-limit.jsonREADME.mdapps/docs/blume.config.tsapps/docs/content/adapters/index.mdxapps/docs/content/adapters/lit.mdxapps/docs/content/adapters/meta.tsapps/docs/content/concepts/comparison.mdxapps/docs/content/concepts/glossary.mdxapps/docs/content/concepts/overview.mdxapps/docs/content/concepts/stability.mdxapps/docs/content/concepts/when-to-use.mdxapps/docs/content/examples/confirm-dialog.mdxapps/docs/content/guides/headless-rendering.mdxapps/docs/content/guides/install.mdxapps/docs/content/guides/nested-overlays.mdxapps/docs/content/reference/adapter-hooks.mdxapps/docs/content/reference/core-api.mdxapps/docs/content/reference/index.mdxapps/docs/content/reference/migration.mdxapps/docs/content/reference/roadmap.mdxapps/docs/package.jsonapps/docs/pages/_home/FrameworkTabs.astroapps/docs/pages/_home/Hero.astroapps/docs/pages/_home/UseCases.astroapps/docs/pages/_home/adapter-snippets.tsapps/docs/recipes/confirm-dialog/lit.tsapps/docs/tsconfig.recipes-lit.jsondocs/architecture.mddocs/glossary.mddocs/plans/lit-adapter.mddocs/roadmap.mdknip.jsonpackages/core/skills/layers/SKILL.mdpackages/lit/README.mdpackages/lit/package.jsonpackages/lit/skills/lit-layers/SKILL.mdpackages/lit/src/index.test.tspackages/lit/src/index.tspackages/lit/tests-dom/create-stack-hook.test.tspackages/lit/tests-dom/headless.test.tspackages/lit/tests-dom/inference.test-d.tspackages/lit/tests-dom/layer-handles.test.tspackages/lit/tests-dom/stack-outlet.test.tspackages/lit/tsconfig.jsonpackages/lit/tsdown.config.tspackages/lit/vitest.config.ts
💤 Files with no reviewable changes (3)
- docs/plans/lit-adapter.md
- apps/docs/content/reference/roadmap.mdx
- docs/roadmap.md
| describe("useMutationFlow (lit)", () => { | ||
| it("drives pending and ends the layer on success", async () => { | ||
| const client = new LayerClient(); | ||
| const { host } = createHost(); | ||
| const call = makeCall(client); | ||
| const flow = useMutationFlow<unknown, unknown>(host, call); | ||
| expect(flow.pending).toBe(false); | ||
| let resolved = false; | ||
| void flow | ||
| .run(async () => { | ||
| await Promise.resolve(); | ||
| }) | ||
| .orEnd("ok"); | ||
| // pending flips true synchronously, then the layer ends | ||
| expect(flow.pending).toBe(true); | ||
| await Promise.resolve(); | ||
| await Promise.resolve(); | ||
| resolved = true; | ||
| expect(resolved).toBe(true); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the mutation-flow result instead of a local flag.
resolved is assigned by the test itself, so this passes even if the layer never ends or pending never resets. Await orEnd(), then assert flow.pending === false and that the layer was removed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.test.ts` around lines 377 - 396, Update the
useMutationFlow success test to await the promise returned by
run(...).orEnd("ok") instead of tracking the locally assigned resolved flag.
After it completes, assert flow.pending is false and verify the host layer was
removed, using the existing host/layer inspection API.
| run = (fn: () => Promise<void> | void): MutationRun<R> => ({ | ||
| orEnd: async (response: R) => { | ||
| this.#pending = true; | ||
| this.#host.requestUpdate(); | ||
| this.#call.setRunning(true); | ||
| try { | ||
| await fn(); | ||
| this.#call.end(response); | ||
| } finally { | ||
| this.#call.setRunning(false); | ||
| this.#pending = false; | ||
| this.#host.requestUpdate(); | ||
| } | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Prevent overlapping mutation runs.
Two concurrent orEnd() calls share one boolean. The first completion resets pending/actionStatus and ends the layer while the second operation is still running. Reject overlapping runs or track an in-flight count.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.ts` around lines 804 - 817, Update the MutationRun
implementation returned by run so concurrent orEnd invocations cannot
prematurely clear shared state or call this.#call.end while another operation
remains active. Either reject a new invocation while one is in flight, or track
active runs with a count and only reset `#pending`, update the host, and finalize
the call after the last run completes; preserve cleanup for rejected or failed
operations.
| #init(client: LayerClient): void { | ||
| if (this.#states === null) { | ||
| this.#clientRef = client; | ||
| this.#stack = client.getStack(this.stack); | ||
| this.#states = new StackController( | ||
| this, | ||
| { stack: this.stack, client }, | ||
| undefined, | ||
| false, | ||
| ); | ||
| this.requestUpdate(); | ||
| return; | ||
| } | ||
| // Context provider may push a new client via setValue — rebind. | ||
| if (client === this.#clientRef) return; | ||
| this.#clientRef = client; | ||
| this.#states.reconfigure({ stack: this.stack }, client); | ||
| this.#stack = client.getStack(this.stack); | ||
| this.requestUpdate(); | ||
| } | ||
|
|
||
| updated(changed: PropertyValues<this>): void { | ||
| if (this.#stack === null || this.#states === null) return; | ||
|
|
||
| if ( | ||
| changed.has("client") && | ||
| this.client && | ||
| this.client !== this.#clientRef | ||
| ) { | ||
| this.#clientRef = this.client; | ||
| this.#states.reconfigure({ stack: this.stack }, this.client); | ||
| this.#stack = this.client.getStack(this.stack); | ||
| this.requestUpdate(); | ||
| return; | ||
| } | ||
|
|
||
| if (changed.has("stack") && this.#clientRef) { | ||
| this.#states.reconfigure({ stack: this.stack }, this.#clientRef); | ||
| this.#stack = this.#clientRef.getStack(this.stack); | ||
| this.requestUpdate(); | ||
| } | ||
| } | ||
|
|
||
| connectedCallback(): void { | ||
| super.connectedCallback(); | ||
| if (this.client) { | ||
| this.#init(this.client); | ||
| } else if (this.#stack === null) { | ||
| new ContextConsumer(this, { | ||
| context: layerClientContext, | ||
| subscribe: true, | ||
| callback: (c: LayerClient) => this.#init(c), | ||
| }); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Preserve explicit .client precedence over context updates.
After initialization from context, assigning .client switches correctly, but the retained context consumer can later call #init() and overwrite that explicit client during a provider swap. Track the latest context client separately, ignore context pushes while .client is set, and fall back to context when it is cleared.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.ts` around lines 1192 - 1245, Update the
client/context handling in `#init`, updated, and connectedCallback to track the
latest context client separately from the explicit client. Ignore context
callbacks while .client is set, preserve explicit-client precedence during
provider swaps, and when .client is cleared rebind to the most recent context
client.
| /** | ||
| * Reactive controller for a controlled layer bound to one stack. Set `.open` to | ||
| * `true` to open and `false` to dismiss; `.payload` / `.options` / `.onResolved` | ||
| * update the live layer. Dismisses automatically when the host disconnects. | ||
| */ | ||
| export class AppLayerController< | ||
| P = unknown, | ||
| R = unknown, | ||
| > implements ReactiveController { | ||
| #client: LayerClient | undefined; | ||
| #stackId: string; | ||
| #opened = false; | ||
| #connected = false; | ||
| #pendingOpen: Promise<R> | null = null; | ||
|
|
||
| options: OmitKeyof<LayerOptions<P, R>, "stack">; | ||
| #open: boolean; | ||
| payload: P; | ||
| onResolved?: (response: R) => void; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Implement the documented live updates for payload, options, and onResolved.
These are plain fields, so assignments while the layer is open never update the mounted layer. Changing options.key also makes later dismissal search for a different key. Add setters or an explicit update method that synchronizes the owned layer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.ts` around lines 1440 - 1458, Update
AppLayerController so assignments to payload, options, and onResolved
synchronize the currently mounted layer whenever it is open, using accessors or
an explicit update method rather than plain fields. Ensure options.key changes
do not alter the key used to dismiss the already-owned layer; retain and use the
original mounted-layer identity for later dismissal.
| } else if (isElementHost(host)) { | ||
| resolveClientLazy(host, undefined, (c) => { | ||
| this.#client = c; | ||
| if (this.#connected) this.#sync(); | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Track the exact layer instance and client owned by this controller.
A context swap replaces #client without dismissing/reopening the layer on the old client. Close and disconnect also use stack.find(key), which can dismiss another controller’s same-key layer. Store the opened layer’s client and unique identity, then operate only on that instance.
Also applies to: 1495-1515, 1534-1539
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.ts` around lines 1473 - 1477, Update the controller’s
layer lifecycle around the client-resolution branch and the close/disconnect
paths to retain the exact client and unique layer identity returned when this
controller opens its layer. When the context client changes, dismiss the
previously owned layer before replacing `#client`, then reopen or resynchronize on
the new client as appropriate. Replace stack.find(key)-based dismissal with
operations targeting only the stored layer instance, and clear the stored
ownership state when the layer is closed or disconnected.
| if (this.#open && !this.#opened) { | ||
| this.#opened = true; | ||
| const pending = this.#client.open({ | ||
| ...this.options, | ||
| stack: this.#stackId, | ||
| payload: this.payload, | ||
| } as OpenLayerOptions<P, R>) as Promise<R>; | ||
| this.#pendingOpen = pending; | ||
| void pending.then((response) => { | ||
| if (this.#pendingOpen !== pending) return; | ||
| this.#opened = false; | ||
| this.#pendingOpen = null; | ||
| this.onResolved?.(response); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Reset controller state when open() fails.
A synchronous throw leaves #opened = true, while a rejected promise leaves both state fields stuck and creates an unhandled rejection from the ignored .then(). Handle both failure paths and restore retryable state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.ts` around lines 1520 - 1533, Update the open flow in
the method containing `#open`, `#opened`, and `#pendingOpen` so synchronous throws
from this.#client.open and asynchronous promise rejections both reset `#opened` to
false and `#pendingOpen` to null, preserving retryability. Attach rejection
handling to the pending promise so failures do not become unhandled rejections,
while retaining the existing successful-response guard and onResolved behavior.
| /** Controlled-layer controller constructor bound to the hook's stack. */ | ||
| AppLayer: new ( | ||
| host: ReactiveControllerHost, | ||
| props: AppLayerProps<unknown, unknown>, | ||
| ) => AppLayerController; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant type and nearby tests.
rg -n "AppLayer: new|AppLayerProps|AppLayerController|BoundAppLayer|StackHook" packages/lit/src packages/lit/test -S
# Show the surrounding source for the reported lines.
sed -n '1510,1615p' packages/lit/src/index.ts
# Show the type-level tests if present.
fd -a "inference.test-d.ts|*.test-d.ts" packages/lit -t fRepository: stainless-code/layers
Length of output: 1313
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the full relevant source sections with line numbers.
cat -n packages/lit/src/index.ts | sed -n '1436,1620p'
# Find type-level tests anywhere in the repo.
fd -a "inference.test.d.ts|*.test-d.ts|*.test.ts" . -t f
# Search for usages of createStackHook/AppLayer in tests or examples.
rg -n "createStackHook\\(|useAppStack|AppLayer\\b" . -g '!**/dist/**' -g '!**/build/**' -SRepository: stainless-code/layers
Length of output: 6965
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List declaration/type tests and examples in the repo.
fd -a -t f '.*(test|spec).*\.(d\.ts|ts|tsx)$' .
fd -a -t f '.*\.d\.ts$' .
# Narrow search for StackHook/AppLayer usages in tests/examples.
rg -n "createStackHook\\(|useAppStack|AppLayerProps<|AppLayerController<" . \
-g '!**/dist/**' -g '!**/build/**' -g '!**/coverage/**' -SRepository: stainless-code/layers
Length of output: 18761
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lit inference test =="
cat -n packages/lit/tests-dom/inference.test-d.ts | sed -n '210,260p'
echo "== lit create-stack-hook test =="
cat -n packages/lit/tests-dom/create-stack-hook.test.ts | sed -n '105,240p'Repository: stainless-code/layers
Length of output: 6026
Preserve AppLayer generics on the bound constructor. StackHook.AppLayer and BoundAppLayer still collapse P/R to unknown, so new AppLayer(this, { payload, onResolved }) loses typed payload/response flow. Make the constructor and bound class generic, and add a packages/lit/tests-dom/inference.test-d.ts assertion for the returned hook.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.ts` around lines 1551 - 1555, Update
StackHook.AppLayer and BoundAppLayer so their constructors/classes remain
generic over payload and response types instead of collapsing them to unknown;
ensure new AppLayer(this, { payload, onResolved }) preserves typed P/R inference
through the returned hook, and add the corresponding assertion in
inference.test-d.ts.
| export function defineStackElements(): void { | ||
| if (typeof customElements === "undefined") return; | ||
| for (const [tag, ctor] of STACK_ELEMENTS) { | ||
| if (!customElements.get(tag)) customElements.define(tag, ctor); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fail loudly when a custom-element tag is already owned by another constructor.
The current check silently accepts collisions, after which document.createElement("stack-outlet") may return an unrelated element. Verify that an existing registration equals ctor; otherwise throw a descriptive integration error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/src/index.ts` around lines 1620 - 1624, Update
defineStackElements to validate existing registrations: when
customElements.get(tag) returns a constructor, compare it with the expected ctor
and throw a descriptive integration error if they differ; retain definition for
unregistered tags and accept registrations already owned by the same ctor.
| it("stops updating after disconnect", async () => { | ||
| const client = new LayerClient(); | ||
| const { provider, outlet } = await mountProvider(client); | ||
|
|
||
| void client.open({ | ||
| ...confirmOptions, | ||
| payload: { title: "Mounted" }, | ||
| }); | ||
| await (outlet as unknown as { updateComplete: Promise<unknown> }) | ||
| .updateComplete; | ||
| await Promise.resolve(); | ||
|
|
||
| provider.remove(); | ||
| await Promise.resolve(); | ||
|
|
||
| expect(() => { | ||
| void client.open({ | ||
| ...confirmOptions, | ||
| payload: { title: "After disconnect" }, | ||
| }); | ||
| }).not.toThrow(); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Verify that disconnect actually stops rendering updates.
LayerClient.open() not throwing does not prove unsubscription. After removal, open another layer and assert that the detached outlet does not render "After disconnect".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit/tests-dom/stack-outlet.test.ts` around lines 209 - 230,
Strengthen the “stops updating after disconnect” test around provider.remove()
by opening the “After disconnect” layer, waiting for any relevant update
completion, and asserting the detached outlet does not render that content. Keep
the existing no-throw assertion, but verify the outlet’s rendered output remains
unchanged after disconnection.
Add Lit to the footer adapters column, restore the verbatim experimental strip, and put Angular → Vue → Lit column order in adapter-hooks and guides.
Keep the package-binds-via-mechanism lead used by React/Vue/Solid/Angular instead of a one-off pain-first opener on the Lit page alone.
Declare reactive layer props, register CEs at module scope, use useLayer + provideLayerClient correctly, and match official light-DOM createRenderRoot.
Full-matrix Lit adapter (controllers + stack CEs) lands at 4.01 kB gzip ignored peers — 9 B over the React-matched 4 KB cap.
Summary
@stainless-code/lit-layers— Reactive Controllers +@lit/context, light-DOM<stack-outlet>/<stack-subscribe>/<app-host>, shadow<stack-provider>, explicitdefineStackElements(), full matrix (useLayer,useLayerGroup.outlet(),createStackHook, etc.)architecture.md); changeset for the new packageuseLayer/useLayerGroup/useAppStack/useStackHandlesstay on one client across provider swapsTest plan
bun test packages/lit/src/index.test.ts(24)bun run --filter @stainless-code/lit-layers test:dom(20)bun run --filter @stainless-code/lit-layers typechecklittopic per retired plan closeoutSummary by CodeRabbit
New Features
Documentation
Tests