Skip to content

Commit bf22396

Browse files
committed
feat(npm): add pnpm support for installing node packages
1 parent 8024d64 commit bf22396

File tree

7 files changed

+148
-8
lines changed

7 files changed

+148
-8
lines changed

lua/mason-core/installer/compiler/compilers/npm.lua

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local Result = require "mason-core.result"
22
local _ = require "mason-core.functional"
33
local providers = require "mason-core.providers"
4+
local settings = require "mason.settings"
45

56
---@param purl Purl
67
local function purl_to_npm(purl)
@@ -33,11 +34,11 @@ end
3334
---@param ctx InstallContext
3435
---@param source ParsedNpmSource
3536
function M.install(ctx, source)
36-
local npm = require "mason-core.installer.managers.npm"
37-
37+
local manager = settings.current.npm.use_pnpm and require "mason-core.installer.managers.pnpm"
38+
or require "mason-core.installer.managers.npm"
3839
return Result.try(function(try)
39-
try(npm.init())
40-
try(npm.install(source.package, source.version, {
40+
try(manager.init())
41+
try(manager.install(source.package, source.version, {
4142
extra_packages = source.extra_packages,
4243
}))
4344
end)

lua/mason-core/installer/compiler/link.lua

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local fs = require "mason-core.fs"
77
local log = require "mason-core.log"
88
local path = require "mason-core.path"
99
local platform = require "mason-core.platform"
10+
local settings = require "mason.settings"
1011

1112
local M = {}
1213

@@ -112,8 +113,18 @@ local bin_delegates = {
112113
["nuget"] = function(target)
113114
return require("mason-core.installer.managers.nuget").bin_path(target)
114115
end,
115-
["npm"] = function(target)
116-
return require("mason-core.installer.managers.npm").bin_path(target)
116+
["npm"] = function(target, bin)
117+
if not settings.current.npm.use_pnpm then
118+
return require("mason-core.installer.managers.npm").bin_path(target)
119+
end
120+
local installer = require "mason-core.installer"
121+
local ctx = installer.context()
122+
return Result.pcall(function()
123+
return ctx:write_exec_wrapper(
124+
bin,
125+
path.concat { "node_modules", ".bin", bin }
126+
)
127+
end)
117128
end,
118129
["gem"] = function(target)
119130
return require("mason-core.installer.managers.gem").create_bin_wrapper(target)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
local Result = require "mason-core.result"
2+
local _ = require "mason-core.functional"
3+
local installer = require "mason-core.installer"
4+
local log = require "mason-core.log"
5+
local path = require "mason-core.path"
6+
local platform = require "mason-core.platform"
7+
8+
local M = {}
9+
10+
---@async
11+
function M.init()
12+
log.debug "pnpm: init"
13+
local ctx = installer.context()
14+
return Result.try(function(try)
15+
try(ctx.spawn.pnpm { "init" })
16+
local package_json = try(Result.pcall(vim.json.decode, ctx.fs:read_file "package.json"))
17+
package_json.name = "@mason/" .. package_json.name
18+
ctx.fs:write_file("package.json", try(Result.pcall(vim.json.encode, package_json)))
19+
ctx.stdio_sink:stdout "Initialized pnpm root.\n"
20+
end)
21+
end
22+
23+
---@async
24+
---@param pkg string
25+
---@param version string
26+
---@param opts? { extra_packages?: string[] }
27+
function M.install(pkg, version, opts)
28+
opts = opts or {}
29+
log.fmt_debug("pnpm: add %s %s %s", pkg, version, opts)
30+
local ctx = installer.context()
31+
ctx.stdio_sink:stdout(("Installing npm package %s@%s…\n"):format(pkg, version))
32+
return ctx.spawn.pnpm {
33+
"add",
34+
("%s@%s"):format(pkg, version),
35+
opts.extra_packages or vim.NIL,
36+
}
37+
end
38+
39+
---@param exec string
40+
function M.bin_path(exec)
41+
return Result.pcall(platform.when, {
42+
unix = function()
43+
return path.concat { "node_modules", ".bin", exec }
44+
end,
45+
win = function()
46+
return path.concat { "node_modules", ".bin", ("%s.cmd"):format(exec) }
47+
end,
48+
})
49+
end
50+
51+
return M

lua/mason/health.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,14 @@ local function check_languages()
182182
check_thunk { cmd = "composer", args = { "--version" }, name = "Composer", relaxed = true },
183183
check_thunk { cmd = "php", args = { "--version" }, name = "PHP", relaxed = true },
184184
check_thunk {
185-
cmd = "npm",
185+
cmd = settings.current.npm.use_pnpm and "pnpm" or "npm",
186186
args = { "--version" },
187-
name = "npm",
187+
name = settings.current.npm.use_pnpm and "pnpm" or "npm",
188188
relaxed = true,
189189
version_check = function(version)
190+
if settings.current.npm.use_pnpm then
191+
return
192+
end
190193
-- Parses output such as "8.1.2" into major, minor, patch components
191194
local _, _, major = version:find "(%d+)%.(%d+)%.(%d+)"
192195
-- Based off of general observations of feature parity.

lua/mason/settings.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ local DEFAULT_SETTINGS = {
5555
download_url_template = "https://github.com/%s/releases/download/%s/%s",
5656
},
5757

58+
npm = {
59+
---@since 2.0.0
60+
-- Use `pnpm` instead of `npm` to install node packages
61+
use_pnpm = false,
62+
},
63+
5864
pip = {
5965
---@since 1.0.0
6066
-- Whether to upgrade pip to the latest version in the virtual environment before installing packages.

tests/mason-core/installer/compiler/compilers/npm_spec.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,26 @@ describe("npm compiler :: installing", function()
5656
assert.spy(manager.install).was_called(1)
5757
assert.spy(manager.install).was_called_with("@namespace/package", "v1.5.0", { extra_packages = { "extra" } })
5858
end)
59+
60+
it("should install npm packages when use_pnpm is set to true", function()
61+
local ctx = create_dummy_context()
62+
local manager = require "mason-core.installer.managers.pnpm"
63+
local settings = require "mason.settings"
64+
settings.current.npm.use_pnpm = true
65+
stub(manager, "init", mockx.returns(Result.success()))
66+
stub(manager, "install", mockx.returns(Result.success()))
67+
68+
local result = installer.exec_in_context(ctx, function()
69+
return npm.install(ctx, {
70+
package = "@namespace/package",
71+
version = "v1.5.0",
72+
extra_packages = { "extra" },
73+
})
74+
end)
75+
76+
assert.is_true(result:is_success())
77+
assert.spy(manager.init).was_called(1)
78+
assert.spy(manager.install).was_called(1)
79+
assert.spy(manager.install).was_called_with("@namespace/package", "v1.5.0", { extra_packages = { "extra" } })
80+
end)
5981
end)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
local Result = require "mason-core.result"
2+
local installer = require "mason-core.installer"
3+
local match = require "luassert.match"
4+
local pnpm = require "mason-core.installer.managers.pnpm"
5+
local spawn = require "mason-core.spawn"
6+
local spy = require "luassert.spy"
7+
local stub = require "luassert.stub"
8+
9+
describe("pnpm manager", function()
10+
it("should init package.json", function()
11+
local ctx = create_dummy_context()
12+
stub(ctx.fs, "read_file")
13+
stub(ctx.fs, "write_file")
14+
stub(spawn, "pnpm")
15+
ctx.fs.read_file.returns '{"name": "my-package", "version": "1.0.0"}'
16+
spawn.pnpm.returns(Result.success {})
17+
installer.exec_in_context(ctx, function()
18+
pnpm.init()
19+
end)
20+
21+
assert.spy(ctx.spawn.pnpm).was_called(1)
22+
assert.spy(ctx.spawn.pnpm).was_called_with { "init" }
23+
assert.spy(ctx.fs.read_file).was_called(1)
24+
assert.spy(ctx.fs.read_file).was_called_with(match.is_ref(ctx.fs), "package.json")
25+
assert.spy(ctx.fs.write_file).was_called(1)
26+
assert
27+
.spy(ctx.fs.write_file)
28+
.was_called_with(match.is_ref(ctx.fs), "package.json", match.has_match '"name":"@mason/my--package"')
29+
end)
30+
31+
it("should install extra packages", function()
32+
local ctx = create_dummy_context()
33+
installer.exec_in_context(ctx, function()
34+
pnpm.install("my-package", "1.0.0", {
35+
extra_packages = { "extra-package" },
36+
})
37+
end)
38+
39+
assert.spy(ctx.spawn.pnpm).was_called(1)
40+
assert.spy(ctx.spawn.pnpm).was_called_with {
41+
"add",
42+
43+
{ "extra-package" },
44+
}
45+
end)
46+
end)

0 commit comments

Comments
 (0)