Skip to content

A Vite context's build retires with its last open environment build - #395

Merged
codecaaron merged 9 commits into
mainfrom
wc/vite-env-refcount
Oct 11, 2026
Merged

codecaaron merged 9 commits into
mainfrom
wc/vite-env-refcount

Conversation

@codecaaron

@codecaaron codecaaron commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Pre-release blocker (teardown audit gap D): a Vite custom buildApp that builds a shared plugin's environments in parallel failed the build. #380 retires the context's Session and engine in closeBundle, and the first environment to close did so while the others still transformed: "[animus-extract] Failed to transform src/Sentinel.ts: v2 transform before analyze — engine instance not initialized". Unreleased (#346 and #380 postdate v0.1.25).

Fix

The context tracks the environments whose build started and has not closed (openBuild in buildStart, closeBuild in closeBundle) and retires its build only when that set empties. A set rather than a count, because the hook pairing is not one-to-one (probe below).

Root's three checks (run-and-discard probe, real Vite 8.2.2, macOS)

Path Hooks observed Effect of the set
vite build --watch each rebuild: buildStart client then closeBundle client retires after each rebuild and rejoins at the next, as before
parallel build, SSR failing mid-transform buildStart client, buildStart ssr, closeBundle client, closeBundle ssr ×2 the duplicate close is a no-op; the build still rejects loudly
dev server, client and SSR buildStart client only; at close closeBundle ssr, closeBundle client the never-started SSR close leaves client open; the client close retires

Sequential and parallel builds (sharedPlugins on and off) produce identical CSS and SSR/client output, with no raw builder code; with the fix, the slow-SSR parallel build matches them.

Review round (spike-codex P2 at 3adeb4e)

A name-keyed set conflated distinct active builds: a builder reused right after a rejected parallel buildApp (the old SSR build's late close emptied ssr under the new one), and two distinct environments named alike (the first close retired the engine under the second). Both were loud. The context now maps each open build's name to its environment and refuses, in buildStart, a second build of an active environment (a previous build of environment ssr is still running on this plugin instance) and a second environment under an active name; only the environment that opened an entry removes it (bffeeee, 53d81d2). Rows (8674dc4), red at 3adeb4e: 'a second build of a running environment is refused, naming it' and 'two environments under one name are refused, naming it'.

Last round (spike-codex: a refused build owned the active generation's end)

Hook contexts are new objects per call, so they carry no per-attempt token; but Vite ends every build exactly once in buildEnd, failed ones included, while closeBundle can run twice for a failed build (probe). So the refusal is gone: a buildStart for an environment whose build is open (the same object, or one named alike) waits for that build's end and then opens its own generation, and buildEnd ends only the generation its environment opened; the last to end retires the Session and engine (c3e3fca). A dev server's close calls buildEnd only for environments that started. Rows (5abde23), red at 53d81d2: 'a builder reused while an earlier build still runs builds after it' (awaits all four builds: only the planned client failure rejects; the original SSR fulfils) and 'two environments under one name build one after the other'.

Witness

tests/dev-server/build-app.test.ts › the first environment to close leaves the others their Session: a real createBuilder with client and SSR in parallel and SSR transforms delayed; red on main 26a2b51 (the error above), green here.

Checks

macOS at 53d81d2: Vite suite 229/229, lint and fmt clean. Linux CI: runs 38174475316 (3adeb4e), 38175931696 (53d81d2) and 38177357515 (5abde23), every job green.

🤖 Generated with Claude Code

codecaaron and others added 2 commits October 11, 2026 16:59
A custom buildApp builds a shared plugin's environments in parallel, and
#380 retired the Session and engine in the first environment's
closeBundle: the other environments then transformed without them and
failed the build ("v2 transform before analyze — engine instance not
initialized"). The context now tracks the environments whose build
started and has not closed, and retires its build when the set empties.
A set, not a count: Vite closes a failed environment's build twice and,
in dev, closes environments whose build never started.

Probe (run-and-discard): build --watch pairs each rebuild's buildStart
with its closeBundle; a failing environment still rejects the build;
sequential builds and dev close as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A real Vite createBuilder over the dev-server fixture builds client and
SSR in parallel through a custom buildApp, with SSR transforms delayed so
the client closes first; SSR output must be extracted. Red on main
26a2b51 (macOS): the build fails with "v2 transform before analyze".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
codecaaron and others added 3 commits October 11, 2026 17:19
… under one name

spike-codex: the name-keyed set conflated distinct active builds. A
builder reused right after a rejected parallel buildApp let the old SSR
build's late closeBundle empty 'ssr' under the new SSR build; two
distinct environments named alike let the first close retire the engine
under the second (both loud, 'engine instance not initialized'). The
context now maps each open build's name to its environment and, in
buildStart, refuses a second build of an active environment ('a previous
build of environment ssr is still running on this plugin instance') and a
second environment under an active name. Only the environment that
opened an entry removes it, so a refused build's closeBundle never
retires the running one.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- 'a second build of a running environment is refused, naming it': the
  builder reused straight after a parallel buildApp whose client failed
  while SSR still ran.
- 'two environments under one name are refused, naming it': buildApp
  builds the client environment and a distinct twin named client in
  parallel.
Both red at 3adeb4e.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The pushed head failed lint (anti-slop no-object-parameters): the open
builds took a broad `object`. They now take a BuildingEnvironment (Vite's
environment, or the context's single stand-in for a host without
environments).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
codecaaron and others added 2 commits October 11, 2026 17:40
…buildEnd

spike-codex: a refused build owned the active generation's end. Its
closeBundle removed the original's entry, so the reused builder's second
client retired the engine under the original SSR build, which rejected.
Hook contexts are new objects per call (no per-attempt token), but Vite
ends every build exactly once in buildEnd, failed ones included, while
closeBundle can run twice. So a buildStart for an environment whose
build is open (the same object or one named alike) now waits for that
build's end and then opens its own generation, and buildEnd ends only
the generation its environment opened; the last to end retires the
Session and engine. No refusal is left, so no stray end exists. A dev
server ends only the environments that started.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…built in turn

- 'a builder reused while an earlier build still runs builds after it':
  spike-codex's witness awaiting all four build promises; only the
  planned client failure rejects, the original SSR build fulfils.
- 'two environments under one name build one after the other': the twin
  waits for the client and both build.
Both red at 53d81d2.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
codecaaron added a commit that referenced this pull request Oct 11, 2026
…buildEnd

spike-codex (P1, silent): a refused build owned the active generation's
end. Under a builder reused after a failed parallel buildApp, the second
client's buildEnd disposed the shared session directory before the
original SSR build ended, which then fulfilled with animus.css
referencing ./assets/badge.*.svg and no SVG emitted. As in vite-plugin
(#395): a buildStart for an environment whose build is open (the same
object, or one named alike) waits for that build's end, then opens its
own generation; buildEnd, which runs once per build, ends only the
generation its environment opened; the last to end disposes the session
directory and releases the claim. No refusal is left, so no stray end.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@codecaaron
codecaaron marked this pull request as draft October 11, 2026 21:57
@codecaaron

Copy link
Copy Markdown
Owner Author

Parked by root under the stopping rule after the final round. spike-codex confirmed the reused-builder findings closed (all four builds settle correctly; the original SSR keeps its assets), but the serialise shape introduces a circular wait: a plugin whose transform awaits a nested build of the same environment hangs, because the nested buildStart awaits the outer build's buildEnd (vite-plugin context.ts:517-521 / unplugin core.ts:529-537). The earlier refusal heads fail that case loudly instead. Owner decision pending: ship this head with the nested-build case as a stated limit, ship the refusal head, or leave parallel buildApp unsupported (main fails it loudly).

🤖 Generated with Claude Code

@codecaaron
codecaaron marked this pull request as ready for review October 11, 2026 23:17
@codecaaron
codecaaron merged commit 5cfcde7 into main Oct 11, 2026
14 checks passed
codecaaron added a commit that referenced this pull request Oct 12, 2026
Session C. #395 (vite-plugin) and #402 (unplugin) each tracked open
environment builds with the same algorithm: open by name after any open
build of that name ends, end only the entry this environment opened, and
retire the shared build with the last. extract/session's EnvironmentBuilds
now holds it once, with the single environment a host without
environments builds as; both plugins delete their copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant