You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e9855a0
Browse filesBrowse the repository at this point in the historyBrowse files
🐛 Build npm packages from their local release closure (#847)
* 🐛 Build npm packages from their local release closure
A tagged publish waited on npm indexing a sibling the previous job had just
published, because the builder rewrote every `workspace:*` dependency to
`^<version>` before calling dnt. `publish-one.yml` bounded that wait with four
attempts and 15-second sleeps; the observed lag exceeded it on 0.12.0, 0.12.1
and 0.13.1, and 0.13.1 reached npm only after four rounds of rerunning failed
jobs.
`scripts/build-npm.ts` now builds in two phases. Phase 1 builds the requested
package's internal dependencies depth-first from the same checkout, each at most
once, and hands dnt absolute `file:` ranges naming those artifacts, so the
install and the type check never reach the registry for a package from this
release. Phase 2 finalizes every manifest in the closure together, once the last
dnt call has returned, replacing each internal `file:` range with the sibling's
`^<version>`. Together, not per package: in a chain A → B → C, rewriting B when
its own build finishes puts C's registry version back in front of A's install.
Before reporting success the builder refuses any dependency range still starting
with `workspace:` or `file:`, and any string naming the checkout's path — a
release gate, so an unexpected local dependency fails the build instead of being
normalized into something npm would accept. An internal dependency no workspace
member declares is refused by name, with no registry fallback.
`DNT_LOCAL_SIBLINGS` is gone: the default path is what it used to provide, and
the artifact a developer builds is now the artifact a release publishes.
`DNT_SKIP_INSTALL` keeps its leaf-only contract. `publish-one.yml` builds once
and keeps its already-published guard; `publish-packages.yml` keeps its
dependency-ordered `needs:`, which is now a publication guarantee rather than a
build one — a failed upstream still withholds its dependents, so whatever npm
holds is dependency-closed.
The builder is importable, so the regression drives the real path over a
three-member closure whose versions npm has never seen.
* 🧪 Prove closure finalization with packed local dependencies
N4 was returned as unprovable, and that was wrong about the mechanism rather
than about the contract. npm symlinks a directory `file:` dependency by default,
so a dependent reaches whatever the sibling's own build left in its
`node_modules` and an early-finalized sibling costs nothing. npm's supported
`install-links=true` packs and installs it as an ordinary dependency instead,
which leaves the sibling's own manifest as the only statement of where its
dependencies come from — and that is the discriminator.
N4 now runs the real builder twice under an invocation-private npm
configuration: a positive control, so packed local artifacts are known to work
before a failure means anything, then the same closure with `b` rewritten to the
registry range the moment its build completes. `a`'s install fails, no manifest
is finalized, and `a` never completes. The registry it would have to reach is
unreachable and retries are off, so the failure is hermetic and immediate rather
than whatever npmjs.org happens to answer. The environment is restored however
the case ends; every other build in the file is the ordinary one.
Setting `install-links=false` makes the negative control pass, which is what
makes the scoped configuration load-bearing rather than decoration.
The three places that explained phase 2 by claiming an early rewrite puts a
registry version in front of the dependent's install now say what is actually
true: the cost of finalizing early depends on how npm installs a local directory
and on the sibling's own build residue, and finalizing the closure at the end is
what makes the build independent of both.
* 🧪 Hold N4 to the failure it is about
`expect(caught).toBeInstanceOf(Error)` accepted any throw, including one from
the observer that applies the mutation — so a negative control that never
reached `a` would have passed while proving nothing.
The build-start events must now be exactly `c`, `b`, `a`, which is what says the
mutation was applied and `a` entered dnt; an observer that threw leaves that list
one short. The caught error must be dnt's `npm install failed with exit code 1`,
which is what says it failed where a registry range has to be resolved rather
than anywhere else in the run. The proof that only `c` and `b` complete and that
finalization never starts is unchanged.
Making the observer throw before its rewrite now fails the case.
0 commit comments