Repository navigation
Test the published package, and fix two defects it found - #190
Merged
Merged
Conversation
|
Comprehensive consumer packaging validation with clear, well-documented tests and CI integration. 🎯 Quality: 77% Good · 📦 Size: Large — consider splitting if possible 🛡️ Standards: no pre-flight fit check ran for this change — wire 📈 This month: Your 147th PR — above team average · Averaging Good |
Contributor
Author
|
/review |
mariojgt
approved these changes
Aug 28, 2026
Everything in the suite tested the source, or `dist/` from inside the repository. Neither can see the questions a consumer actually hits, because the tarball's METADATA decides them: which file an `exports` condition resolves to, which declarations TypeScript reads beside it, whether `files` left something out, whether the bin runs when npm installs it as a shim. `scripts/compat-matrix.mjs` packs a real tarball and installs it into isolated consumer projects — ESM JavaScript, CommonJS JavaScript, ESM TypeScript, CommonJS TypeScript, CommonJS TypeScript without `@types/node`, the installed bin, and export encapsulation. It runs identically locally (`npm run test:consumers`) and in CI across npm, pnpm, Yarn and Bun, plus a Windows smoke test for the generated `.cmd` launcher. The existing Node 18/20/22/24 matrix is untouched. It found two shipped defects immediately. ## CommonJS TypeScript could not use this package at all `exports` carried a single top-level `types` pointing at the ESM declarations. A CommonJS TypeScript consumer therefore resolved `require` for the runtime and ESM types for the shape, so TypeScript concluded the target was an ES module and refused the `require` outright — TS1479, at the import line, with no way around it. CommonJS JavaScript worked perfectly, which is why nothing noticed. Types are per-condition now, `import` to `.d.ts` and `require` to `.d.cts`, with `default` last. The root's `.d.cts` was already being built and simply never referenced; `./protect` had no `.d.cts` at all despite advertising a `require` condition, so the build now emits one — a copy, because tsup emits the root's two declaration files byte-identical and there is nothing per-format to express. Edge conditions stay ahead of `import`, so a Worker still gets the edge bundle. TypeScript does not apply them and falls through to `import`/`require`, where it now finds the right declarations. ## The published types required a dependency the package does not declare Two exported functions took `NodeJS.ProcessEnv`, which comes from `@types/node`. A consumer without it could not compile against the public surface at all — the declarations referenced a namespace their project had never heard of. An `EnvLike` alias replaces it: `process.env` satisfies it, and so does a plain object. `cjs-ts-no-node-types` exists for exactly this and is not redundant with `cjs-ts`, since every other fixture installs `@types/node` and would keep passing. Both regressions confirmed to fail the matrix: reverting `exports` fails 2 of 7 shapes, restoring `NodeJS.ProcessEnv` fails 1. Dropping the `protect.d.cts` copy fails the packaging test, which also now checks that every file the export map names is inside `files` AND actually built — a map can name a path the build never produces, and that surfaces three layers away as "cannot find module" in someone else's project. `module` is kept. Removing it needs evidence no bundler still reads it, and the bundled-consumer test that would provide that is not written yet; keeping a redundant field costs nothing, guessing costs a consumer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six corrections to the compatibility check and the code it exercises. **Windows.** `execFileSync` creates a process directly, and the package managers and installed binaries are `.cmd` shims — a batch file is not an executable image, so it cannot be launched that way. Windows now goes through the command interpreter with every token quoted, and every other platform keeps direct execution: no shell, nothing to escape wrongly. Quoting wraps each token because a Windows temp path contains spaces, and doubles `"`, which is cmd's own escape. **The encapsulation case had no positive control.** Every assertion in it expects an import to fail, so it passed whether or not the package was installed. It now resolves the public root first and fails if that does not work — verified by removing the install for that shape, which turns the case red. **Yarn Berry needs the fixture declared self-contained.** Berry walks up from the working directory looking for a project root and adopts any `package.json` above it, which a temp directory often has. An empty `yarn.lock` in the fixture is Berry's own documented answer, and it is harmless under Classic. Per-manager preparation now exists for that. **Manager versions are pinned and printed.** An unpinned manager silently changes what was exercised, and Yarn Classic and Berry resolve differently enough that a pass under one is not a pass under the other — so both are in the matrix, and the script reports the version it actually saw alongside Node and the platform. **`EnvLike` had displaced the documentation of the function below it**, leaving two adjacent doc blocks and a function with none. The type carries its own comment and sits above the function's. **Comments state the invariant, not the history.** Several of the new comments narrated what used to happen and how it was found, which belongs in the private engineering record rather than in a public repository. They now say what holds and why it matters to a consumer. Verified: full suite green, and all seven consumer shapes pass under npm, Yarn Classic and Bun locally. Berry and Windows are exercised by CI — the Berry blocker above was reproduced and fixed from its own error message, but the complete Berry run has not been observed outside CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…actly Yarn Berry is installed from `@yarnpkg/cli-dist`, which is the distribution carrying the `yarn` binary at 4.x. The `yarn` package on npm stops at 2.x, so a `yarn@4` install cannot resolve and the job ends before any fixture runs. Windows quotes the command word only when it is a path. A bare name has to stay unquoted: `npm` there is `npm.cmd`, found through `PATH` and `PATHEXT`, and the shim locates its own installation with `%~dp0` — the directory of the batch file. Quoting the name makes that the working directory instead, so npm looks for its CLI under whatever project is current. Absolute paths are quoted, since the binaries in `node_modules/.bin` are addressed by path and a Windows temp or workspace path contains spaces. The interpreter is invoked explicitly as `cmd /d /s /c "<line>"` with `windowsVerbatimArguments`: `/s` strips exactly the outermost quote pair and takes the rest verbatim, and the verbatim flag stops Node re-escaping a line that is already correct. The build runs before the tests. Several tests only assert against `dist/`, and one of them returns early and green when it is absent — so the check that the built edge bundle carries no static Node-builtin import was not being made. Two comments claiming CI tests before building are corrected; they now describe what happens. Every manager version is exact — npm 11.6.0, pnpm 9.15.4, Yarn 1.22.22, Yarn 4.18.0, Bun 1.2.21 — and the step prints the version on PATH before any fixture runs, so an install that resolved to something else is visible rather than inferred from a later failure. A floating major means a passing run happened for a reason nobody chose. Berry needed no linker configuration: 4.x uses a `node_modules` layout for a project with no `.yarnrc.yml`, so the two config steps asserting otherwise are removed. The empty `yarn.lock` the script writes is what Berry actually requires. Verified locally: all seven consumer shapes pass under npm 11.6.0, Yarn Classic 1.22.22, Yarn Berry 4.18.0 and Bun. Windows remains CI-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Berry resolved nothing on Linux: the fixtures installed but no shape could import the package. Its linker depends on the Yarn version and on any `.yarnrc.yml` above the fixture, so the same script produced a `node_modules` layout on one machine and Plug'n'Play on another — and under Plug'n'Play nothing is in `node_modules`, so `node probe.mjs` cannot resolve the package at all. The fixture now states `nodeLinker: node-modules` alongside the empty `yarn.lock`, in the script rather than in the workflow, so it holds wherever the script runs. Confirmed to be the deciding factor: with `nodeLinker: pnp` all seven shapes fail, with `node-modules` all seven pass. Plug'n'Play is a real Berry configuration and this matrix does not cover it. The comment says so, because a matrix that quietly exercises one layout while reading as "Yarn works" is the more expensive outcome. Verified locally under npm, Yarn Classic 1.22.22, Yarn Berry 4.18.0 and Bun. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check read "Yarn Berry 4.18.0" while testing Berry with a `node_modules` layout only. Plug'n'Play is a real Berry configuration and is not covered, so the label claimed more than the job holds. It now says `Yarn Berry 4.18.0 (node-modules)`. Renaming is safe here: the consumer jobs are not required status checks, so no protection rule references the old name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pre-pull-request checklist named typecheck, test, build, while the section above it establishes that the build has to come first: several tests only assert against `dist/`, and one returns early and green without it. The checklist now matches, and points at that reason rather than restating it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
patchstackdave
force-pushed
the
test/published-package-compatibility
branch
from
August 28, 2026 07:49
c3d0705 to
387b611
Compare
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tests the published package against real consumers, and corrects two defects in what it publishes.
Stacked on #189 until that merges; the rebase is noted at the end.
What the check covers
Every other test in this repository exercises the source, or
dist/from within the repository. Neither reaches the questions a consumer's build asks, because the tarball's metadata answers them: which file anexportscondition resolves to, which declarations TypeScript reads beside it, whetherfilesomits something, whether the bin runs as the shim npm installs. A defect there leaves the suite green while nothing can import the package.scripts/compat-matrix.mjspacks a tarball and installs it into isolated consumer projects, one per consumption path:esm-jsimportcjs-jsrequireesm-tscjs-tscjs-ts-no-node-types@types/nodecliencapsulationexportsresolvesIt runs the same way locally (
npm run test:consumers) and in CI across npm, pnpm, Yarn Classic, Yarn Berry and Bun at exact versions, with a Windows job for the.cmdlauncher. The Node 18/20/22/24 matrix is unchanged.Two corrections to what is published
Types are per-condition.
exportsroutesimporttodist/index.d.tsandrequiretodist/index.d.cts, withdefaultlast. TypeScript reads the declarations beside the condition it resolved: an ESM.d.tsunderrequiretells it the target is an ES module, and it refuses therequirewith TS1479 even though the runtime works../protectnow ships a.d.ctsfor the same reason.The public declarations no longer name
NodeJS.ProcessEnv.@types/nodeis not a dependency of this package, so nothing in its public surface may require it; anEnvLikealias describes the environment structurally.process.envsatisfies it, and so does a plain object.Controls
Each shape asserts a positive before asserting an absence.
encapsulationresolves the public root first and fails if that does not work — otherwise every assertion in it is an expected failure and the case passes when the package is absent.cjs-ts-no-node-typesis the only TypeScript fixture without@types/nodeinstalled, which is what makes it able to hold that constraint.A packaging test asserts that every file the export map names is inside
filesand present after a build. A map can name a path the build does not produce, and that surfaces as "cannot find module" in a consumer's project.Windows
execFileSynccreates a process directly and a batch file is not an executable image, so the interpreter is invoked explicitly:cmd /d /s /c "<line>"withwindowsVerbatimArguments./sstrips exactly the outermost quote pair; the verbatim flag stops Node re-escaping a correct line.The command word is quoted only when it is a path. A bare name must not be:
npmisnpm.cmd, resolved throughPATHandPATHEXT, and its shim locates its own installation with%~dp0. Arguments are always quoted, because Windows temp and workspace paths contain spaces.Managers
Exact versions — npm 11.6.0, pnpm 9.15.4, Yarn 1.22.22, Yarn 4.18.0, Bun 1.2.21 — and the version on
PATHis printed before any fixture runs. Berry comes from@yarnpkg/cli-dist, which is the distribution carrying theyarnbinary at 4.x; theyarnpackage on npm stops at 2.x. Berry treats a fixture as its own project only when ayarn.lockis present, so the script writes an empty one; that is harmless under Classic.Build order
CI builds before it tests. Several tests assert against
dist/, and one returns early and green when it is absent, so the check that the built edge bundle carries no static Node-builtin import was not being made.Verified
All seven shapes pass locally under npm 11.6.0, Yarn Classic 1.22.22, Yarn Berry 4.18.0 and Bun. Full suite 1710 passed, 7 skipped. Windows is exercised by CI only.
Rebase
This branch carries #189's commits, so the two do not have separate review or rollback boundaries. Once #189 merges, this is rebuilt from the new
mainwith only its own three commits and the matrix rerun.🤖 Generated with Claude Code