summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/modules/pnpm.py
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/community/general/plugins/modules/pnpm.py')
-rw-r--r--ansible_collections/community/general/plugins/modules/pnpm.py462
1 files changed, 462 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/modules/pnpm.py b/ansible_collections/community/general/plugins/modules/pnpm.py
new file mode 100644
index 000000000..315b07ba8
--- /dev/null
+++ b/ansible_collections/community/general/plugins/modules/pnpm.py
@@ -0,0 +1,462 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023 Aritra Sen <aretrosen@proton.me>
+# Copyright (c) 2017 Chris Hoffman <christopher.hoffman@gmail.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: pnpm
+short_description: Manage node.js packages with pnpm
+version_added: 7.4.0
+description:
+ - Manage node.js packages with the L(pnpm package manager, https://pnpm.io/).
+author:
+ - "Aritra Sen (@aretrosen)"
+ - "Chris Hoffman (@chrishoffman), creator of NPM Ansible module"
+extends_documentation_fragment:
+ - community.general.attributes
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+options:
+ name:
+ description:
+ - The name of a node.js library to install.
+ - All packages in package.json are installed if not provided.
+ type: str
+ required: false
+ alias:
+ description:
+ - Alias of the node.js library.
+ type: str
+ required: false
+ path:
+ description:
+ - The base path to install the node.js libraries.
+ type: path
+ required: false
+ version:
+ description:
+ - The version of the library to be installed, in semver format.
+ type: str
+ required: false
+ global:
+ description:
+ - Install the node.js library globally.
+ required: false
+ default: false
+ type: bool
+ executable:
+ description:
+ - The executable location for pnpm.
+ - The default location it searches for is E(PATH), fails if not set.
+ type: path
+ required: false
+ ignore_scripts:
+ description:
+ - Use the C(--ignore-scripts) flag when installing.
+ required: false
+ type: bool
+ default: false
+ no_optional:
+ description:
+ - Do not install optional packages, equivalent to C(--no-optional).
+ required: false
+ type: bool
+ default: false
+ production:
+ description:
+ - Install dependencies in production mode.
+ - Pnpm will ignore any dependencies under C(devDependencies) in package.json.
+ required: false
+ type: bool
+ default: false
+ dev:
+ description:
+ - Install dependencies in development mode.
+ - Pnpm will ignore any regular dependencies in C(package.json).
+ required: false
+ default: false
+ type: bool
+ optional:
+ description:
+ - Install dependencies in optional mode.
+ required: false
+ default: false
+ type: bool
+ state:
+ description:
+ - Installation state of the named node.js library.
+ - If V(absent) is selected, a name option must be provided.
+ type: str
+ required: false
+ default: present
+ choices: ["present", "absent", "latest"]
+requirements:
+ - Pnpm executable present in E(PATH).
+"""
+
+EXAMPLES = """
+- name: Install "tailwindcss" node.js package.
+ community.general.pnpm:
+ name: tailwindcss
+ path: /app/location
+
+- name: Install "tailwindcss" node.js package on version 3.3.2
+ community.general.pnpm:
+ name: tailwindcss
+ version: 3.3.2
+ path: /app/location
+
+- name: Install "tailwindcss" node.js package globally.
+ community.general.pnpm:
+ name: tailwindcss
+ global: true
+
+- name: Install "tailwindcss" node.js package as dev dependency.
+ community.general.pnpm:
+ name: tailwindcss
+ path: /app/location
+ dev: true
+
+- name: Install "tailwindcss" node.js package as optional dependency.
+ community.general.pnpm:
+ name: tailwindcss
+ path: /app/location
+ optional: true
+
+- name: Install "tailwindcss" node.js package version 0.1.3 as tailwind-1
+ community.general.pnpm:
+ name: tailwindcss
+ alias: tailwind-1
+ version: 0.1.3
+ path: /app/location
+
+- name: Remove the globally-installed package "tailwindcss".
+ community.general.pnpm:
+ name: tailwindcss
+ global: true
+ state: absent
+
+- name: Install packages based on package.json.
+ community.general.pnpm:
+ path: /app/location
+
+- name: Update all packages in package.json to their latest version.
+ community.general.pnpm:
+ path: /app/location
+ state: latest
+"""
+import json
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+
+class Pnpm(object):
+ def __init__(self, module, **kwargs):
+ self.module = module
+ self.name = kwargs["name"]
+ self.alias = kwargs["alias"]
+ self.version = kwargs["version"]
+ self.path = kwargs["path"]
+ self.globally = kwargs["globally"]
+ self.executable = kwargs["executable"]
+ self.ignore_scripts = kwargs["ignore_scripts"]
+ self.no_optional = kwargs["no_optional"]
+ self.production = kwargs["production"]
+ self.dev = kwargs["dev"]
+ self.optional = kwargs["optional"]
+
+ self.alias_name_ver = None
+
+ if self.alias is not None:
+ self.alias_name_ver = self.alias + "@npm:"
+
+ if self.name is not None:
+ self.alias_name_ver = (self.alias_name_ver or "") + self.name
+ if self.version is not None:
+ self.alias_name_ver = self.alias_name_ver + "@" + str(self.version)
+ else:
+ self.alias_name_ver = self.alias_name_ver + "@latest"
+
+ def _exec(self, args, run_in_check_mode=False, check_rc=True):
+ if not self.module.check_mode or (self.module.check_mode and run_in_check_mode):
+ cmd = self.executable + args
+
+ if self.globally:
+ cmd.append("-g")
+
+ if self.ignore_scripts:
+ cmd.append("--ignore-scripts")
+
+ if self.no_optional:
+ cmd.append("--no-optional")
+
+ if self.production:
+ cmd.append("-P")
+
+ if self.dev:
+ cmd.append("-D")
+
+ if self.name and self.optional:
+ cmd.append("-O")
+
+ # If path is specified, cd into that path and run the command.
+ cwd = None
+ if self.path:
+ if not os.path.exists(self.path):
+ os.makedirs(self.path)
+
+ if not os.path.isdir(self.path):
+ self.module.fail_json(msg="Path %s is not a directory" % self.path)
+
+ if not self.alias_name_ver and not os.path.isfile(
+ os.path.join(self.path, "package.json")
+ ):
+ self.module.fail_json(
+ msg="package.json does not exist in provided path"
+ )
+
+ cwd = self.path
+
+ _rc, out, err = self.module.run_command(cmd, check_rc=check_rc, cwd=cwd)
+ return out, err
+
+ return None, None
+
+ def missing(self):
+ if not os.path.isfile(os.path.join(self.path, "pnpm-lock.yaml")):
+ return True
+
+ cmd = ["list", "--json"]
+
+ if self.name is not None:
+ cmd.append(self.name)
+
+ try:
+ out, err = self._exec(cmd, True, False)
+ if err is not None and err != "":
+ raise Exception(out)
+
+ data = json.loads(out)
+ except Exception as e:
+ self.module.fail_json(
+ msg="Failed to parse pnpm output with error %s" % to_native(e)
+ )
+
+ if "error" in data:
+ return True
+
+ data = data[0]
+
+ for typedep in [
+ "dependencies",
+ "devDependencies",
+ "optionalDependencies",
+ "unsavedDependencies",
+ ]:
+ if typedep not in data:
+ continue
+
+ for dep, prop in data[typedep].items():
+ if self.alias is not None and self.alias != dep:
+ continue
+
+ name = prop["from"] if self.alias is not None else dep
+ if self.name != name:
+ continue
+
+ if self.version is None or self.version == prop["version"]:
+ return False
+
+ break
+
+ return True
+
+ def install(self):
+ if self.alias_name_ver is not None:
+ return self._exec(["add", self.alias_name_ver])
+ return self._exec(["install"])
+
+ def update(self):
+ return self._exec(["update", "--latest"])
+
+ def uninstall(self):
+ if self.alias is not None:
+ return self._exec(["remove", self.alias])
+ return self._exec(["remove", self.name])
+
+ def list_outdated(self):
+ if not os.path.isfile(os.path.join(self.path, "pnpm-lock.yaml")):
+ return list()
+
+ cmd = ["outdated", "--format", "json"]
+ try:
+ out, err = self._exec(cmd, True, False)
+
+ # BUG: It will not show correct error sometimes, like when it has
+ # plain text output intermingled with a {}
+ if err is not None and err != "":
+ raise Exception(out)
+
+ # HACK: To fix the above bug, the following hack is implemented
+ data_lines = out.splitlines(True)
+
+ out = None
+ for line in data_lines:
+ if len(line) > 0 and line[0] == "{":
+ out = line
+ continue
+
+ if len(line) > 0 and line[0] == "}":
+ out += line
+ break
+
+ if out is not None:
+ out += line
+
+ data = json.loads(out)
+ except Exception as e:
+ self.module.fail_json(
+ msg="Failed to parse pnpm output with error %s" % to_native(e)
+ )
+
+ return data.keys()
+
+
+def main():
+ arg_spec = dict(
+ name=dict(default=None),
+ alias=dict(default=None),
+ path=dict(default=None, type="path"),
+ version=dict(default=None),
+ executable=dict(default=None, type="path"),
+ ignore_scripts=dict(default=False, type="bool"),
+ no_optional=dict(default=False, type="bool"),
+ production=dict(default=False, type="bool"),
+ dev=dict(default=False, type="bool"),
+ optional=dict(default=False, type="bool"),
+ state=dict(default="present", choices=["present", "absent", "latest"]),
+ )
+ arg_spec["global"] = dict(default=False, type="bool")
+ module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True)
+
+ name = module.params["name"]
+ alias = module.params["alias"]
+ path = module.params["path"]
+ version = module.params["version"]
+ globally = module.params["global"]
+ ignore_scripts = module.params["ignore_scripts"]
+ no_optional = module.params["no_optional"]
+ production = module.params["production"]
+ dev = module.params["dev"]
+ optional = module.params["optional"]
+ state = module.params["state"]
+
+ if module.params["executable"]:
+ executable = module.params["executable"].split(" ")
+ else:
+ executable = [module.get_bin_path("pnpm", True)]
+
+ if name is None and version is not None:
+ module.fail_json(msg="version is meaningless when name is not provided")
+
+ if name is None and alias is not None:
+ module.fail_json(msg="alias is meaningless when name is not provided")
+
+ if path is None and not globally:
+ module.fail_json(msg="path must be specified when not using global")
+ elif path is not None and globally:
+ module.fail_json(msg="Cannot specify path when doing global installation")
+
+ if globally and (production or dev or optional):
+ module.fail_json(
+ msg="Options production, dev, and optional is meaningless when installing packages globally"
+ )
+
+ if name is not None and path is not None and globally:
+ module.fail_json(msg="path should not be mentioned when installing globally")
+
+ if production and dev and optional:
+ module.fail_json(
+ msg="Options production and dev and optional don't go together"
+ )
+
+ if production and dev:
+ module.fail_json(msg="Options production and dev don't go together")
+
+ if production and optional:
+ module.fail_json(msg="Options production and optional don't go together")
+
+ if dev and optional:
+ module.fail_json(msg="Options dev and optional don't go together")
+
+ if name is not None and name[0:4] == "http" and version is not None:
+ module.fail_json(msg="Semver not supported on remote url downloads")
+
+ if name is None and optional:
+ module.fail_json(
+ msg="Optional not available when package name not provided, use no_optional instead"
+ )
+
+ if state == "absent" and name is None:
+ module.fail_json(msg="Package name is required for uninstalling")
+
+ if globally:
+ _rc, out, _err = module.run_command(executable + ["root", "-g"], check_rc=True)
+ path, _tail = os.path.split(out.strip())
+
+ pnpm = Pnpm(
+ module,
+ name=name,
+ alias=alias,
+ path=path,
+ version=version,
+ globally=globally,
+ executable=executable,
+ ignore_scripts=ignore_scripts,
+ no_optional=no_optional,
+ production=production,
+ dev=dev,
+ optional=optional,
+ )
+
+ changed = False
+ out = ""
+ err = ""
+ if state == "present":
+ if pnpm.missing():
+ changed = True
+ out, err = pnpm.install()
+ elif state == "latest":
+ outdated = pnpm.list_outdated()
+ if name is not None:
+ if pnpm.missing() or name in outdated:
+ changed = True
+ out, err = pnpm.install()
+ elif len(outdated):
+ changed = True
+ out, err = pnpm.update()
+ else: # absent
+ if not pnpm.missing():
+ changed = True
+ out, err = pnpm.uninstall()
+
+ module.exit_json(changed=changed, out=out, err=err)
+
+
+if __name__ == "__main__":
+ main()