Skip to content

Fix #62: declare picocolors and @clack/core as runtime dependencies#68

Open
hyericlee wants to merge 1 commit into
mainfrom
fix/issue-62-phantom-deps
Open

Fix #62: declare picocolors and @clack/core as runtime dependencies#68
hyericlee wants to merge 1 commit into
mainfrom
fix/issue-62-phantom-deps

Conversation

@hyericlee
Copy link
Copy Markdown
Contributor

Problem

Fixes #62opkg add crashes at startup on some installs with:

Cannot find package 'picocolors' imported from .../node_modules/opkg/packages/cli/dist/chunk-*.js

Reported on Homebrew/macOS and linuxbrew (Node v25). Workaround was a manual npm i -g picocolors.

Root cause

The CLI bundle imports picocolors and @clack/core directly — they power the custom interactive file selector (packages/cli/src/utils/file-selector-with-header.ts: @clack/core's AutocompletePrompt base class + picocolors for coloring). esbuild builds with packages: 'external', so every npm import stays external and must be present at runtime.

Neither was declared in the published root package.json. npm only reads package.json, never the bundled JS, so it never installed them on opkg's behalf — they only appeared as hoisted transitive deps of @clack/prompts. Whether opkg's own chunk could resolve them came down to hoisting luck:

  • Hoisted to top-level node_modules/picocolors → resolves → works (most npm installs + dev, which is why it went unnoticed).
  • Strict / non-hoisting layout (pnpm, or npm under a version conflict) → buried at @clack/prompts/node_modules/picocolors, off opkg's resolution path → crash.

This is a classic phantom-dependency bug, and it had drifted twice (picocolors and @clack/core).

Fix

Declare both in the root dependencies:

+    "@clack/core": "^1.0.1",
     "@clack/prompts": "^1.0.1",
+    "picocolors": "^1.1.0",

@clack/core is pinned to ^1.0.1 to dedupe with @clack/prompts (which pins it exactly 1.0.1), avoiding a second copy of the AutocompletePrompt base class we subclass.

Verification

Repro packs the actual build and installs it with npm install --install-strategy=nested (the canonical way to expose a phantom dep — no hoisting to mask it):

Also: npm run build clean, tsc type-check clean, 123/123 tests pass.

Follow-up (not in this PR)

A build-time guard that diffs the built bundle's external imports against the declared root deps (fail on under-declaration) would catch this class of bug in CI — recommended, since dev hoisting always masks it.

🤖 Generated with Claude Code

The CLI bundle imports picocolors and @clack/core directly (the custom
interactive file selector in file-selector-with-header.ts), but neither
was declared in the published root package.json. esbuild keeps all npm
packages external (packages: 'external'), so both must exist at runtime.

They only resolved by accident, as hoisted transitive deps of
@clack/prompts. In strict / non-hoisting installs (pnpm, or npm under a
version conflict) they land nested under @clack/prompts and become
unreachable from opkg's own chunk:

  Cannot find package 'picocolors' imported from .../packages/cli/dist/chunk-*.js

Declaring both as direct dependencies puts them on opkg's resolution path
regardless of hoisting. @clack/core is pinned to ^1.0.1 to dedupe with
@clack/prompts (which pins it exactly), avoiding a second copy of the
AutocompletePrompt base class we subclass.

Verified with a non-hoisting repro (npm --install-strategy=nested): the
exact #62 error before, clean after. Type-check clean, 123/123 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Error with "picocolors" when adding a Claude project

1 participant