Skip to content

@vibecook/ghosttead breaks when bundled: platform package resolution is relative to its own module location #22

Description

@jamesyong-42

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

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions