Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions lua/mason-core/installer/compiler/compilers/npm.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local Result = require "mason-core.result"
local _ = require "mason-core.functional"
local providers = require "mason-core.providers"
local settings = require "mason.settings"

---@param purl Purl
local function purl_to_npm(purl)
Expand Down Expand Up @@ -33,11 +34,11 @@ end
---@param ctx InstallContext
---@param source ParsedNpmSource
function M.install(ctx, source)
local npm = require "mason-core.installer.managers.npm"

local manager = settings.current.npm.use_pnpm and require "mason-core.installer.managers.pnpm"
or require "mason-core.installer.managers.npm"
return Result.try(function(try)
try(npm.init())
try(npm.install(source.package, source.version, {
try(manager.init())
try(manager.install(source.package, source.version, {
extra_packages = source.extra_packages,
}))
end)
Expand Down
17 changes: 15 additions & 2 deletions lua/mason-core/installer/compiler/link.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ local fs = require "mason-core.fs"
local log = require "mason-core.log"
local path = require "mason-core.path"
local platform = require "mason-core.platform"
local settings = require "mason.settings"

local M = {}

Expand Down Expand Up @@ -112,8 +113,20 @@ local bin_delegates = {
["nuget"] = function(target)
return require("mason-core.installer.managers.nuget").bin_path(target)
end,
["npm"] = function(target)
return require("mason-core.installer.managers.npm").bin_path(target)
["npm"] = function(target, bin)
local manager = settings.current.npm.use_pnpm and require "mason-core.installer.managers.pnpm"
or require "mason-core.installer.managers.npm"
local bin_path_result = manager.bin_path(target)
if not settings.current.npm.use_pnpm then
return bin_path_result
end
return bin_path_result:and_then(function(bin_path)
local installer = require "mason-core.installer"
local ctx = installer.context()
return Result.pcall(function()
return ctx:write_exec_wrapper(bin, bin_path)
end)
end)
end,
["gem"] = function(target)
return require("mason-core.installer.managers.gem").create_bin_wrapper(target)
Expand Down
51 changes: 51 additions & 0 deletions lua/mason-core/installer/managers/pnpm.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
local Result = require "mason-core.result"
local _ = require "mason-core.functional"
local installer = require "mason-core.installer"
local log = require "mason-core.log"
local path = require "mason-core.path"
local platform = require "mason-core.platform"

local M = {}

---@async
function M.init()
log.debug "pnpm: init"
local ctx = installer.context()
return Result.try(function(try)
try(ctx.spawn.pnpm { "init" })
local package_json = try(Result.pcall(vim.json.decode, ctx.fs:read_file "package.json"))
package_json.name = "@mason/" .. package_json.name
ctx.fs:write_file("package.json", try(Result.pcall(vim.json.encode, package_json)))
ctx.stdio_sink:stdout "Initialized pnpm root.\n"
end)
end

---@async
---@param pkg string
---@param version string
---@param opts? { extra_packages?: string[] }
function M.install(pkg, version, opts)
opts = opts or {}
log.fmt_debug("pnpm: add %s %s %s", pkg, version, opts)
local ctx = installer.context()
ctx.stdio_sink:stdout(("Installing npm package %s@%s…\n"):format(pkg, version))
return ctx.spawn.pnpm {
"add",
("%s@%s"):format(pkg, version),
opts.extra_packages or vim.NIL,
}
end

---@param exec string
function M.bin_path(exec)
return Result.pcall(platform.when, {
unix = function()
return path.concat { "node_modules", ".bin", exec }
end,
win = function()
return path.concat { "node_modules", ".bin", ("%s.cmd"):format(exec) }
end,
})
end

return M
7 changes: 5 additions & 2 deletions lua/mason/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,14 @@ local function check_languages()
check_thunk { cmd = "composer", args = { "--version" }, name = "Composer", relaxed = true },
check_thunk { cmd = "php", args = { "--version" }, name = "PHP", relaxed = true },
check_thunk {
cmd = "npm",
cmd = settings.current.npm.use_pnpm and "pnpm" or "npm",
args = { "--version" },
name = "npm",
name = settings.current.npm.use_pnpm and "pnpm" or "npm",
relaxed = true,
version_check = function(version)
if settings.current.npm.use_pnpm then
return
end
-- Parses output such as "8.1.2" into major, minor, patch components
local _, _, major = version:find "(%d+)%.(%d+)%.(%d+)"
-- Based off of general observations of feature parity.
Expand Down
6 changes: 6 additions & 0 deletions lua/mason/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ local DEFAULT_SETTINGS = {
download_url_template = "https://github.com/%s/releases/download/%s/%s",
},

npm = {
---@since 2.0.0
-- Use `pnpm` instead of `npm` to install node packages
use_pnpm = false,
},

pip = {
---@since 1.0.0
-- Whether to upgrade pip to the latest version in the virtual environment before installing packages.
Expand Down
22 changes: 22 additions & 0 deletions tests/mason-core/installer/compiler/compilers/npm_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,26 @@ describe("npm compiler :: installing", function()
assert.spy(manager.install).was_called(1)
assert.spy(manager.install).was_called_with("@namespace/package", "v1.5.0", { extra_packages = { "extra" } })
end)

it("should install npm packages when use_pnpm is set to true", function()
local ctx = create_dummy_context()
local manager = require "mason-core.installer.managers.pnpm"
local settings = require "mason.settings"
settings.current.npm.use_pnpm = true
stub(manager, "init", mockx.returns(Result.success()))
stub(manager, "install", mockx.returns(Result.success()))

local result = installer.exec_in_context(ctx, function()
return npm.install(ctx, {
package = "@namespace/package",
version = "v1.5.0",
extra_packages = { "extra" },
})
end)

assert.is_true(result:is_success())
assert.spy(manager.init).was_called(1)
assert.spy(manager.install).was_called(1)
assert.spy(manager.install).was_called_with("@namespace/package", "v1.5.0", { extra_packages = { "extra" } })
end)
end)
46 changes: 46 additions & 0 deletions tests/mason-core/installer/managers/pnpm_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
local Result = require "mason-core.result"
local installer = require "mason-core.installer"
local match = require "luassert.match"
local pnpm = require "mason-core.installer.managers.pnpm"
local spawn = require "mason-core.spawn"
local spy = require "luassert.spy"
local stub = require "luassert.stub"

describe("pnpm manager", function()
it("should init package.json", function()
local ctx = create_dummy_context()
stub(ctx.fs, "read_file")
stub(ctx.fs, "write_file")
stub(spawn, "pnpm")
ctx.fs.read_file.returns '{"name": "my-package", "version": "1.0.0"}'
spawn.pnpm.returns(Result.success {})
installer.exec_in_context(ctx, function()
pnpm.init()
end)

assert.spy(ctx.spawn.pnpm).was_called(1)
assert.spy(ctx.spawn.pnpm).was_called_with { "init" }
assert.spy(ctx.fs.read_file).was_called(1)
assert.spy(ctx.fs.read_file).was_called_with(match.is_ref(ctx.fs), "package.json")
assert.spy(ctx.fs.write_file).was_called(1)
assert
.spy(ctx.fs.write_file)
.was_called_with(match.is_ref(ctx.fs), "package.json", match.has_match '"name":"@mason/my--package"')
end)

it("should install extra packages", function()
local ctx = create_dummy_context()
installer.exec_in_context(ctx, function()
pnpm.install("my-package", "1.0.0", {
extra_packages = { "extra-package" },
})
end)

assert.spy(ctx.spawn.pnpm).was_called(1)
assert.spy(ctx.spawn.pnpm).was_called_with {
"add",
"[email protected]",
{ "extra-package" },
}
end)
end)