Skip to content

fix: resolve sharp through @papra/lecture, not a hoisted path - #6

Closed
jtrotsky wants to merge 1 commit into
daemonless:mainfrom
jtrotsky:fix-sharp-guard-isolated-linker
Closed

jtrotsky wants to merge 1 commit into
daemonless:mainfrom
jtrotsky:fix-sharp-guard-isolated-linker

Conversation

@jtrotsky

@jtrotsky jtrotsky commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

The latest build has failed since papra 26.6.2. The sharp/wasm32 version guard reads
node_modules/sharp/package.json by literal path, but 26.6.2 changed pnpm's linker so
sharp is no longer at that path.

Resolve sharp through @papra/lecture with createRequire instead, which works under
either linker layout and matches how the application loads it at runtime.

Environment

Image ghcr.io/daemonless/papra:latest
App version (last good) 26.6.1
App version (breaks) 26.6.2
Node 26.7.0
pnpm 11.1.1
sharp 0.34.5
@img/sharp-wasm32 0.34.5 (pinned)
@papra/lecture 0.5.1

Symptom

Containerfile STEP 20/22 fails:

Error: ENOENT: no such file or directory, open 'node_modules/sharp/package.json'
  code: 'ENOENT',
  path: 'node_modules/sharp/package.json'
Error: building at STEP "RUN cd /app/production && ... SHARP_VER=$(node -e "$PKGVER" node_modules/sharp/package.json) ..."

Root cause

papra 26.6.2 changed pnpm-workspace.yaml:

-nodeLinker: hoisted
 verifyDepsBeforeRun: false
+injectWorkspacePackages: true
+syncInjectedDepsAfterScripts:
+  - build

Dropping nodeLinker: hoisted reverts pnpm to its default isolated linker. sharp is a
transitive dependency (@papra/lecture → sharp), so it is no longer hoisted to the top of
the deployed tree:

Linker sharp location in /app/production
hoisted (≤ 26.6.1) node_modules/sharp
isolated (≥ 26.6.2) node_modules/.pnpm/sharp@0.34.5/node_modules/sharp

The guard's literal path only ever matched the hoisted layout.

Evidence

  • sharp@0.34.5 does install in the workspace — the build log shows its install script
    running (.../sharp@0.34.5/node_modules/sharp install: Done) well before the failing step.
    It is present, just not where the guard looks.
  • The deploy step logs Virtual store is at: ../app/production/node_modules/.pnpm, confirming
    the isolated layout.
  • STEP 19, which patches @papra/lecture at /app/production/node_modules/@papra/lecture,
    still passes: @papra/lecture is a direct dependency of app-server, so it stays at the
    top level under both linkers. Only the transitive dependency moved.

Ruled out

  • @papra/lecture dropping sharp — packages/lecture/package.json is byte-identical
    between 26.6.1 and 26.6.2 (sharp: catalog:, version 0.5.1 in both).
  • app-server dropping lecture — apps/papra-server/package.json declares
    @papra/lecture: workspace:* in both tags, unchanged.
  • A sharp version bump — the catalog pins sharp: ^0.34.5 in both tags, so the
    @img/sharp-wasm32@0.34.5 pin is still correct. This is not version drift; the guard never
    got far enough to compare versions.
  • A change on this side — the only commits here since the last green build are the
    previous fix and its generated-file follow-up; neither touches this step.

Fix

Resolve sharp via createRequire from @papra/lecture's manifest:

const req = createRequire('/app/production/node_modules/@papra/lecture/package.json');
const sharpPkg = req.resolve('sharp/package.json');

This is layout-agnostic, and it exercises the same resolution the application performs at
runtime rather than an approximation of it. require('sharp') is likewise called through that
require, so the wasm load test now runs from the context that actually loads sharp.

@img/sharp-wasm32 continues to be read by literal path: the tar-merge step places it at the
top level itself, so it is not subject to the linker layout. sharp still finds it by walking up
from the virtual store to /app/production/node_modules.

The stale nodeLinker: hoisted comment above the dependency-pinning step is corrected in the
same commit — that step survived the change only because it already tried two lookup bases.

Testing

The guard logic was exercised against three fixtures built to mirror pnpm's on-disk layouts,
run under node 26.7.0 in a throwaway container from this image:

Fixture Expected Result
isolated (26.6.2 layout, sharp in .pnpm, symlinked into lecture) pass pass — resolved from the virtual store
hoisted (≤ 26.6.1 layout, sharp at top level) pass pass — backward compatible
drift (sharp 0.34.5 vs wasm32 0.34.4) fail fail — VERSION DRIFT message intact

Not yet validated by a full image build; the CI run on this PR is the real test.

Note on build timeouts

Three of the four runs since 26.6.2 ended by timing out rather than failing fast — one was
cancelled at the 6-hour job limit. Those look like interactive debug sessions held open rather
than a second defect, but worth confirming the runner isn't left waiting after a build failure.

papra 26.6.2 dropped `nodeLinker: hoisted` from pnpm-workspace.yaml (adding
injectWorkspacePackages instead), reverting pnpm to its default isolated
linker. sharp is a transitive dep (@papra/lecture -> sharp), so it moved from
/app/production/node_modules/sharp to
/app/production/node_modules/.pnpm/sharp@0.34.5/node_modules/sharp.

The wasm-fallback guard read that path literally and died with ENOENT before it
could compare versions, breaking every build since 26.6.2.

Resolve sharp with createRequire from @papra/lecture's manifest instead. That
works under either linker layout and is the same resolution the app performs at
runtime, so the require('sharp') load test now exercises the real path.

@img/sharp-wasm32 is still read by literal path: the tar-merge step places it at
the top level itself, so it is not subject to the linker layout.

Also corrects the now-stale `nodeLinker: hoisted` comment above the dependency
pinning step, which survived the change only because it already tried two
lookup bases.

Verified against fixtures mirroring both pnpm layouts plus a version-drift case:
isolated resolves from the virtual store, hoisted still resolves at top level,
and drift still fails with the actionable message.
@jtrotsky
jtrotsky requested a review from ahze September 7, 2026 01:36
@jtrotsky
jtrotsky marked this pull request as draft September 7, 2026 21:06
@ahze ahze self-assigned this Sep 27, 2026
@ahze ahze closed this in 4e64886 Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants