{"_id":"@janhapke/sharp-electron","_rev":"3-1e3513293fdb2e4b4a6875c9ae1a35b7","name":"@janhapke/sharp-electron","dist-tags":{"latest":"0.35.4-electron.1"},"versions":{"0.35.3-electron.0":{"name":"@janhapke/sharp-electron","version":"0.35.3-electron.0","keywords":["sharp","libvips","electron","image","jpeg","png","resize"],"author":{"name":"Jan Hapke","email":"sharp-electron@janhapke.com"},"license":"Apache-2.0","_id":"@janhapke/sharp-electron@0.35.3-electron.0","maintainers":[{"name":"janhapke","email":"npm@janhapke.com"}],"homepage":"https://github.com/janhapke/sharp-electron#readme","bugs":{"url":"https://github.com/janhapke/sharp-electron/issues"},"dist":{"shasum":"a8259b2f02474e8bebdaf3f46821c10a645909b8","tarball":"https://registry.npmjs.org/@janhapke/sharp-electron/-/sharp-electron-0.35.3-electron.0.tgz","fileCount":103,"integrity":"sha512-B7I9M0hdzrAA+SoV35KoW3+bO/TGokJuCBSAyrNdoJo6qIm/mnWW40QqQ9KA2x2S2y74Vw2OQ2IUYzEAVz2+iQ==","signatures":[{"sig":"MEUCIDa7kb0uKouvubqfI3Fdy6HuBhbq7aAweMgTk8NodU9ZAiEAkacNbQV0iPfCIzdSlBliVBe8N2Fw8G4hje6BReqzIO4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19470993},"main":"index.js","type":"commonjs","types":"index.d.ts","engines":{"node":">=20.9.0"},"gitHead":"87bf5eba05a41c941c6654a3b4f5393e5ea46c0d","_npmUser":{"name":"janhapke","email":"npm@janhapke.com"},"repository":{"url":"git+https://github.com/janhapke/sharp-electron.git","type":"git","directory":"package"},"_npmVersion":"11.6.2","description":"Electron-safe replacement for sharp on Linux; re-exports the real sharp unmodified everywhere else","directories":{},"_nodeVersion":"24.11.1","dependencies":{"sharp":"0.35.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/sharp-electron_0.35.3-electron.0_1783120897189_0.9231380778851008","host":"s3://npm-registry-packages-npm-production"}},"0.35.3-electron.1":{"name":"@janhapke/sharp-electron","version":"0.35.3-electron.1","keywords":["sharp","libvips","electron","image","jpeg","png","resize"],"author":{"name":"Jan Hapke","email":"sharp-electron@janhapke.com"},"license":"Apache-2.0","_id":"@janhapke/sharp-electron@0.35.3-electron.1","maintainers":[{"name":"janhapke","email":"npm@janhapke.com"}],"homepage":"https://github.com/janhapke/sharp-electron#readme","bugs":{"url":"https://github.com/janhapke/sharp-electron/issues"},"dist":{"shasum":"e5a89581304126301475d8c04952641078b6cd07","tarball":"https://registry.npmjs.org/@janhapke/sharp-electron/-/sharp-electron-0.35.3-electron.1.tgz","fileCount":103,"integrity":"sha512-ZDnFPMsKolANxS+RDjgxw6/jc6O0JfJ0msu6bgBdPCz6NFm4+K+cPurwXB+rdl5UY2/EQ5bzQ7ENYnRXtdprLQ==","signatures":[{"sig":"MEUCIQDmxvYMiE8iHrQK/mYMACXtvzBbMiiQjnMGmwp1ZMRtCwIgQuOpsmshJbikcWPjlRXfJuuqyO6NUcZhqS9l1S3U09c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19472307},"main":"index.js","type":"commonjs","types":"index.d.ts","engines":{"node":">=20.9.0"},"gitHead":"38158255a299087c6aab59fceaa3619f5710f5dc","_npmUser":{"name":"janhapke","email":"npm@janhapke.com"},"repository":{"url":"git+https://github.com/janhapke/sharp-electron.git","type":"git","directory":"package"},"_npmVersion":"11.6.2","description":"Electron-safe replacement for sharp on Linux; re-exports the real sharp unmodified everywhere else","directories":{},"_nodeVersion":"24.11.1","dependencies":{"sharp":"0.35.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/sharp-electron_0.35.3-electron.1_1783124308671_0.2002659998467642","host":"s3://npm-registry-packages-npm-production"}},"0.35.4-electron.1":{"name":"@janhapke/sharp-electron","version":"0.35.4-electron.1","description":"Electron-safe replacement for sharp on Linux; re-exports the real sharp unmodified everywhere else","author":{"name":"Jan Hapke","email":"sharp-electron@janhapke.com"},"type":"commonjs","main":"index.js","types":"index.d.ts","license":"Apache-2.0","homepage":"https://github.com/janhapke/sharp-electron#readme","repository":{"type":"git","url":"git+https://github.com/janhapke/sharp-electron.git","directory":"package"},"bugs":{"url":"https://github.com/janhapke/sharp-electron/issues"},"publishConfig":{"access":"public"},"keywords":["sharp","libvips","electron","image","jpeg","png","resize"],"engines":{"node":">=20.9.0"},"dependencies":{"sharp-upstream":"npm:sharp@0.35.4"},"gitHead":"f7afa507bfc6975bad73ed9c6a8ee5c3be88b848","_id":"@janhapke/sharp-electron@0.35.4-electron.1","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-a8G1T2Cs+SND8Be61gJspHNoUXXVQujbfnHr1+a6IxsPgDNSId58LjiL+HMPg0pCxyMY7aC8U/INRWRp7G7RXQ==","shasum":"a45fec3fc9b04baf79cdf68d51df5d22dfe73aa2","tarball":"https://registry.npmjs.org/@janhapke/sharp-electron/-/sharp-electron-0.35.4-electron.1.tgz","fileCount":103,"unpackedSize":19907544,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCbUSqD/CcYwM1+u6xfeBPprSxdvJPPqysvcWEGQGga0QIge/06byECHFvr+F8QMnk6wdG0Wxz8YzcB2xKi9kLQvH4="}]},"_npmUser":{"name":"janhapke","email":"npm@janhapke.com"},"directories":{},"maintainers":[{"name":"janhapke","email":"npm@janhapke.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sharp-electron_0.35.4-electron.1_1787870850229_0.043235411098994314"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-03T23:21:36.990Z","modified":"2026-08-27T22:47:30.731Z","0.35.3-electron.0":"2026-07-03T23:21:37.512Z","0.35.3-electron.1":"2026-07-04T00:18:28.980Z","0.35.4-electron.1":"2026-08-27T22:47:30.525Z"},"bugs":{"url":"https://github.com/janhapke/sharp-electron/issues"},"author":{"name":"Jan Hapke","email":"sharp-electron@janhapke.com"},"license":"Apache-2.0","homepage":"https://github.com/janhapke/sharp-electron#readme","keywords":["sharp","libvips","electron","image","jpeg","png","resize"],"repository":{"type":"git","url":"git+https://github.com/janhapke/sharp-electron.git","directory":"package"},"description":"Electron-safe replacement for sharp on Linux; re-exports the real sharp unmodified everywhere else","maintainers":[{"name":"janhapke","email":"npm@janhapke.com"}],"readme":"# sharp-electron\n\nA drop-in replacement for [`sharp`](https://github.com/lovell/sharp) that doesn't segfault under Electron on Linux.\n\n`sharp` crashes with a native segfault — not a catchable JS error — when decoding an image (JPEG, PNG, etc.) inside any process built on Electron's Linux binary. The cause is two copies of `glib` colliding in one process; no configuration flag fixes it. This package is a rebuild of `sharp` and its bundled `libvips` with the collision fixed at the linker level. To use it, add one `overrides` entry to your `package.json` ([Usage](#usage)). macOS and Windows aren't affected, so there this package transparently re-exports the real, unmodified `sharp`. To rebuild it yourself or bump it to a newer `sharp` release, see [Building from source](#building-from-source) and [Engineering notes](#engineering-notes).\n\n## The problem\n\nElectron's Linux binary dynamically links the system's `glib` (for GTK integration). `sharp`/`libvips` bundle their own private `glib` inside `libvips-cpp.so`, but don't fully hide its symbols. Two copies of `glib` exporting the same symbols into one process corrupt `glib`'s internal state, and the process dies with `SIGSEGV` the first time an image is actually decoded — no JS exception, no stack trace. This affects any process built on Electron's binary, including a `utilityProcess`, and is a known, currently-unresolved upstream bug: [electron/electron#46323](https://github.com/electron/electron/issues/46323).\n\nCharacteristically, *encoding* raw pixels works fine — only *decoding* a real compressed image crashes. If that matches what you're seeing, this is your bug.\n\n## Usage\n\nUse npm `overrides` — not a direct `import` — so that **every** `require('sharp')` / `import sharp from 'sharp'` in your dependency tree, direct or transitive, resolves to this package:\n\n```json\n{\n  \"overrides\": {\n    \"sharp\": \"npm:@janhapke/sharp-electron@0.35.4-electron.1\"\n  }\n}\n```\n\n```bash\nnpm install\n```\n\nThat's it — no source changes anywhere in your project. The `npm:` alias syntax is required: the override target's package name differs from `sharp`, so a plain `\"sharp\": \"0.35.3-electron.1\"` would tell npm to look for a package literally named `sharp` at that version, which doesn't exist.\n\nIf `sharp` is also a **direct dependency** of your project, npm rejects an override that conflicts with it (`EOVERRIDE`). In that case point the dependency itself at this package and let the override reference it via the **nested** form, not a plain `\"sharp\": \"$sharp\"` (npm requires the nested form whenever a root dependency and an override target the same package name, regardless of what either resolves to):\n\n```json\n{\n  \"dependencies\": {\n    \"sharp\": \"npm:@janhapke/sharp-electron@0.35.4-electron.1\"\n  },\n  \"overrides\": {\n    \"sharp\": \"$sharp\"\n  }\n}\n```\n\nThis package's own internal fallback dependency (the real, unpatched `sharp` its non-Linux build and its TypeScript types re-export) is deliberately installed under the aliased name `sharp-upstream`, not `sharp` — so an `overrides` rule targeting `\"sharp\"` can never recurse into it, however the rule is written. (Earlier releases installed it as a plain `sharp` dependency, which meant a *plain* `overrides` rule silently rewrote that internal dependency back into a self-referential circular reference — no install error, but `require('sharp')` inside the fallback returned `{}` at runtime and `tsc` failed to resolve `sharp`'s types. If you hit that, upgrade to `0.35.4-electron.1` or later.)\n\n### Local testing against an unpublished build\n\nTo test a locally built copy of this repo (see [Building from source](#building-from-source)) against a real consuming project, **don't** point an `overrides` rule at `file:../sharp-electron/package` or a packed `.tgz` — both reliably trigger bugs in npm's dependency resolver (`@npmcli/arborist`) once the consuming project's dependency graph is large enough: a `file:`-directory override crashes `npm install` outright (`Cannot read properties of null (reading 'package')`, a race in how arborist links `file:` \"Link\" nodes when more than one edge resolves to the same target concurrently — confirmed via `Promise.all` in the crash's own stack trace), and overriding to a packed tarball can leave a **broken symlink pointing at the `.tgz` file itself** instead of extracting it. Both are npm bugs, not something fixable from this package's `package.json`.\n\nThe reliable way to test a local build: let npm install the *real* `sharp` normally (no override at all — this sidesteps the buggy code path entirely, since `overrides` never enters the picture), then physically replace every real-`sharp` directory it created with this repo's built `package/`:\n\n```bash\n# in your consuming project, with a normal \"sharp\": \"^X.Y.Z\" dependency (no override):\nrm -rf node_modules && npm install\n\n# then, for every node_modules/**/sharp directory npm actually created:\nfind node_modules -maxdepth 4 -iname sharp -type d | while read -r d; do\n  rm -rf \"$d\"\n  cp -r ../sharp-electron/package \"$d\"\ndone\n```\n\nRe-run the `find`/`cp -r` loop after every rebuild of this repo (`npm run package`) — there's no live symlink here, just a plain directory copy, so changes don't propagate on their own.\n\n### Why `overrides`, not a direct import\n\nYou *can* instead `npm install @janhapke/sharp-electron` and change call sites to import it directly — but only safely if you change **every** place your project touches `sharp`, including transitive dependencies you don't control.\n\nThe failure mode if you miss one: this package and the real `sharp` both ship a `libvips-cpp.so` with the identical SONAME (intentionally, for compatibility). The ELF dynamic linker deduplicates shared libraries by SONAME **within a process** — once one `libvips-cpp.so` is loaded, every other module in that process that needs that SONAME silently reuses the already-loaded copy, regardless of how correctly it was built or what its RPATH says. So if two plugins each import `sharp` and only one is switched over, whichever loads first wins for both. If the unpatched one wins, the symptom isn't even the original segfault — it's a confusing ELF loader error (`undefined symbol: vips_g_...`) in the correctly-patched module. This exact scenario happened while validating this package in a real project.\n\n`overrides` sidesteps the problem structurally: every `sharp`-touching module resolves to the exact same files, so two different `libvips-cpp.so`s can never coexist in the process. Auditing call sites by hand gives no such guarantee.\n\n### Platform support\n\n| Platform | Behavior |\n|---|---|\n| `linux-x64` | The patched build. Verified against `sharp`'s own upstream test suite and a dedicated Electron crash-regression test. |\n| `linux` (other architectures, e.g. `arm64`) | Throws a clear error at load time rather than silently falling back to real `sharp` — a silent fallback would just defer the crash to the first image decode. |\n| macOS / Windows | Re-exports the real, unmodified `sharp` (a normal dependency of this package; npm installs the correct official binary automatically). The bug doesn't exist on these platforms. |\n\n### Troubleshooting\n\n- **`undefined symbol: vips_g_...` at load time** (an exception, not a crash) — almost always the SONAME-collision issue described above: some module in the process still resolves `sharp` to the real, unpatched package. Check that the `overrides` entry is in place and that `node_modules/sharp` actually contains this package's files.\n- **Segfault decoding an image under Electron on Linux, even with `overrides` correctly in place** — an unpatched `libvips-cpp.so` from some other source is winning the SONAME race (e.g. a copy loaded by a non-npm mechanism). Check what else in the process loads `libvips`.\n- **`ERR_DLOPEN_FAILED: libvips-cpp.so.8.18.3: cannot open shared object file` — but only in a *packaged* app, never under `npm start`/dev mode.** This isn't a `sharp-electron` bug — it's your packager silently dropping or mis-placing the `libvips-cpp.so` that sits next to `linux-x64/sharp/.../sharp-linux-x64-*.node` (found via that addon's `$ORIGIN` RPATH). Two independent causes, both confirmed in a real Electron Forge project — check both:\n  1. **Your own native-module whitelist/externals script only walks `dependencies`, not `optionalDependencies`.** If you (or your packaging plugin) hand-roll a \"which `node_modules` packages contain native binaries\" trace to avoid webpack bundling them, and it only follows `package.json`'s `dependencies` field, it will never reach `@img/sharp-libvips-linux-x64` — prebuilt-binary packages like this are conventionally declared as `optionalDependencies`. Result: the `.so` gets excluded from the packaged app outright, not just mis-placed.\n  2. **Your asar tooling unpacks `.node` files but not their `.so` sidecars.** Electron Forge's `@electron-forge/plugin-auto-unpack-natives`, for example, sets `asar.unpack` to a glob matching only `**/*.node` — it physically moves the addon out of the compressed `app.asar` into `app.asar.unpacked`, but leaves any co-located shared library trapped inside the archive, where `dlopen()` can never reach it (even though `npx asar list` will still show it \"present\" — that lists the archive's logical contents, not what's been physically unpacked). You need to extend the unpack glob yourself to also match `.so`/`.so.*`, e.g. (Forge): `packagerConfig.asar.unpack = '{**/*.so,**/*.so.*}'` (merges with the plugin's own `.node` pattern rather than replacing it).\n\n  Diagnose by checking the packaged output directly, not just build logs: `find <packaged-app>/resources/app.asar.unpacked -iname 'libvips-cpp.so*'` — if it's missing, you're hitting #1; if the addon is there but the `.so` isn't, you're hitting #2.\n\n- **`npm error Invalid comparator` from `npm install`/`npm ci`, before any code runs at all.** This is an npm bug, not a `sharp-electron` bug — but it's directly triggered by following the [Usage](#usage) instructions above verbatim. Root cause: npm's arborist cannot resolve an `overrides` rule targeting a package name (`sharp`) when your **root** `package.json`'s own dependency of that same name is declared via an `npm:` alias (exactly the form both `overrides` snippets above use: `\"sharp\": \"npm:@janhapke/sharp-electron@...\"`). The alias means your root \"version\" for `sharp` isn't an ordinary semver range, and arborist's overrides matcher throws trying to compare it against the override's version selector — regardless of whether your override is the flat or nested form.\n\n  There's no `package.json`-only fix for this — it reproduces with either override shape. Two ways out, in order of preference:\n\n  1. **Drop the `npm:` alias in favor of the plain `overrides`-only form** (top of [Usage](#usage)): don't add `sharp` as a direct/dev dependency at all, just the single `overrides` entry pointing at this package by its real name. This is exactly what that form was already meant for — the crash only happens when a root dependency *and* an override target the same name *and* the root side is an alias. If nothing else in your tree needs `sharp` as a direct dependency, this sidesteps the bug entirely and keeps the full structural guarantee `overrides` gives (see [Why overrides](#why-overrides-not-a-direct-import)).\n  2. **If you do need `sharp` as a direct/dev dependency via the `npm:` alias** (e.g. your own code does `import sharp from 'sharp'` and you want that exact package resolved, not just an override rewriting transitive requires), you can't use `overrides` for this package at all without hitting the crash. Fall back to a targeted `postinstall` step that strips any other dependency's own bundled, nested copy of `sharp` so its `require('sharp')` has nothing local to resolve to and falls through to your root, aliased install instead:\n\n     ```json\n     {\n       \"scripts\": {\n         \"postinstall\": \"node -e \\\"require('fs').rmSync('node_modules/<the-other-dependency>/node_modules', { recursive: true, force: true })\\\"\"\n       }\n     }\n     ```\n\n     This is narrower than `overrides`' guarantee — it only closes the gap for the specific nested copy you name, not structurally for anything that might bundle `sharp` in the future. If you add another dependency that also bundles its own `sharp`, you need to extend this by hand; nothing will warn you if you miss one (the symptom would be the original SONAME-collision failure mode above, not an install-time error). Revisit option 1 if you can.\n\n## Building from source\n\nRequires only Docker on the host — the entire toolchain runs in a container matching `sharp`'s own CI environment.\n\n```bash\ngit clone --recurse-submodules https://github.com/janhapke/sharp-electron.git\ncd sharp-electron\nnpm install\nnpm run build\n```\n\nThis applies the patches to the vendored `sharp-libvips`/`sharp` submodules, rebuilds both, runs both test gates, and packages the result into `package/` — ready to consume via a `file:` override as shown above. The individual stages are also available separately: `npm run build:libvips`, `npm run build:sharp`, `npm run test:gates`, `npm run package`.\n\nTo confirm the bug itself on the *unpatched* `sharp` first (recommended before trusting any of this): `npm run repro` runs [test/electron-crash-repro.js](test/electron-crash-repro.js) against the stock npm `sharp` under `ELECTRON_RUN_AS_NODE` — expect the encode step to succeed and the decode step to kill the process.\n\n## Engineering notes\n\nEverything a contributor (or future maintainer bumping versions) needs to know about how and why this works.\n\n### How the fix works\n\nThe fix has two halves, applied as patches (in [patches/](patches/)) to two upstream repos vendored as git submodules pinned at release tags. The submodules stay pristine in git; [scripts/apply-patches.sh](scripts/apply-patches.sh) applies the patches to their working trees at build time (idempotently — it detects an already-patched tree and skips).\n\n**Phase A — `sharp-libvips` (the `libvips` binary).** `sharp-libvips` doesn't vendor `libvips`'s source; its `build/posix.sh` downloads the upstream release tarball at build time and patches it inline. Our patch extends that same mechanism:\n\n- Adds `extra/glib_wrapper.c`/`.h`: thin wrapper functions re-exporting each needed `glib` symbol under a renamed `vips_g_*` identity (e.g. `vips_g_malloc` calls the bundled `glib`'s `g_malloc`).\n- Broadens the `vips.map` linker version script from hiding a single symbol (`g_param_spec_types` — upstream's own partial fix for this class of bug) to hiding *everything* except `libvips`'s own API: `{ global: vips_*; _Z*; local: *; }`. This is what actually removes the colliding `g_*` exports from `libvips-cpp.so`.\n- Patches `libvips`'s C++ header `VImage8.h`, whose inline `VObject` smart-pointer class calls `g_object_ref`/`g_object_unref` directly — those calls get compiled straight into `sharp`'s addon wherever `vips::VImage` is used, so they must be redirected at the header level, not in `sharp`'s source.\n\nOne non-obvious gotcha, recorded because it *will* bite again: `libvips-cpp.so`'s meson target sets `gnu_symbol_visibility: 'hidden'`, which strips any new symbol from the dynamic table at compile time — before the version script even applies — unless it's explicitly marked `__attribute__((visibility(\"default\")))`. The build succeeds cleanly either way; only `objdump -T` reveals the difference. A clean build is not sufficient evidence here.\n\n**Phase B — `sharp` (the Node addon).** Redirects `sharp`'s own direct `glib` calls (`common.cc`, `metadata.cc`, `sharp.cc`) to the `vips_g_*` wrappers, then rebuilds the addon against the Phase A `libvips` using `sharp`'s own supported mechanism for custom libvips builds: `SHARP_FORCE_GLOBAL_LIBVIPS=1` plus `PKG_CONFIG_PATH` pointed at generated `.pc` files. The build runs in a Docker image ([scripts/sharp-build.Dockerfile](scripts/sharp-build.Dockerfile)) replicating `sharp`'s official CI environment (Rocky Linux 8, gcc-toolset-14, Node 20), so nothing touches the host toolchain and the binary matches upstream's baseline glibc compatibility.\n\n### The wrapper symbol set\n\nSeven symbols are wrapped. The machine-readable source of truth — including each symbol's origin, risk classification, and how it was found — is [patches/wrapper-symbols.json](patches/wrapper-symbols.json); version bumps should diff a fresh grep against that file, not against this prose. The short version:\n\n- `g_object_ref`, `g_object_unref` — from `libvips`'s `VImage8.h` inline header (see Phase A above); the original crash culprits.\n- `g_malloc`, `g_free` — from `sharp`'s own source. Wrapped **as a pair** deliberately: an allocator and its matching free must come from the same `glib` copy, or the result is silent heap corruption, not just a symbol-hygiene issue.\n- `g_signal_connect_data`, `g_log_set_handler` — from `sharp`'s own source.\n- `g_utf8_validate` — from `sharp`'s `metadata.cc`. Notably, this one was **missed by grepping** and only surfaced when running `sharp`'s full test suite against the rebuilt addon (`undefined symbol` at runtime). Lesson: the test-gate loop below is load-bearing, not a formality — expect a version bump to surface a symbol the grep missed.\n\n### Packaging details that matter\n\nThree hard-won, non-obvious decisions live in [scripts/package-local.sh](scripts/package-local.sh):\n\n- **`libvips-cpp.so` sits next to the `.node` addon, found via RPATH — and it must be old-style `DT_RPATH`, not `DT_RUNPATH`.** `patchelf --set-rpath` produces `DT_RUNPATH` by default, which the loader consults *after* `LD_LIBRARY_PATH` — so anything else in a real consumer's environment or `node_modules` that provides the same SONAME can win over the co-located patched copy. `patchelf --force-rpath` produces `DT_RPATH`, consulted *before* `LD_LIBRARY_PATH`, so the co-located copy always wins for the addon's own direct dependency. (Setting `process.env.LD_LIBRARY_PATH` from JavaScript doesn't work at all: glibc reads it once at process start, not per `dlopen()`.)\n- **The SONAME is kept identical to upstream's** — that's what makes the per-process SONAME deduplication safe *when everything resolves to this package* (see [Why overrides](#why-overrides-not-a-direct-import)) and is why `overrides` is the recommended install method rather than an optional nicety.\n- **`package/` runs its own `npm install` during packaging** because npm does not recursively install a `file:` dependency's own dependencies the way it does for registry packages — without this, the non-Linux `require('sharp-upstream')` fallback breaks in every local-testing setup. Relatedly, `package/package.json` must **not** declare `\"os\"`/`\"cpu\"` restrictions: those make npm skip installing the package entirely on other platforms, which would prevent the fallback from ever existing there.\n- **The fallback's real-`sharp` dependency is named `sharp-upstream`, not `sharp`** (`\"sharp-upstream\": \"npm:sharp@<version>\"` in `package/package.json`, matched by `require('sharp-upstream')` in `index.js` and `index.d.ts`). A consumer's `overrides` rule targeting `\"sharp\"` applies recursively to every dependency edge named `sharp` anywhere in the tree — including this package's own internal one, since npm doesn't distinguish \"the package I'm overriding\" from \"a same-named dependency three levels inside it.\" Without the alias, that self-reference resolves silently (no install error) into a circular reference: `require('sharp')` inside the fallback returns `{}` at runtime, and `index.d.ts`'s `import sharp = require('sharp')` can't resolve real `sharp`'s types, producing spurious `tsc` errors in consumers (`has no exported member 'X'`, `can only be referenced with ECMAScript imports`) with no indication the cause is this package. The alias sidesteps the collision structurally, the same reason this package's own published name isn't `sharp`.\n\n### Test gates\n\nEvery build must pass both, enforced by [scripts/run-gates.sh](scripts/run-gates.sh) (non-zero exit on failure, usable in CI):\n\n1. **`sharp`'s own upstream test suite** against the rebuilt addon (1804/1811 at last run; the one known failure is `test/unit/esm.mjs`, a pre-existing Node CJS/ESM interop quirk unrelated to these patches — it fails identically against stock `sharp`).\n2. **The Electron crash regression test** ([test/electron-crash-repro.js](test/electron-crash-repro.js)): encode raw pixels to JPEG, then decode a real JPEG via `.metadata()` and `.resize().toBuffer()`, under `ELECTRON_RUN_AS_NODE`. On an unpatched build the decode step reliably segfaults; on a correct build both steps pass. The same script serves both directions via `SHARP_MODULE_PATH`.\n\n**If a gate fails with `undefined symbol: g_<something>`**: that's a missing wrapper symbol. Add it to `extra/glib_wrapper.c`/`.h` in `vendor/sharp-libvips` (remember the `visibility(\"default\")` attribute), patch the call site, update `patches/wrapper-symbols.json`, regenerate the patch files, rebuild, re-run the gates. This loop is normal — it's how `g_utf8_validate` was found.\n\n### Alternatives that didn't work\n\nTried before concluding a from-source rebuild was the only real fix:\n\n| Approach | Result |\n|---|---|\n| `LD_PRELOAD` sharp's `libvips-cpp.so` before Electron starts | Made Electron crash even earlier |\n| `objcopy --localize-symbols` (hide symbols post-hoc, no recompile) | Still segfaults — `sharp.node`'s undefined references then bind to Electron's `glib` instead |\n| `RTLD_DEEPBIND` | Crashes differently, during load |\n| Do `sharp` work in a real separate Node process (not Electron's binary) | Works, but requires bundling a Node binary and adds an IPC boundary |\n\n### Updating to a new `sharp` release\n\nThis is deliberately a guided Claude Code command, not a blind script: [.claude/commands/rebuild-for-version.md](.claude/commands/rebuild-for-version.md) (`/rebuild-for-version <sharp-version> <sharp-libvips-version>`). A version bump involves genuine judgment calls a script would get silently wrong — whether the bug even still reproduces upstream, whether a patch conflict is trivial context drift or a structural change, whether the wrapper symbol set changed. The command:\n\n1. Re-pins the submodules and first verifies the bug **still reproduces on an unpatched build** — upstream may have fixed it.\n2. Attempts the existing patches; diagnoses (rather than force-fixes) any conflict.\n3. Re-runs the exhaustive `glib`-call grep and diffs it against `patches/wrapper-symbols.json`.\n4. Rebuilds, runs both gates, and follows the wrapper-expansion loop above (bounded to 3 iterations before stopping to report).\n5. Re-verifies symbol visibility (`objdump -T`) and `DT_RPATH` (`readelf -d`), and prepares — **but never executes** — the release.\n\nVersion scheme: `<upstream-sharp-version>-electron.N` — the version always names the exact `sharp` release it tracks; `N` bumps for this package's own revisions. Every version this scheme produces is, by construction, a semver **prerelease** (the `-electron.N` suffix), which matters at publish time — see below.\n\n### Releasing\n\n`npm run release <version>` ([scripts/release.sh](scripts/release.sh)) runs the full pipeline, then syncs every version reference — `package/package.json`'s `version` and its `sharp` fallback dependency (pinned to the upstream part of the new version), and this README's install snippets — then prints the remaining steps. **Publishing is always a manual, human-confirmed action** — no script or command in this repo runs `npm publish`, `git tag`, or `git push` on its own. That's a deliberate design decision, not a missing feature: a bad automated judgment call shipping silently to consumers is a worse failure mode than a release waiting a day for review.\n\n**Publishing requires `--tag latest`, every time**: `npm publish --tag latest` (not plain `npm publish`). Because every version has the `-electron.N` suffix, npm/semver treats it as a prerelease and refuses to move the `latest` dist-tag implicitly (`npm error: You must specify a tag using --tag when publishing a prerelease version`) — without `--tag latest`, a bare `npm install @janhapke/sharp-electron` (or the version-less `overrides` form) would resolve to nothing. `--access public` is not needed on the command line; it's already set via `package/package.json`'s `publishConfig`.\n\nNote for the published package: `package/README.md` is generated at package time (a copy of this file, since npmjs.com displays the published package's own README) — edit this file, never that copy.\n\n### Repository layout\n\n```\npatches/                     the actual fix: two .patch files + wrapper-symbols.json (symbol manifest)\nvendor/sharp-libvips/        git submodule, pinned at the target release tag (pristine; patched at build time)\nvendor/sharp/                git submodule, same\nscripts/                     the full pipeline: apply-patches → build-libvips → build-sharp → run-gates → package-local\ntest/electron-crash-repro.js the regression test (and original bug repro)\npackage/                     the published npm package: index.js dispatcher, index.d.ts (forwards to sharp's own types), built linux-x64/ payload\ndist/                        gitignored Phase A build output\n.github/workflows/ci.yml    Linux full-pipeline job + macOS/Windows fallback smoke test\n```\n\n## Status\n\nThe `linux-x64` build passes both test gates and has been verified end-to-end in a real consuming Electron project. The macOS/Windows fallback is implemented and covered by CI — check the [workflow's status](.github/workflows/ci.yml) for the current result on real runners. `linux-arm64` has no patched build — it fails loudly rather than silently.\n\n## License\n\nApache-2.0, matching `sharp`. See [LICENSE](LICENSE) and [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) (this project patches `libvips`, LGPL-2.1-or-later; the `glib_wrapper.c`/`.h` shim is original code).\n","readmeFilename":"README.md"}