Skip to content

Commit c437bf7

Browse files
authored
Merge pull request #21820 from mvanhorn/osc/feat-bundle-npm-extension
bundle: add npm (Node.js) extension
2 parents 01e4036 + b3f03e9 commit c437bf7

File tree

7 files changed

+241
-11
lines changed

7 files changed

+241
-11
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "bundle/extensions/extension"
5+
6+
module Homebrew
7+
module Bundle
8+
class Npm < Extension
9+
PACKAGE_TYPE = :npm
10+
PACKAGE_TYPE_NAME = "npm Package"
11+
BANNER_NAME = "npm packages"
12+
13+
class << self
14+
sig { override.void }
15+
def reset!
16+
@packages = T.let(nil, T.nilable(T::Array[String]))
17+
@installed_packages = T.let(nil, T.nilable(T::Array[String]))
18+
end
19+
20+
sig { override.returns(String) }
21+
def package_manager_name
22+
"node"
23+
end
24+
25+
sig { override.returns(T.nilable(Pathname)) }
26+
def package_manager_executable
27+
which("npm", ORIGINAL_PATHS)
28+
end
29+
30+
sig { override.returns(T::Array[String]) }
31+
def packages
32+
packages = @packages
33+
return packages if packages
34+
35+
@packages = if (npm = package_manager_executable) &&
36+
(!npm.to_s.start_with?("/") || npm.exist?)
37+
parse_package_list(`#{npm} list -g --depth=0 --json 2>/dev/null`)
38+
end
39+
return [] if @packages.nil?
40+
41+
@packages
42+
end
43+
44+
sig {
45+
override.params(
46+
name: String,
47+
with: T.nilable(T::Array[String]),
48+
verbose: T::Boolean,
49+
).returns(T::Boolean)
50+
}
51+
def install_package!(name, with: nil, verbose: false)
52+
_ = with
53+
54+
npm = package_manager_executable
55+
return false if npm.nil?
56+
57+
Bundle.system(npm.to_s, "install", "-g", name, verbose:)
58+
end
59+
60+
sig { override.returns(T::Array[String]) }
61+
def installed_packages
62+
installed_packages = @installed_packages
63+
return installed_packages if installed_packages
64+
65+
@installed_packages = packages.dup
66+
end
67+
68+
sig { params(output: String).returns(T::Array[String]) }
69+
def parse_package_list(output)
70+
return [] if output.blank?
71+
72+
json = JSON.parse(output)
73+
deps = json.fetch("dependencies", {})
74+
deps.keys.reject { |name| name == "npm" }
75+
rescue JSON::ParserError
76+
[]
77+
end
78+
private :parse_package_list
79+
end
80+
end
81+
end
82+
end
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# frozen_string_literal: true
2+
3+
require "bundle"
4+
require "bundle/dsl"
5+
require "bundle/extensions/npm"
6+
7+
RSpec.describe Homebrew::Bundle::Npm do
8+
describe "dumping" do
9+
subject(:dumper) { described_class }
10+
11+
context "when npm is not installed" do
12+
before do
13+
described_class.reset!
14+
allow(described_class).to receive(:package_manager_executable).and_return(nil)
15+
end
16+
17+
it "returns an empty list" do
18+
expect(dumper.packages).to be_empty
19+
end
20+
21+
it "dumps an empty string" do # rubocop:todo RSpec/AggregateExamples
22+
expect(dumper.dump).to eql("")
23+
end
24+
end
25+
26+
context "when npm is installed" do
27+
before do
28+
described_class.reset!
29+
allow(described_class).to receive(:package_manager_executable).and_return(Pathname.new("npm"))
30+
end
31+
32+
it "returns package list" do
33+
allow(described_class).to receive(:`).with("npm list -g --depth=0 --json 2>/dev/null").and_return(<<~JSON)
34+
{
35+
"dependencies": {
36+
"npm": { "version": "11.11.0" },
37+
"vercel": { "version": "39.0.0" },
38+
"typescript": { "version": "5.7.3" }
39+
}
40+
}
41+
JSON
42+
43+
expect(dumper.packages).to eql(%w[vercel typescript])
44+
end
45+
46+
it "excludes npm itself from the package list" do
47+
allow(described_class).to receive(:`).with("npm list -g --depth=0 --json 2>/dev/null").and_return(<<~JSON)
48+
{
49+
"dependencies": {
50+
"npm": { "version": "11.11.0" }
51+
}
52+
}
53+
JSON
54+
55+
expect(dumper.packages).to be_empty
56+
end
57+
58+
it "handles invalid JSON" do
59+
allow(described_class).to receive(:`).with("npm list -g --depth=0 --json 2>/dev/null").and_return("not json")
60+
61+
expect(dumper.packages).to be_empty
62+
end
63+
64+
it "handles empty output" do
65+
allow(described_class).to receive(:`).with("npm list -g --depth=0 --json 2>/dev/null").and_return("")
66+
67+
expect(dumper.packages).to be_empty
68+
end
69+
70+
it "dumps package list" do
71+
allow(dumper).to receive(:packages).and_return(["vercel", "typescript"])
72+
expect(dumper.dump).to eql("npm \"vercel\"\nnpm \"typescript\"")
73+
end
74+
end
75+
end
76+
77+
describe "installing" do
78+
context "when npm is not installed" do
79+
before do
80+
described_class.reset!
81+
allow(described_class).to receive(:package_manager_executable).and_return(nil)
82+
end
83+
84+
it "tries to install node" do
85+
expect(Homebrew::Bundle).to \
86+
receive(:system).with(HOMEBREW_BREW_FILE, "install", "--formula", "node", verbose: false)
87+
.and_return(true)
88+
expect { described_class.preinstall!("vercel") }.to raise_error(RuntimeError)
89+
end
90+
end
91+
92+
context "when npm is installed" do
93+
before do
94+
allow(described_class).to receive(:package_manager_executable).and_return(Pathname.new("npm"))
95+
end
96+
97+
context "when package is installed" do
98+
before do
99+
allow(described_class).to receive(:installed_packages)
100+
.and_return(["vercel"])
101+
end
102+
103+
it "skips" do
104+
expect(Homebrew::Bundle).not_to receive(:system)
105+
expect(described_class.preinstall!("vercel")).to be(false)
106+
end
107+
end
108+
109+
context "when package is not installed" do
110+
before do
111+
allow(described_class).to receive_messages(
112+
package_manager_executable: Pathname.new("/opt/homebrew/bin/npm"),
113+
installed_packages: [],
114+
)
115+
end
116+
117+
it "installs package" do
118+
expect(Homebrew::Bundle).to receive(:system)
119+
.with("/opt/homebrew/bin/npm", "install", "-g", "vercel", verbose: false)
120+
.and_return(true)
121+
expect(described_class.preinstall!("vercel")).to be(true)
122+
expect(described_class.install!("vercel")).to be(true)
123+
end
124+
end
125+
end
126+
end
127+
end

