Config catalog, module graph, event producer-consumer tables, and
third-party notices regenerate over the webworker surface; the oxlint
rule fingerprint and the ui-renderer NodeNext import face follow.
One Vite build emits dist/index.html and dist/preview.html sharing every
chunk; the only difference is one prepended bootstrap entry whose module
connects the worker host, so the page from the stock entry onward is the
served startup chain verbatim. The dist moves to a relative base so the
preview mounts under any static directory, and the served form anchors
deep SPA-fallback paths with a rendered <base href="/">. The preview-boot
e2e serves the real built pages, packs the VFS image when absent, and
holds the boot line's lowering contract, the interactive hero, and a
clean page-error channel in headless Chromium.
The webserver renders a boot-readiness tail after the injection rows and
AppWebEntry.run awaits the __DSH_BOOT_READY__ deferred before reading any
injected state. Whichever bootstrap applies the injection table settles
the deferred - the served renderer resolves it inline, an asynchronous
bootstrap installs it ahead of the entry module and settles it with the
handshake - so both deployments run one startup chain and a failed
handshake surfaces on the boot page instead of proceeding on missing
globals.
Buy the grammar, own the execution: @yarnpkg/parsers parses the command
line - aliased at bundle time to its shell entry so the root barrel's
syml/js-yaml closure stays out of the worker - and a VFS-backed evaluator
with a coreutils command table runs it inside the worker host. Each shell
process is a real child WebWorker spawned from the same bundle (the first
frame decides the role), so the TERM-then-KILL ladder is real - TERM
requests, KILL terminates the worker - and the file face stays
asynchronous end to end, since the deployment target serves no COOP/COEP
headers and SharedArrayBuffer never exists there. node:child_process
reports through the ChildProcess surface the subprocess service consumes;
execSync, execFileSync and fork refuse, and node-pty stays stubbed.
Two private experimental packages run the whole harness tree inside one
dedicated Web Worker. dsh-experimental-webworker-runtime owns the in-memory
VFS (BigInt stats with per-path identity and strictly increasing mtimes),
the CommonJS wrapper loader over a lazily-evaluated builtin table whose
shims typecheck against Node's own module types, the postMessage tunnel
speaking plain HTTP, the AsyncLocalStorage runtime, and the worker
assembly. dsh-experimental-webworker-packer lowers every module body at
pack time against the shared wrapper contract, sweeps the profile closure
by static reachability, and writes a deterministically gzip-compressed tar
the worker inflates through the browser's native DecompressionStream while
it downloads.
Replace per-plugin tapIndex regex edits with pure-data IndexInjection rows
collected fresh per render over one webserver/index-inject event. One table,
two renderers: the served form renders rows into index.html; a static worker
form ships the same rows over its boot payload. tapIndex survives as the
raw-HTML escape hatch, applied after row rendering; client-modules and
ui-theme move to the event, and the manifest global renders as
globalThis["__DSH_BOOT__"].
The client boot chain gains the seams a pre-injected transport needs: the
module loader takes loadBundle from the transport global by default, HTTP
prefetch stands down when a transport owns bundle bytes, the web-app bundle
can decline frontend serving, the gateway client installs a namespace's
whole method group inside its fiber apply so a parked dependent never
observes the service without its methods, and the dynamic-code precheck
gates through new Function so hosts without a real node:vm keep the
define-time parse gate.
The shell's Vite config aliases a few workspace packages to source; every other
workspace name resolved through node_modules to a lib/ entry the real build has
emitted but a clean checkout has not, so the generator only worked on a built
tree — and a static gate has to pass on a clean one. The dry run now supplies
source aliases for the names the shell leaves out.
Aliases rather than the recorder's resolveId hook, because Vite resolves a
stylesheet @import through aliases alone, and the theme package publishes its
stylesheets from lib/styles/. lib/ is compiled from src/, and the recorded set is
identical either way: 24 packages on a built tree and on a clean one.
Moving react, shiki, katex and the markdown pipeline to devDependencies took
them out of the notices runtime tier, which tiers by declaring section — yet
their code is inside lib/client.js and the shell dist. The generator now learns
what the browser artifacts carry from the real build configs: each client bundle
through its own tsdown config, the shell through apps/web's Vite config, with a
recorder that resolves each bare specifier, notes the package behind it, and
stops there. About three seconds, and only packages a resolved file backs, so a
bundler's virtual module is not mistaken for a shipped one.
Net effect on the file: the type-only packages @types/mdast and
micromark-util-types move to the development tier, because neither ships code.
A package a published browser artifact carries is a runtime disclosure whatever
section declares it, and the build answers which ones those are. Record that
clause, the dry run behind it, why a bundler's virtual module is not a shipped
package, why a types-only package is not either, and the generator run cost the
dry run adds.
react, react-dom, shiki, katex, clsx, the micromark and mdast families and nine
more reach only browser artifacts, which resolve nothing on a user's machine.
Moving 79 declarations out of dependencies and non-optional peerDependencies
drops 103 tarballs and 6.05 MB from an install of the published CLI.
verify-client-runtime-deps walks each package's host and browser faces from the
entries its manifest publishes, over the bound host and client Programs, and
reports an external package no host-face reference reaches. Wired into hygiene.
A workspace name is out of scope: it also states which package supplies an
injected service or a mounted Remote contribution, and the app installs it
either way. A package whose published Node entry has no source counterpart is
skipped and named, because a generated artifact carries imports no source
states.
Browser artifacts resolve nothing on a user's machine: tsdown inlines every
non-platform specifier, the shell dist answers the rest from its frozen module
table, and Vite inlines the shell's own imports into the published dist. Record
the resulting declaration rule, and the Agent Note behind it.
A dsh publication uploads 221 packages one at a time, spaced apart, and
the log gave no way to tell how far along a run was: every line named a
package, none said where that package sat in the set.
Each per-package line now carries [n/total]. Every entry in the order
settles as either published or already present, so the counter is both
"packages settled" and "position in the publish order", and the closing
summary names the member count alongside the published and skipped
totals.
Three corrections from review, none of which change behaviour.
attemptStreaming promised output "as the command produces it", which
spawnSync cannot do: it returns only after the child exits, and the two
streams are echoed one after the other, so their interleaving is lost.
For an npm publish that is visible — notices go to stderr while the
`+ name@version` confirmation goes to stdout, so the confirmation prints
first. The helper is now attemptEchoed and its contract says buffered,
echoed after exit, stdout before stderr; live progress would need an
asynchronous spawn with data listeners.
The traversal comment claimed a node on the stack is a cycle only peer
edges can form, and that skipping it drops just that edge. The cycle does
carry a peer edge, because the install edges were proved acyclic a moment
earlier, but the back edge that reaches the stacked node need not be the
peer one — which is what the post-condition exists to catch, so the
comment now points at it instead of asserting an invariant the traversal
does not have.
The `if (placed.has(member.name)) return` after leaving the stack was
unreachable: a re-entrant visit returns at the top guard while the member
is on the stack, so it can never be placed by the time the recursion
unwinds.
A dependency in optionalDependencies, or a peer carrying
peerDependenciesMeta.<name>.optional, may be absent from an installed
tree — that absence is the whole promise of "optional". A static import
is evaluated when the importing module loads, so one absent package
stops being "this capability is unavailable" and becomes a load failure
for everything that reaches the importing module.
Nothing checked it, and nothing here could: the failure needs an
installed tree missing that package, and a workspace install always has
every package, so the unit tests, the snapshots, and the packed-install
probe all pass while the published package is broken for the consumer
who declined the optional peer.
verify-optional-dependency-imports reads each package's own manifest for
what it allows to be absent, then scans the files that ship across both
compiler faces. Value-versus-type is decided against a bound Program
rather than the import syntax, because verbatimModuleSyntax is off: the
compiler already erases an import whose bindings resolve to types, so a
syntactic rule would report four forms that emit nothing. Only the type
phase erases an import — `import defer` still resolves and links its
module, deferring evaluation alone — which is what phaseModifier
expresses and the deprecated isTypeOnly cannot.
A violation names the package, the declaration that made it optional,
and the way out in order: import it as a type, or restructure so module
scope does not need it. A dynamic import() only moves the failure to
first use, so the gate does not offer it as the remedy.
The gate runs in ci-static and ci-primary through ciSharedStaticGates
and locally in hygiene; it needs no build. TypeScriptProject gained a
face parameter so a repository-wide gate can seed the client aggregate,
which was previously unreachable; the constraint it was built with is
unchanged, a face config and never the root solution.
The tree has no violation today, so this guards the rule rather than
fixing a defect. The spec pins all seven import forms against what tsc
emits, including the four a syntactic rule would misreport.
The verify step resolved the publish order and said only that it had:
the order a release actually follows, and the ordering it could not
honour, stayed invisible until a publication was already running.
publishOrder now returns that order together with the peer edges it
dropped, verify prints both, and pack reads the order off the plan. The
dropped edges are part of the result rather than a detail of forming it:
the dsh family drops one (dsh-api-remotes -> dsh-api-gateway) and the
vendored family drops two (cordis-plugin-include and
cordis-plugin-loader, which cordis declares as peers in return), and
only whoever reads the log can judge whether a newly dropped edge is
expected.
Because pack runs on every pull request and master push, a change to the
order is now reviewable there rather than observable only at publish
time.
The order is also checked against the edges it exists to honour. A cycle
mixing peer and dependency declarations can put a dependency on the
traversal stack, where it is skipped like a peer edge, emitting a
consumer before something it installs; no later step can detect that,
and it would surface as an unresolvable install for a consumer of the
published packages. No family has that shape today, and the new test
pins the three-package case that would.
Retry classification needs npm's failure text, so the publish call captured its
streams instead of inheriting them. That silenced npm on the success path: the
log lost the tarball contents, the notices, and the '+ name@version'
confirmation for every package.
Pipe the streams and echo them, so the log shows what npm reported and the
caller still gets the text it classifies. The registry probe behind it keeps its
streams captured, since its JSON and its E404 are internal queries rather than
progress.
Publish order exists to make a partial publication self-consistent: an
interrupted run should leave a prefix whose packages never point at a version
absent from the registry. It read only dependencies and optionalDependencies, so
peer declarations — how sibling harness packages reference each other, 1088 edges
in the dsh family — constrained nothing.
Peer edges now order the publication too. devDependencies still do not: a dev
dependency is absent from the published package.
Peers cannot constrain it absolutely. Sibling packages declare each other as
peers, which is what closes the two cycles here, and npm treats an unmet peer as
a warning rather than a resolution failure. Install edges therefore win: a peer
edge is dropped where the peer installs the member declaring it, or where
following it would revisit a member already being visited. One peer edge is
dropped in the dsh family and two in the vendored family; every install edge is
honoured.
A cycle among install edges stays a defect rather than something to order
around, and release:verify now reports it before the build instead of letting it
surface once pack is already writing tarballs. Install-edge acyclicity is checked
on its own graph, because a peer edge leading into an install edge otherwise
reads as a cycle where the install edges are perfectly orderable.