Skip to content

fix: make @webjsdev/core a peer dependency of @webjsdev/server - #1450

Merged
vivek7405 merged 2 commits into
mainfrom
fix/split-core-install
Aug 21, 2026
Merged

fix: make @webjsdev/core a peer dependency of @webjsdev/server#1450
vivek7405 merged 2 commits into
mainfrom
fix/split-core-install

Conversation

@vivek7405

Copy link
Copy Markdown
Collaborator

The bug

packages/server/test/dev/asset-helper-serve.test.js failed on a clean checkout with an assertion that could only say the rendered url carried no ?v=. That points nowhere near the cause, so here it is.

Three of @webjsdev/core's server-facing features are provider seams held in module scope:

seam file what it does when the provider never arrives
asset() asset-url.js returns the bare path, silently losing immutable caching
cspNonce() csp-nonce.js returns empty, so an inline script is blocked under a CSP
bound-form identity form-action.js formActionId answers null, so <form action=${fn}> loses the identity the dispatcher reads (invariant 12)

@webjsdev/server installs each at boot by importing core and calling a setter. That only reaches the app when both sides load the same module instance. Two copies of core on disk are two independent sets of module-scope state, so the setter lands on one and the app reads the other. Nothing throws in any of the three cases.

The failing checkout had a stale packages/server/node_modules/@webjsdev/core. The server resolved that copy; the app resolved the workspace one, whose _provider was still null. Removing the nested copy makes the test pass 2/2, and the live website then renders href="/public/tailwind.css?v=99a5f9074737" instead of the bare path.

The fix

@webjsdev/core moves from dependencies to peerDependencies (carried in devDependencies at the same range for workspace dev). A regular dependency is exactly what lets npm nest a second copy whenever it cannot dedupe to one. A peer is resolved against the app's own copy, and a genuine version conflict is reported at install time rather than silently satisfied by duplication.

The regenerated lockfile shows the effect: packages/server/node_modules now nests only ws.

This is a real-app exposure, not just a test artifact. Any app whose core version does not dedupe against @webjsdev/server's range could get the nested copy and lose all three features with no error.

What I tried and removed

I first added a boot-time warning comparing the app's core path against the server's. It broke dev-handler.test.js, which asserts boot emits no warnings because analysis is lazy, and it fired for a false reason: in a linked worktree the app-side path resolves to the worktree's packages/core while the server resolves the primary's through linked node_modules. Two paths, same package, no broken seam.

Path comparison is not a sound proxy for a split module instance, and a boot warning that misfires on this repo's own documented worktree workflow is worse than none. Making it sound would mean functionally probing the seam at boot, which costs a second core import to catch a case the peer fix already prevents. So it is gone.

Test plan

  • packages/server/test/core-peer-dependency.test.js (new, 2/2) pins the packaging decision and records the failure mode so nobody moves it back.
  • asset-helper-serve.test.js passes 2/2 with the split removed. It was 1/2 before.
  • Full node suite clean apart from this checkout's known linked-worktree baseline (test/bun/listener*, three differential-elision assertions), each unrelated and reproducible with the branch source reverted to origin/main.
  • Dogfood: website boots 200 and asset() fingerprints correctly (?v=99a5f9074737); webjs check clean.
  • N/A browser and e2e: this changes packaging metadata and docs, no runtime code.
  • N/A Bun parity: no runtime-sensitive surface touched.

Docs

.agents/skills/webjs/references/built-ins.md gains a paragraph on asset() being a provider seam, why a second core breaks it silently along with cspNonce() and bound forms, and npm ls @webjsdev/core plus npm dedupe as the diagnostic.

Note

The regenerated lockfile also picks up core 0.7.52, which the release commit bumped without the lock following. Unrelated to this fix, called out so it is not a surprise in the diff.

Three of core's server-facing features are provider seams held in module
scope: asset(), cspNonce(), and the bound-form identity resolver. The server
installs each at boot by importing core and calling a setter, which only
reaches the app when both sides load the SAME module instance. Two copies of
core on disk are two independent sets of that state, so the setter lands on
one and the app reads the other.

Nothing throws. asset() returns bare paths and loses its immutable caching,
cspNonce() returns empty so an inline script is blocked under a CSP, and
formActionId answers null so a bound <form action=${fn}> loses the identity
the dispatcher reads. That silence is the reason this was found from the far
end: asset-helper-serve failed on a checkout carrying a stale nested copy
under packages/server/node_modules, and the only symptom it could report was
a url with no ?v=.

A regular dependencies entry is what lets npm nest that second copy whenever
it cannot dedupe to one. A peer is resolved against the app's own copy, and a
real version conflict is reported at install time rather than silently
satisfied by duplication. The regenerated lockfile shows the effect: nothing
but ws is nested under packages/server now.

The lockfile also picks up core 0.7.52, which the release commit bumped
without the lock following.
@vivek7405 vivek7405 self-assigned this Aug 21, 2026

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the whole diff again end to end. Four files, and the mechanism holds up.

What I checked beyond the diff itself. A scaffolded app declares BOTH @webjsdev/core: latest and @webjsdev/server: latest (create.js:442-443), so the peer resolves against the app's own copy in the exact shape webjs create emits, and an app that depends only on server still gets core through npm's peer auto-install. The lock regenerated cleanly and nests only ws under packages/server, which is the observable proof the fix does what it claims. Nothing in AGENTS.md, framework-dev.md, or the server README describes the old dependency shape, so no doc went stale. And the release checklist's dependent-range grep matches a peerDependencies entry the same as a dependencies one, so range maintenance at bump time flows unchanged.

The trade this makes, stated so it is a decision rather than a surprise: the day core bumps past ^0.7.51 before server's range widens, an install that would previously have nested a second core silently now fails loudly with a conflict. Loud beats three features quietly doing nothing, and the lockstep release flow is what keeps the ranges in step anyway. The one escape hatch worth knowing about is --legacy-peer-deps, which skips peers entirely; a scaffolded app is safe there because it declares core itself, and an app that only declared server would fail at boot with a module-not-found, which is still loud.

The new test reads the right package.json and pins both halves, including the dev range tracking the peer range so workspace dev resolves what apps will. The doc paragraph describes what actually ships, with no trace of the boot warning that was tried and removed.

Nothing must-fix.

#1447 landed the linked-worktree install guard, which is plausibly the vector
that created the stale nested @webjsdev/core this branch was chasing: an npm
install run inside a linked worktree writing real packages into the shared
node_modules. The two changes are complementary, one stopping the install
that creates a second copy and this one stopping npm from being entitled to
create it at all.

built-ins.md auto-merged, both paragraphs intact and on different subjects.
@vivek7405
vivek7405 merged commit ac89745 into main Aug 21, 2026
10 checks passed
@vivek7405
vivek7405 deleted the fix/split-core-install branch August 21, 2026 13:28
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