Skip to content

refactor: move package managers into lib/package-managers - #5379

Draft
rigor789 wants to merge 6 commits into
feat/define-command-migrationfrom
refactor/package-manager
Draft

rigor789 wants to merge 6 commits into
feat/define-command-migrationfrom
refactor/package-manager

Conversation

@rigor789

@rigor789 rigor789 commented Sep 7, 2020

Copy link
Copy Markdown
Member

Steps towards fully abstracting package management in the CLI. The end goal is that the CLI stops caring what node_modules or package.json files are (to an extent) and talks to a package-manager-agnostic API, with each package manager mapping that API onto its own flags and terminology. A related goal is full pnpm support without --shamefully-hoist.

Stacked on #6153 (feat/define-command-migration) since that changes the command and $options setup.

Commit 1: move into lib/package-managers/

The dispatcher, the npm / yarn / yarn2 / pnpm / bun implementations, the shared base class and PackageInstallationManager now live in lib/package-managers/. DI bootstrap and tests updated. No behaviour changes.

Commit 2: typed install/uninstall options

install and uninstall take IPackageInstallOptions / IPackageUninstallOptions instead of an untyped bag of npm flags:

option npm yarn 1 yarn berry pnpm bun
save: true --save default default default --save
save: false --no-save dropped dropped dropped --no-save
dev --save-dev --dev --dev --save-dev --dev
optional --save-optional --optional --optional --save-optional --optional
exact --save-exact --exact --exact --save-exact --exact
silent --silent --silent --silent --silent --silent
ignoreScripts --ignore-scripts --ignore-scripts --mode=skip-build --ignore-scripts --ignore-scripts

Each manager declares its spelling in a table on the class; the base class does the mapping and drops anything a manager has no flag for. This fixes yarn berry silently dropping --save-dev / --save-exact (platforms ended up in dependencies) and rejecting --ignore-scripts, and bun being handed npm's spellings. test/package-manager-flags.ts pins the table.

Classes are NpmPackageManager, YarnPackageManager, Yarn2PackageManager, PnpmPackageManager, BunPackageManager.

Commit 3: manager-agnostic view/search

view(packageName, field?) takes an optional registry field (versions, dist-tags, ...) instead of { "dist-tags": true }; search(keywords) takes only keywords. yarn berry spells the field as --fields. The generic npm flag-string builder is gone, so the package manager layer no longer emits npm syntax on behalf of another manager anywhere.

Commit 4: getInstalledPackagePath on the contract

"Where is package X installed?" is now a question services ask the package manager. The base implementation walks node_modules the way Node does; a manager with a different layout (yarn Plug'n'Play, for instance) can override it, and tests stub one method instead of faking a filesystem.

Migrated: plugins-service, doctor's short-import scan, versions-service, prepare-controller's runtime lookup, android-plugin-build-service's local gradle versions, the preview command, test-init, and PackageInstallationManager.

Commit 5: bundler, test runners and extensions on the contract

The bundler executable lookup, the vitest and karma readiness checks, and the extensibility service's "is this extension installed?" check go through getInstalledPackagePath. Their tests stub that one method instead of faking directory listings or monkey-patching Node's module resolution.

Commit 6: synchronous selection and resolution

Nothing about locating an installed package is asynchronous; the only async link was the dispatcher reading the packageManager user setting through the settings lock. JsonFileSettingsService gains a lock-free getSettingValueSync, the dispatcher selects its implementation lazily and synchronously (no more @cache/@invokeInit), and getInstalledPackagePath is a plain synchronous method on the contract. That unwinds the async earlier commits had threaded through services, and lets the last two direct users of the resolution helper move onto the contract: getRuntimePackage in project-data-service and the transitive walk in node-modules-dependencies-builder. No service resolves packages on its own any more.

Still hardcoding node_modules, on purpose

  • Cleanup and ignore lists (clean, migrate, update, log source maps, watch app resources) genuinely target the directory.
  • The extensions dir and inspector cache compute the path of a package they installed themselves with a node_modules join after the contract confirms it is installed. Every supported manager places direct installs at the top level of node_modules, so this holds even for pnpm's isolated layout.
  • --shamefully-hoist stays for now: pnpm's isolated layout only breaks transitive resolution, which is driven by the bundler and runtime inside the project rather than by the CLI's own lookups. Dropping the flag needs verification against a real pnpm project.

🤖 Generated with Claude Code

