The JSR version of my logger could fall back to console output even when Winston was installed. Its warning said Winston wasn’t found, but the failing import pointed to a file inside the logger package.
The published code was trying to load ./winston, not the installed winston package.
A different specifier in the artifact
The source used a bare package import:
const winston = await import('winston');
Inspection of the JSR compatibility tarball for version 1.1.16 found a relative import instead:
const winston = await import('./winston');
Those strings ask the runtime to resolve different things. The second looks for a neighboring module within the logger package.
The project’s retained investigation traced the change to the publication path for its optional Winston dependency. The npm build didn’t have the same problem.
JSR performs publish-time import processing, but this is a record of that package’s historical setup, not a claim that JSR rewrites every optional dependency incorrectly. Current publishing documentation describes supported npm dependencies and manifest-based specifier rewriting.
The artifact supplied the useful evidence. Reading the source tree alone would have shown the intended import, not the one the consumer received.
The fallback named the wrong cause
Winston was optional, so the logger caught import failures and fell back to console logging.
The fallback itself was deliberate. The diagnostic was too specific: it described Winston as missing.
A failed import didn’t establish that cause. In this case, the installed package could be present while the generated import requested a nonexistent local module. The catch also covered initialization work, so treating every failure there as an absent peer dependency was too broad.
Console fallback kept logging available, but it changed the capabilities in use. A caller expecting Winston transports or formatting could receive console output instead.
I want an error message at this boundary to say which operation failed without claiming more than the error establishes. “Initialization failed; using console logging” is less specific, but more useful than confidently naming the wrong repair.
The workaround and the later removal
The workaround moved the specifier into a variable:
const winstonModule = 'winston';
const winston = await import(winstonModule);
In that publishing setup, the indirection preserved the intended runtime import. It was a workaround for the observed artifact transformation, not a guarantee about what static analysis can or cannot follow in future versions.
The 2.0 implementation subsequently removed Winston and used its own transport layer, eliminating this import path. The investigation stayed in the repository because the packaging failure was still worth documenting.
A type-check suppression hadn’t addressed it. Type checking the source and resolving the published module are different checks.
For a library with multiple distribution paths, a package test needs to install the generated artifact through each supported path and exercise the behavior that matters. Optional dependencies need both present and absent cases.
The source can be correct while the package fails. A fallback can then make the failure look like supported degraded behavior. Testing what gets installed is how to tell those cases apart.
Sources
- The retained JSR/Winston investigation — artifact inspection, the historical workaround, and removal in 2.0.
- JSR publishing documentation — dependency declarations and publish-time import processing.
- Node.js ERR_MODULE_NOT_FOUND — module-resolution failures.
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].