completions/bash/brew

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,11 +687,13 @@ _brew_bundle() {
687687
--no-cargo
688688
--no-flatpak
689689
--no-go
690+
--no-npm
690691
--no-restart
691692
--no-secrets
692693
--no-upgrade
693694
--no-uv
694695
--no-vscode
696+
--npm
695697
--quiet
696698
--services
697699
--tap

completions/fish/brew.fish

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ __fish_brew_complete_arg 'bump-unversioned-casks' -a '(__fish_brew_suggest_casks
504504
__fish_brew_complete_arg 'bump-unversioned-casks' -a '(__fish_brew_suggest_taps_installed)'
505505

506506

507-
__fish_brew_complete_cmd 'bundle' 'Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store dependencies, VSCode (and forks/variants) extensions, Go packages, Cargo packages, uv tools and Flatpak packages'
507+
__fish_brew_complete_cmd 'bundle' 'Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store dependencies, VSCode (and forks/variants) extensions, Go packages, Cargo packages, uv tools, Flatpak packages and npm packages'
508508
__fish_brew_complete_sub_cmd 'bundle' 'install'
509509
__fish_brew_complete_sub_cmd 'bundle' 'dump'
510510
__fish_brew_complete_sub_cmd 'bundle' 'cleanup'
@@ -533,11 +533,13 @@ __fish_brew_complete_arg 'bundle' -l mas -d '`list` or `dump` Mac App Store depe
533533
__fish_brew_complete_arg 'bundle' -l no-cargo -d '`dump` without Cargo packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_CARGO` is set'
534534
__fish_brew_complete_arg 'bundle' -l no-flatpak -d '`dump` without Flatpak packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_FLATPAK` is set'
535535
__fish_brew_complete_arg 'bundle' -l no-go -d '`dump` without Go packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_GO` is set'
536+
__fish_brew_complete_arg 'bundle' -l no-npm -d '`dump` without npm packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_NPM` is set'
536537
__fish_brew_complete_arg 'bundle' -l no-restart -d '`dump` does not add `restart_service` to formula lines'
537538
__fish_brew_complete_arg 'bundle' -l no-secrets -d 'Attempt to remove secrets from the environment before `exec`, `sh`, or `env`. Enabled by default if `$HOMEBREW_BUNDLE_NO_SECRETS` is set'
538539
__fish_brew_complete_arg 'bundle' -l no-upgrade -d '`install` does not run `brew upgrade` on outdated dependencies. `check` does not check for outdated dependencies. Note they may still be upgraded by `brew install` if needed. Enabled by default if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set'
539540
__fish_brew_complete_arg 'bundle' -l no-uv -d '`dump` without uv tools. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_UV` is set'
540541
__fish_brew_complete_arg 'bundle' -l no-vscode -d '`dump` without VSCode (and forks/variants) extensions. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_VSCODE` is set'
542+
__fish_brew_complete_arg 'bundle' -l npm -d '`list` or `dump` npm packages'
541543
__fish_brew_complete_arg 'bundle' -l quiet -d 'Make some output more quiet'
542544
__fish_brew_complete_arg 'bundle' -l services -d 'Temporarily start services while running the `exec` or `sh` command. Enabled by default if `$HOMEBREW_BUNDLE_SERVICES` is set'
543545
__fish_brew_complete_arg 'bundle' -l tap -d '`list`, `dump` or `cleanup` Homebrew tap dependencies'

completions/zsh/_brew

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ __brew_internal_commands() {
150150
'bump-formula-pr:Create a pull request to update formula with a new URL or a new tag'
151151
'bump-revision:Create a commit to increment the revision of formula'
152152
'bump-unversioned-casks:Check all casks with unversioned URLs in a given tap for updates'
153-
'bundle:Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store dependencies, VSCode (and forks/variants) extensions, Go packages, Cargo packages, uv tools and Flatpak packages'
153+
'bundle:Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store dependencies, VSCode (and forks/variants) extensions, Go packages, Cargo packages, uv tools, Flatpak packages and npm packages'
154154
'casks:List all locally installable casks including short names'
155155
'cat:Display the source of a formula or cask'
156156
'cleanup:Remove stale lock files and outdated downloads for all formulae and casks, and remove old versions of installed formulae'
@@ -670,7 +670,7 @@ _brew_bump_unversioned_casks() {
670670
# brew bundle
671671
_brew_bundle() {
672672
_arguments \
673-
'(--no-vscode --no-go --no-cargo --no-uv --no-flatpak)--all[`list` all dependencies]' \
673+
'(--no-vscode --no-go --no-cargo --no-uv --no-flatpak --no-npm)--all[`list` all dependencies]' \
674674
'(--no-cargo)--cargo[`list` or `dump` Cargo packages]' \
675675
'--cask[`list`, `dump` or `cleanup` Homebrew cask dependencies]' \
676676
'--check[Check that all dependencies in the Brewfile are installed before running `exec`, `sh`, or `env`. Enabled by default if `$HOMEBREW_BUNDLE_CHECK` is set]' \
@@ -689,11 +689,13 @@ _brew_bundle() {
689689
'(--all --cargo)--no-cargo[`dump` without Cargo packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_CARGO` is set]' \
690690
'(--all --flatpak)--no-flatpak[`dump` without Flatpak packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_FLATPAK` is set]' \
691691
'(--all --go)--no-go[`dump` without Go packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_GO` is set]' \
692+
'(--all --npm)--no-npm[`dump` without npm packages. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_NPM` is set]' \
692693
'--no-restart[`dump` does not add `restart_service` to formula lines]' \
693694
'--no-secrets[Attempt to remove secrets from the environment before `exec`, `sh`, or `env`. Enabled by default if `$HOMEBREW_BUNDLE_NO_SECRETS` is set]' \
694695
'--no-upgrade[`install` does not run `brew upgrade` on outdated dependencies. `check` does not check for outdated dependencies. Note they may still be upgraded by `brew install` if needed. Enabled by default if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set]' \
695696
'(--all --uv)--no-uv[`dump` without uv tools. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_UV` is set]' \
696697
'(--all --vscode)--no-vscode[`dump` without VSCode (and forks/variants) extensions. Enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_VSCODE` is set]' \
698+
'(--no-npm)--npm[`list` or `dump` npm packages]' \
697699
'--quiet[Make some output more quiet]' \
698700
'--services[Temporarily start services while running the `exec` or `sh` command. Enabled by default if `$HOMEBREW_BUNDLE_SERVICES` is set]' \
699701
'--tap[`list`, `dump` or `cleanup` Homebrew tap dependencies]' \

docs/Manpage.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ and are now no longer needed.
138138

139139
Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store
140140
dependencies, VSCode (and forks/variants) extensions, Go packages, Cargo
141-
packages, uv tools and Flatpak packages.
141+
packages, uv tools, Flatpak packages and npm packages.
142142

143143
Note: Flatpak support is only available on Linux.
144144

@@ -193,14 +193,14 @@ By default, only Homebrew formula dependencies are listed.
193193
`brew bundle add` *`name`* \[...\]
194194

195195
: Add entries to your `Brewfile`. Adds formulae by default. Use `--cask`,
196-
`--tap`, `--vscode`, `--go`, `--cargo`, `--uv` and `--flatpak` to add the
197-
corresponding entry instead.
196+
`--tap`, `--vscode`, `--go`, `--cargo`, `--uv`, `--flatpak` and `--npm` to add
197+
the corresponding entry instead.
198198

199199
`brew bundle remove` *`name`* \[...\]
200200

201201
: Remove entries that match `name` from your `Brewfile`. Use `--formula`,
202-
`--cask`, `--tap`, `--mas`, `--vscode`, `--go`, `--cargo`, `--uv` and
203-
`--flatpak` to remove only entries of the corresponding type. Passing
202+
`--cask`, `--tap`, `--mas`, `--vscode`, `--go`, `--cargo`, `--uv`, `--flatpak`
203+
and `--npm` to remove only entries of the corresponding type. Passing
204204
`--formula` also removes matches against formula aliases and old formula
205205
names.
206206

@@ -317,6 +317,10 @@ flags which will help with finding keg-only dependencies like `openssl`,
317317

318318
: `list`, `dump` or `cleanup` Flatpak packages. Note: Linux only.
319319

320+
`--npm`
321+
322+
: `list` or `dump` npm packages.
323+
320324
`--no-vscode`
321325

322326
: `dump` without VSCode (and forks/variants) extensions. Enabled by default if
@@ -342,6 +346,11 @@ flags which will help with finding keg-only dependencies like `openssl`,
342346
: `dump` without Flatpak packages. Enabled by default if
343347
`$HOMEBREW_BUNDLE_DUMP_NO_FLATPAK` is set.
344348

349+
`--no-npm`
350+
351+
: `dump` without npm packages. Enabled by default if
352+
`$HOMEBREW_BUNDLE_DUMP_NO_NPM` is set.
353+
345354
`--describe`
346355

347356
: `dump` and `add` add a description comment above each line, unless the

manpages/brew.1

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Uninstall formulae that were only installed as a dependency of another formula a
8383
\fB\-n\fP, \fB\-\-dry\-run\fP
8484
List what would be uninstalled, but do not actually uninstall anything\.
8585
.SS "\fBbundle\fP \fR[\fIsubcommand\fP]"
86-
Bundler for non\-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store dependencies, VSCode (and forks/variants) extensions, Go packages, Cargo packages, uv tools and Flatpak packages\.
86+
Bundler for non\-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store dependencies, VSCode (and forks/variants) extensions, Go packages, Cargo packages, uv tools, Flatpak packages and npm packages\.
8787
.P
8888
Note: Flatpak support is only available on Linux\.
8989
.TP
@@ -121,10 +121,10 @@ By default, only Homebrew formula dependencies are listed\.
121121
Edit the \fBBrewfile\fP in your editor\.
122122
.TP
123123
\fBbrew bundle add\fP \fIname\fP [\.\.\.]
124-
Add entries to your \fBBrewfile\fP\&\. Adds formulae by default\. Use \fB\-\-cask\fP, \fB\-\-tap\fP, \fB\-\-vscode\fP, \fB\-\-go\fP, \fB\-\-cargo\fP, \fB\-\-uv\fP and \fB\-\-flatpak\fP to add the corresponding entry instead\.
124+
Add entries to your \fBBrewfile\fP\&\. Adds formulae by default\. Use \fB\-\-cask\fP, \fB\-\-tap\fP, \fB\-\-vscode\fP, \fB\-\-go\fP, \fB\-\-cargo\fP, \fB\-\-uv\fP, \fB\-\-flatpak\fP and \fB\-\-npm\fP to add the corresponding entry instead\.
125125
.TP
126126
\fBbrew bundle remove\fP \fIname\fP [\.\.\.]
127-
Remove entries that match \fBname\fP from your \fBBrewfile\fP\&\. Use \fB\-\-formula\fP, \fB\-\-cask\fP, \fB\-\-tap\fP, \fB\-\-mas\fP, \fB\-\-vscode\fP, \fB\-\-go\fP, \fB\-\-cargo\fP, \fB\-\-uv\fP and \fB\-\-flatpak\fP to remove only entries of the corresponding type\. Passing \fB\-\-formula\fP also removes matches against formula aliases and old formula names\.
127+
Remove entries that match \fBname\fP from your \fBBrewfile\fP\&\. Use \fB\-\-formula\fP, \fB\-\-cask\fP, \fB\-\-tap\fP, \fB\-\-mas\fP, \fB\-\-vscode\fP, \fB\-\-go\fP, \fB\-\-cargo\fP, \fB\-\-uv\fP, \fB\-\-flatpak\fP and \fB\-\-npm\fP to remove only entries of the corresponding type\. Passing \fB\-\-formula\fP also removes matches against formula aliases and old formula names\.
128128
.TP
129129
\fBbrew bundle exec\fP [\fB\-\-check\fP] [\fB\-\-no\-secrets\fP] \fIcommand\fP
130130
Run an external command in an isolated build environment based on the \fBBrewfile\fP dependencies\.
@@ -197,6 +197,9 @@ Temporarily start services while running the \fBexec\fP or \fBsh\fP command\. En
197197
\fB\-\-flatpak\fP
198198
\fBlist\fP, \fBdump\fP or \fBcleanup\fP Flatpak packages\. Note: Linux only\.
199199
.TP
200+
\fB\-\-npm\fP
201+
\fBlist\fP or \fBdump\fP npm packages\.
202+
.TP
200203
\fB\-\-no\-vscode\fP
201204
\fBdump\fP without VSCode (and forks/variants) extensions\. Enabled by default if \fB$HOMEBREW_BUNDLE_DUMP_NO_VSCODE\fP is set\.
202205
.TP
@@ -212,6 +215,9 @@ Temporarily start services while running the \fBexec\fP or \fBsh\fP command\. En
212215
\fB\-\-no\-flatpak\fP
213216
\fBdump\fP without Flatpak packages\. Enabled by default if \fB$HOMEBREW_BUNDLE_DUMP_NO_FLATPAK\fP is set\.
214217
.TP
218+
\fB\-\-no\-npm\fP
219+
\fBdump\fP without npm packages\. Enabled by default if \fB$HOMEBREW_BUNDLE_DUMP_NO_NPM\fP is set\.
220+
.TP
215221
\fB\-\-describe\fP
216222
\fBdump\fP and \fBadd\fP add a description comment above each line, unless the dependency does not have a description\. Enabled by default if \fB$HOMEBREW_BUNDLE_DESCRIBE\fP is set\.
217223
.TP

0 commit comments

Comments
 (0)