diff --git a/lua/mason-core/installer/compiler/compilers/npm.lua b/lua/mason-core/installer/compiler/compilers/npm.lua index e8489fe8d..c442295de 100644 --- a/lua/mason-core/installer/compiler/compilers/npm.lua +++ b/lua/mason-core/installer/compiler/compilers/npm.lua @@ -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) @@ -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) diff --git a/lua/mason-core/installer/compiler/link.lua b/lua/mason-core/installer/compiler/link.lua index d60fce478..92f7a6aa8 100644 --- a/lua/mason-core/installer/compiler/link.lua +++ b/lua/mason-core/installer/compiler/link.lua @@ -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 = {} @@ -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) diff --git a/lua/mason-core/installer/managers/pnpm.lua b/lua/mason-core/installer/managers/pnpm.lua new file mode 100644 index 000000000..a6cc0bc91 --- /dev/null +++ b/lua/mason-core/installer/managers/pnpm.lua @@ -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 diff --git a/lua/mason/health.lua b/lua/mason/health.lua index b105940d0..63b19b867 100644 --- a/lua/mason/health.lua +++ b/lua/mason/health.lua @@ -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. diff --git a/lua/mason/settings.lua b/lua/mason/settings.lua index ebff1e0b4..1060108c6 100644 --- a/lua/mason/settings.lua +++ b/lua/mason/settings.lua @@ -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. diff --git a/tests/mason-core/installer/compiler/compilers/npm_spec.lua b/tests/mason-core/installer/compiler/compilers/npm_spec.lua index 94d678019..38ebe9e2f 100644 --- a/tests/mason-core/installer/compiler/compilers/npm_spec.lua +++ b/tests/mason-core/installer/compiler/compilers/npm_spec.lua @@ -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) diff --git a/tests/mason-core/installer/managers/pnpm_spec.lua b/tests/mason-core/installer/managers/pnpm_spec.lua new file mode 100644 index 000000000..2929d9329 --- /dev/null +++ b/tests/mason-core/installer/managers/pnpm_spec.lua @@ -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", + "my-package@1.0.0", + { "extra-package" }, + } + end) +end)