Summary
ghostteadPath() resolves the platform package relative to its own module location. Any consumer that bundles @vibecook/ghosttead — which is the norm for an Electron main process — severs that resolution and gets MODULE_NOT_FOUND at startup.
Not a defect in the resolver's logic so much as an undocumented constraint: this package must not be bundled, and nothing says so.
Reproduction
Bundle an Electron main process with esbuild (platform: 'node', format: 'cjs') that calls ghostteadPath(), then launch:
Error: Cannot find module '@vibecook/ghosttead-darwin-arm64/package.json'
Require stack:
- /…/apps/godview/dist/main.cjs
at defaultResolvePackage (/…/dist/main.cjs:5338:172)
at ghostteadPath (/…/dist/main.cjs:5354:16)
at backendOptions (/…/dist/main.cjs:33769:63)
Cause
The resolver uses createRequire(import.meta.url). Once inlined, import.meta.url is the bundle's own location, so resolution runs from dist/ instead of from inside the package. Under pnpm's layout the platform package is reachable only from within @vibecook/ghosttead's private node_modules:
node_modules/.pnpm/@vibecook+ghosttead@0.5.0/node_modules/@vibecook/
├── ghosttead/
└── ghosttead-darwin-arm64 -> ../../../@vibecook+ghosttead-darwin-arm64@0.5.0/…
It is deliberately not hoisted to the consumer's node_modules, so from dist/ there is nothing to find. A bundler plus a strict layout is a normal combination, not an exotic one.
The --version failure in #21 masks this one — resolution has to succeed before the binary can fail to start — so a consumer hits them in sequence.
Workaround
Mark the package external. For esbuild:
external: ['electron', '@parcel/watcher', '@vibecook/ghosttead'],
Then require("@vibecook/ghosttead") resolves at runtime from the real package directory, import.meta.url is correct, and the platform package is visible. Confirmed working — startup proceeds to spawning the daemon.
Suggestions
- Document it. A line in the README — "this package resolves its platform binary at runtime and must be marked external if you bundle" — would be enough, and is what I would have wanted.
- Optionally make it bundle-tolerant.
resolvePackage is already an injectable seam, so a consumer can thread in a correct resolver, but that requires knowing the pnpm layout. Falling back to a resolution base derived from process.execPath or the consumer's node_modules when import.meta.url points somewhere with no node_modules would make the common case work unmodified.
For comparison, @vibecook/ghosttea-native-tabs has the same shape but no problem in practice — consumers call addonPath() at build time and copy the file, so the resolution happens before bundling.
Context
Found migrating vibecook-dev/chopsticks onto these packages. Already worked around there; filing so the constraint is written down somewhere other than that repo's build config.
Summary
ghostteadPath()resolves the platform package relative to its own module location. Any consumer that bundles@vibecook/ghosttead— which is the norm for an Electron main process — severs that resolution and getsMODULE_NOT_FOUNDat startup.Not a defect in the resolver's logic so much as an undocumented constraint: this package must not be bundled, and nothing says so.
Reproduction
Bundle an Electron main process with esbuild (
platform: 'node',format: 'cjs') that callsghostteadPath(), then launch:Cause
The resolver uses
createRequire(import.meta.url). Once inlined,import.meta.urlis the bundle's own location, so resolution runs fromdist/instead of from inside the package. Under pnpm's layout the platform package is reachable only from within@vibecook/ghosttead's privatenode_modules:It is deliberately not hoisted to the consumer's
node_modules, so fromdist/there is nothing to find. A bundler plus a strict layout is a normal combination, not an exotic one.The
--versionfailure in #21 masks this one — resolution has to succeed before the binary can fail to start — so a consumer hits them in sequence.Workaround
Mark the package external. For esbuild:
Then
require("@vibecook/ghosttead")resolves at runtime from the real package directory,import.meta.urlis correct, and the platform package is visible. Confirmed working — startup proceeds to spawning the daemon.Suggestions
resolvePackageis already an injectable seam, so a consumer can thread in a correct resolver, but that requires knowing the pnpm layout. Falling back to a resolution base derived fromprocess.execPathor the consumer'snode_moduleswhenimport.meta.urlpoints somewhere with nonode_moduleswould make the common case work unmodified.For comparison,
@vibecook/ghosttea-native-tabshas the same shape but no problem in practice — consumers calladdonPath()at build time and copy the file, so the resolution happens before bundling.Context
Found migrating
vibecook-dev/chopsticksonto these packages. Already worked around there; filing so the constraint is written down somewhere other than that repo's build config.