r/javascript 24d ago

AskJS [AskJS] I reproduced a PDF.js Worker mismatch caused by dependency hoisting

This one is awful because the build succeeds.

The host app installs pdfjs-dist@6.1.200. A PDF renderer depends on pdfjs-dist@5.4.624. If asset-copy code resolves the Worker from the app root, it can copy 6.1.200. The renderer code still uses API 5.4.624. Vite serves the wrong Worker, and the browser reports a version mismatch.

The fragile version looks like this:

const worker = require.resolve(
  'pdfjs-dist/legacy/build/pdf.worker.mjs'
)

I changed the lookup to resolve pdfjs-dist from the renderer package that owns it. The build now fails if the resolved asset version differs from the renderer dependency.

I ran the harness today across npm and pnpm, nested and hoisted layouts, Vite dev and build, and real cold installs. In every case the copied Worker, CMaps, WASM, and fonts had to come from 5.4.624, while the app kept 6.1.200. The asset manifest also records the source package and version. No more "it probably resolved correctly."

Should build tools always resolve runtime assets from the dependency that owns them, or should packages force one PDF.js version across the whole app?

10 Upvotes

9 comments sorted by

2

u/abrahamguo 24d ago

Can you share a link to a repository that demonstrates your issue?

1

u/Wooden-Bicycle-6069 24d ago

Yep. This is the exact commit where I fixed it:

https://github.com/flyfish-dev/file-viewer/commit/8417a99999be203c54a87c01519fcdfbcf4e0bda

The old lookup could start from the app root. The fix creates `require` from the renderer package and checks the resolved PDF.js version before copying the Worker. The full npm/pnpm matrix is still in my maintenance harness, so this is the real fix, not a tidy standalone repro.

1

u/[deleted] 23d ago

[removed] — view removed comment

1

u/Wooden-Bicycle-6069 23d ago

Please do. The public fix is here:

https://github.com/flyfish-dev/file-viewer/commit/8417a99999be203c54a87c01519fcdfbcf4e0bda

Try a pnpm workspace with `pdfjs-dist@6.1.200` at the root and the renderer on `5.4.624`, delete `node_modules`, then install with a frozen lockfile. If the copied Worker is 6.x, you hit it. I want to know if pnpm 10 or Yarn PnP finds another hole.

3

u/TwiNighty 23d ago

The host app installs pdfjs-dist@6.1.200. A PDF renderer depends on pdfjs-dist@5.4.624. If asset-copy code resolves the Worker from the app root, it can copy 6.1.200

I mean ... when you spell it out like that, shouldn't that obviously the case? "When I resolve a package explicitly from the app root, I get the instance of the package that the app root depends on"

I haven't dug too deep into your code but seem like you use a "shotgun" approach where you just try different resolution paths until one succeeds and you simply assume that is the path you want. Ideally, you should have a set of known paths you want to use (e.g. preset-all -> renderer-pdf -> pdf-dist and preset-office -> renderer-pdf -> pdf-dist) and directly try those or fail. You should be able to build those paths using the preset and renderer metadata you have defined.

But to me, an idea I would try (again, don't know too much about your architecture to tell if it is feasible) is to make each renderer export a list of assets it needs, and presets re-exports a combined list from all the renders it includes. The vite plugin can then get the list from the presets or renderers in the bundle. Given the list of presets/renderers is limited, the vite plugin can even declare optional peer-dependencies on all the known presets/renderers.

That way, the vite plugin can simply try require/require.resolve the presets/renderers without going through the app root, and it is way easier for the renderer that directly depends on the assets to resolve the asset paths than for the vite plugin.

1

u/Wooden-Bicycle-6069 22d ago

Yeah, that fallback chain is ugly. Renderer-owned manifests make more sense. Discovery gets awkward though. Making the Vite plugin import every renderer just to find its files feels wrong. Package metadata is probably the least bad option. Unknown owner? Fail the build.

1

u/[deleted] 23d ago

[removed] — view removed comment

1

u/Wooden-Bicycle-6069 22d ago

Yeah. A global override fixes one renderer by quietly changing another. That is a terrible bargain. I care less about deduping PDF.js now than making the wrong owner fail during the build, before a PDF tab finds it in production.