@cla-bot cla-bot Bot added the cla: yes label Sep 7, 2020
@rigor789 rigor789 added this to the 8.3 milestone Jul 9, 2022
@rigor789 rigor789 modified the milestones: 8.3, 8.4 Jul 25, 2022
@NathanWalker NathanWalker modified the milestones: 8.4, 9.0 Oct 23, 2023
@rigor789 rigor789 changed the title refactor: move package managers into a dir refactor: move package managers into lib/package-managers Sep 16, 2026
@rigor789
rigor789 force-pushed the refactor/package-manager branch from 26c4b45 to 2dd6df5 Compare September 16, 2026 09:13
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Group the package manager dispatcher, the per-manager implementations
(npm, yarn, yarn2, pnpm, bun), their shared base class and the
installation manager under a single directory, and shorten the
implementation class names to match their file names.
Replace the untyped npm flag bag passed to install/uninstall with
IPackageInstallOptions and IPackageUninstallOptions (save, dev, optional,
exact, silent, ignoreScripts plus the CLI-internal options). Each package
manager declares how it spells each option, and options a manager has no
flag for are dropped instead of leaking npm syntax onto its command line.

This fixes yarn berry receiving --save-dev / --save-exact (silently
dropped, so platforms landed in dependencies) and --ignore-scripts (an
unknown option that aborted the install); it now gets --dev, --exact and
--mode=skip-build. bun receives its own --dev / --exact instead of npm's.

Also settle the implementation class names on NpmPackageManager,
YarnPackageManager, Yarn2PackageManager, PnpmPackageManager and
BunPackageManager.
…ookups

view() now takes an optional registry field instead of an npm flag bag, and
search() takes only the keywords; each package manager spells the field
selection itself (yarn berry uses --fields). The generic npm flag-string
builder is gone with them, so nothing in the package manager layer emits
npm syntax on behalf of another manager any more.

Project-facing package lookups go through Node resolution from the project
directory instead of joining node_modules/<name> by hand: test-init peer
dependency discovery, the installed path returned by
PackageInstallationManager, the local inspector check, the core modules
short-import scan in doctor, and the installed core modules versions in
versions-service. plugins-service no longer creates an empty node_modules
directory before enumerating dependencies.
@rigor789
rigor789 force-pushed the refactor/package-manager branch from 4904ab7 to f3f4e1b Compare September 16, 2026 09:40
@rigor789
rigor789 changed the base branch from main to feat/define-command-migration September 16, 2026 09:40
…e manager

Add getInstalledPackagePath(packageName, fromDir) to the package manager
contract. The base implementation walks node_modules the way Node does;
a package manager with a different on-disk layout can override it.

Services whose call chains are already async now ask the package manager
where a package lives instead of resolving it themselves: plugins-service,
doctor short-import scan, versions-service, prepare-controller's runtime
package.json lookup, android-plugin-build-service's local gradle versions,
the preview command, test-init and PackageInstallationManager.

Sites reached only from synchronous code (getRuntimePackage in
project-data-service, the bundler executable lookup, the vitest and karma
readiness checks, the transitive walk in node-modules-dependencies-builder)
keep using the resolution helper directly.
… package manager

The bundler executable lookup, the vitest and karma readiness checks and
the extensibility service's "is this extension installed?" check now ask
the package manager where a package lives. Their tests stub that one
method instead of faking directory listings or patching Node's module
resolution.

getRuntimePackage in project-data-service and the transitive walk in
node-modules-dependencies-builder stay on the resolution helper: both feed
synchronous code paths (getPlatformData, getAllProductionPlugins) with
dozens of callers, and threading async through those is a separate change.
…ckages synchronously

Nothing about locating an installed package is asynchronous; the only
async link was the dispatcher reading the "packageManager" user setting
through the settings lock. JsonFileSettingsService gains a lock-free
getSettingValueSync for settings that only change through explicit user
commands, and the dispatcher now picks its implementation lazily and
synchronously, dropping the @cache/@invokeInit init dance.

getInstalledPackagePath is therefore synchronous on the contract, which
unwinds the async that had been threaded through doctor, plugins-service,
the bundler, the test runners, preview and android-plugin-build-service,
and lets the last two direct users of the resolution helper move onto the
contract: getRuntimePackage in project-data-service (resolved lazily via
the injector, as the service is constructed everywhere) and the transitive
walk in node-modules-dependencies-builder.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants