Blame SOURCES/nodejs.req

27913e
#!/usr/bin/python
27913e
# -*- coding: utf-8 -*-
27913e
# Copyright 2012, 2013 T.C. Hollingsworth <tchollingsworth@gmail.com>
27913e
# Copyright 2019 Jan Staněk <jstanek@redat.com>
27913e
#
27913e
# Permission is hereby granted, free of charge, to any person obtaining a copy
27913e
# of this software and associated documentation files (the "Software"), to
27913e
# deal in the Software without restriction, including without limitation the
27913e
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
27913e
# sell copies of the Software, and to permit persons to whom the Software is
27913e
# furnished to do so, subject to the following conditions:
27913e
#
27913e
# The above copyright notice and this permission notice shall be included in
27913e
# all copies or substantial portions of the Software.
27913e
#
27913e
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27913e
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27913e
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27913e
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27913e
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27913e
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
27913e
# IN THE SOFTWARE.
27913e
27913e
""" Automatic dependency generator for Node.js libraries.
27913e
27913e
Metadata parsed from package.json.  See `man npm-json` for details.
27913e
"""
27913e
27913e
from __future__ import print_function, with_statement
27913e
27913e
import json
27913e
import operator
27913e
import os
27913e
import re
27913e
import sys
27913e
from collections import namedtuple
27913e
from itertools import chain
27913e
from itertools import takewhile
27913e
27913e
# Python version detection
27913e
_PY2 = sys.version_info[0] <= 2
27913e
_PY3 = sys.version_info[0] >= 3
27913e
27913e
if _PY2:
27913e
    from future_builtins import map, filter
27913e
27913e
27913e
#: Name format of the requirements
27913e
REQUIREMENT_NAME_TEMPLATE = "npm({name})"
27913e
27913e
#: ``simple`` product of the NPM semver grammar.
27913e
RANGE_SPECIFIER_SIMPLE = re.compile(
27913e
    r"""
27913e
    (?P<operator>
27913e
        <= | >= | < | > | =     # primitive
27913e
        | ~ | \^                # tilde/caret operators
27913e
    )?
27913e
    \s*(?P<version>\S+)\s*  # version specifier
27913e
    """,
27913e
    flags=re.VERBOSE,
27913e
)
27913e
27913e
27913e
class UnsupportedVersionToken(ValueError):
27913e
    """Version specifier contains token unsupported by the parser."""
27913e
27913e
27913e
class Version(tuple):
27913e
    """Normalized RPM/NPM version.
27913e
27913e
    The version has up to 3 components – major, minor, patch.
27913e
    Any part set to None is treated as unspecified.
27913e
27913e
    ::
27913e
27913e
        1.2.3 == Version(1, 2, 3)
27913e
        1.2   == Version(1, 2)
27913e
        1     == Version(1)
27913e
        *     == Version()
27913e
    """
27913e
27913e
    __slots__ = ()
27913e
27913e
    #: Version part meaning 'Any'
27913e
    #: ``xr`` in https://docs.npmjs.com/misc/semver#range-grammar
27913e
    _PART_ANY = re.compile(r"^[xX*]$")
27913e
    #: Numeric version part
27913e
    #: ``nr`` in https://docs.npmjs.com/misc/semver#range-grammar
27913e
    _PART_NUMERIC = re.compile(r"0|[1-9]\d*")
27913e
27913e
    def __new__(cls, *args):
27913e
        """Create new version.
27913e
27913e
        Arguments:
27913e
            Version components in the order of "major", "minor", "patch".
27913e
            All parts are optional::
27913e
27913e
                >>> Version(1, 2, 3)
27913e
                Version(1, 2, 3)
27913e
                >>> Version(1)
27913e
                Version(1)
27913e
                >>> Version()
27913e
                Version()
27913e
27913e
        Returns:
27913e
            New Version.
27913e
        """
27913e
27913e
        if len(args) > 3:
27913e
            raise ValueError("Version has maximum of 3 components")
27913e
        return super(Version, cls).__new__(cls, map(int, args))
27913e
27913e
    def __repr__(self):
27913e
        """Pretty debugging format."""
27913e
27913e
        return "{0}({1})".format(self.__class__.__name__, ", ".join(map(str, self)))
27913e
27913e
    def __str__(self):
27913e
        """RPM version format."""
27913e
27913e
        return ".".join(format(part, "d") for part in self)
27913e
27913e
    @property
27913e
    def major(self):
27913e
        """Major version number, if any."""
27913e
        return self[0] if len(self) > 0 else None
27913e
27913e
    @property
27913e
    def minor(self):
27913e
        """Major version number, if any."""
27913e
        return self[1] if len(self) > 1 else None
27913e
27913e
    @property
27913e
    def patch(self):
27913e
        """Major version number, if any."""
27913e
        return self[2] if len(self) > 2 else None
27913e
27913e
    @property
27913e
    def empty(self):
27913e
        """True if the version contains nothing but zeroes."""
27913e
        return not any(self)
27913e
27913e
    @classmethod
27913e
    def parse(cls, version_string):
27913e
        """Parse individual version string (like ``1.2.3``) into Version.
27913e
27913e
        This is the ``partial`` production in the grammar:
27913e
        https://docs.npmjs.com/misc/semver#range-grammar
27913e
27913e
        Examples::
27913e
27913e
            >>> Version.parse("1.2.3")
27913e
            Version(1, 2, 3)
27913e
            >>> Version.parse("v2.x")
27913e
            Version(2)
27913e
            >>> Version.parse("")
27913e
            Version()
27913e
27913e
        Arguments:
27913e
            version_string (str): The version_string to parse.
27913e
27913e
        Returns:
27913e
            Version: Parsed result.
27913e
        """
27913e
27913e
        # Ignore leading ``v``, if any
27913e
        version_string = version_string.lstrip("v")
27913e
27913e
        part_list = version_string.split(".", 2)
27913e
        # Use only parts up to first "Any" indicator
27913e
        part_list = list(takewhile(lambda p: not cls._PART_ANY.match(p), part_list))
27913e
27913e
        if not part_list:
27913e
            return cls()
27913e
27913e
        # Strip off and discard any pre-release or build qualifiers at the end.
27913e
        # We can get away with this, because there is no sane way to represent
27913e
        # these kinds of version requirements in RPM, and we generally expect
27913e
        # the distro will only carry proper releases anyway.
27913e
        try:
27913e
            part_list[-1] = cls._PART_NUMERIC.match(part_list[-1]).group()
27913e
        except AttributeError:  # no match
27913e
            part_list.pop()
27913e
27913e
        # Extend with ``None``s at the end, if necessary
27913e
        return cls(*part_list)
27913e
27913e
    def incremented(self):
27913e
        """Increment the least significant part of the version::
27913e
27913e
            >>> Version(1, 2, 3).incremented()
27913e
            Version(1, 2, 4)
27913e
            >>> Version(1, 2).incremented()
27913e
            Version(1, 3)
27913e
            >>> Version(1).incremented()
27913e
            Version(2)
27913e
            >>> Version().incremented()
27913e
            Version()
27913e
27913e
        Returns:
27913e
            Version: New incremented Version.
27913e
        """
27913e
27913e
        if len(self) == 0:
27913e
            return self.__class__()
27913e
        else:
27913e
            args = self[:-1] + (self[-1] + 1,)
27913e
            return self.__class__(*args)
27913e
27913e
27913e
class VersionBoundary(namedtuple("VersionBoundary", ("version", "operator"))):
27913e
    """Normalized version range boundary."""
27913e
27913e
    __slots__ = ()
27913e
27913e
    #: Ordering of primitive operators.
27913e
    #: Operators not listed here are handled specially; see __compare below.
27913e
    #: Convention: Lower boundary < 0, Upper boundary > 0
27913e
    _OPERATOR_ORDER = {"<": 2, "<=": 1, ">=": -1, ">": -2}
27913e
27913e
    def __str__(self):
27913e
        """Pretty-print the boundary"""
27913e
27913e
        return "{0.operator}{0.version}".format(self)
27913e
27913e
    def __compare(self, other, operator):
27913e
        """Compare two boundaries with provided operator.
27913e
27913e
        Boundaries compare same as (version, operator_order) tuple.
27913e
        In case the boundary operator is not listed in _OPERATOR_ORDER,
27913e
        it's order is treated as 0.
27913e
27913e
        Arguments:
27913e
            other (VersionBoundary): The other boundary to compare with.
27913e
            operator (Callable[[VersionBoundary, VersionBoundary], bool]):
27913e
                Comparison operator to delegate to.
27913e
27913e
        Returns:
27913e
            bool: The result of the operator's comparison.
27913e
        """
27913e
27913e
        ORDER = self._OPERATOR_ORDER
27913e
27913e
        lhs = self.version, ORDER.get(self.operator, 0)
27913e
        rhs = other.version, ORDER.get(other.operator, 0)
27913e
        return operator(lhs, rhs)
27913e
27913e
    def __eq__(self, other):
27913e
        return self.__compare(other, operator.eq)
27913e
27913e
    def __lt__(self, other):
27913e
        return self.__compare(other, operator.lt)
27913e
27913e
    def __le__(self, other):
27913e
        return self.__compare(other, operator.le)
27913e
27913e
    def __gt__(self, other):
27913e
        return self.__compare(other, operator.gt)
27913e
27913e
    def __ge__(self, other):
27913e
        return self.__compare(other, operator.ge)
27913e
27913e
    @property
27913e
    def upper(self):
27913e
        """True if self is upper boundary."""
27913e
        return self._OPERATOR_ORDER.get(self.operator, 0) > 0
27913e
27913e
    @property
27913e
    def lower(self):
27913e
        """True if self is lower boundary."""
27913e
        return self._OPERATOR_ORDER.get(self.operator, 0) < 0
27913e
27913e
    @classmethod
27913e
    def equal(cls, version):
27913e
        """Normalize single samp:`={version}` into equivalent x-range::
27913e
27913e
            >>> empty = VersionBoundary.equal(Version()); tuple(map(str, empty))
27913e
            ()
27913e
            >>> patch = VersionBoundary.equal(Version(1, 2, 3)); tuple(map(str, patch))
27913e
            ('>=1.2.3', '<1.2.4')
27913e
            >>> minor = VersionBoundary.equal(Version(1, 2)); tuple(map(str, minor))
27913e
            ('>=1.2', '<1.3')
27913e
            >>> major = VersionBoundary.equal(Version(1)); tuple(map(str, major))
27913e
            ('>=1', '<2')
27913e
27913e
        See `X-Ranges <https://docs.npmjs.com/misc/semver#x-ranges-12x-1x-12->`_
27913e
        for details.
27913e
27913e
        Arguments:
27913e
            version (Version): The version the x-range should be equal to.
27913e
27913e
        Returns:
27913e
            (VersionBoundary, VersionBoundary):
27913e
                Lower and upper bound of the x-range.
27913e
            (): Empty tuple in case version is empty (any version matches).
27913e
        """
27913e
27913e
        if version:
27913e
            return (
27913e
                cls(version=version, operator=">="),
27913e
                cls(version=version.incremented(), operator="<"),
27913e
            )
27913e
        else:
27913e
            return ()
27913e
27913e
    @classmethod
27913e
    def tilde(cls, version):
27913e
        """Normalize :samp:`~{version}` into equivalent range.
27913e
27913e
        Tilde allows patch-level changes if a minor version is specified.
27913e
        Allows minor-level changes if not::
27913e
27913e
            >>> with_minor = VersionBoundary.tilde(Version(1, 2, 3)); tuple(map(str, with_minor))
27913e
            ('>=1.2.3', '<1.3')
27913e
            >>> no_minor = VersionBoundary.tilde(Version(1)); tuple(map(str, no_minor))
27913e
            ('>=1', '<2')
27913e
27913e
        Arguments:
27913e
            version (Version): The version to tilde-expand.
27913e
27913e
        Returns:
27913e
            (VersionBoundary, VersionBoundary):
27913e
                The lower and upper boundary of the tilde range.
27913e
        """
27913e
27913e
        # Fail on ``~*`` or similar nonsense specifier
27913e
        assert version.major is not None, "Nonsense '~*' specifier"
27913e
27913e
        lower_boundary = cls(version=version, operator=">=")
27913e
27913e
        if version.minor is None:
27913e
            upper_boundary = cls(version=Version(version.major + 1), operator="<")
27913e
        else:
27913e
            upper_boundary = cls(
27913e
                version=Version(version.major, version.minor + 1), operator="<"
27913e
            )
27913e
27913e
        return lower_boundary, upper_boundary
27913e
27913e
    @classmethod
27913e
    def caret(cls, version):
27913e
        """Normalize :samp:`^{version}` into equivalent range.
27913e
27913e
        Caret allows changes that do not modify the left-most non-zero digit
27913e
        in the ``(major, minor, patch)`` tuple.
27913e
        In other words, this allows
27913e
        patch and minor updates for versions 1.0.0 and above,
27913e
        patch updates for versions 0.X >=0.1.0,
27913e
        and no updates for versions 0.0.X::
27913e
27913e
            >>> major = VersionBoundary.caret(Version(1, 2, 3)); tuple(map(str, major))
27913e
            ('>=1.2.3', '<2')
27913e
            >>> minor = VersionBoundary.caret(Version(0, 2, 3)); tuple(map(str, minor))
27913e
            ('>=0.2.3', '<0.3')
27913e
            >>> patch = VersionBoundary.caret(Version(0, 0, 3)); tuple(map(str, patch))
27913e
            ('>=0.0.3', '<0.0.4')
27913e
27913e
        When parsing caret ranges, a missing patch value desugars to the number 0,
27913e
        but will allow flexibility within that value,
27913e
        even if the major and minor versions are both 0::
27913e
27913e
            >>> rel = VersionBoundary.caret(Version(1, 2)); tuple(map(str, rel))
27913e
            ('>=1.2', '<2')
27913e
            >>> pre = VersionBoundary.caret(Version(0, 0)); tuple(map(str, pre))
27913e
            ('>=0.0', '<0.1')
27913e
27913e
        A missing minor and patch values will desugar to zero,
27913e
        but also allow flexibility within those values,
27913e
        even if the major version is zero::
27913e
27913e
            >>> rel = VersionBoundary.caret(Version(1)); tuple(map(str, rel))
27913e
            ('>=1', '<2')
27913e
            >>> pre = VersionBoundary.caret(Version(0)); tuple(map(str, pre))
27913e
            ('>=0', '<1')
27913e
27913e
        Arguments:
27913e
            version (Version): The version to range-expand.
27913e
27913e
        Returns:
27913e
            (VersionBoundary, VersionBoundary):
27913e
                The lower and upper boundary of caret-range.
27913e
        """
27913e
27913e
        # Fail on ^* or similar nonsense specifier
27913e
        assert len(version) != 0, "Nonsense '^*' specifier"
27913e
27913e
        lower_boundary = cls(version=version, operator=">=")
27913e
27913e
        # Increment left-most non-zero part
27913e
        for idx, part in enumerate(version):
27913e
            if part != 0:
27913e
                upper_version = Version(*(version[:idx] + (part + 1,)))
27913e
                break
27913e
        else:  # No non-zero found; increment last specified part
27913e
            upper_version = version.incremented()
27913e
27913e
        upper_boundary = cls(version=upper_version, operator="<")
27913e
27913e
        return lower_boundary, upper_boundary
27913e
27913e
    @classmethod
27913e
    def hyphen(cls, lower_version, upper_version):
27913e
        """Construct hyphen range (inclusive set)::
27913e
27913e
            >>> full = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3, 4)); tuple(map(str, full))
27913e
            ('>=1.2.3', '<=2.3.4')
27913e
27913e
        If a partial version is provided as the first version in the inclusive range,
27913e
        then the missing pieces are treated as zeroes::
27913e
27913e
            >>> part = VersionBoundary.hyphen(Version(1, 2), Version(2, 3, 4)); tuple(map(str, part))
27913e
            ('>=1.2', '<=2.3.4')
27913e
27913e
        If a partial version is provided as the second version in the inclusive range,
27913e
        then all versions that start with the supplied parts of the tuple are accepted,
27913e
        but nothing that would be greater than the provided tuple parts::
27913e
27913e
            >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3)); tuple(map(str, part))
27913e
            ('>=1.2.3', '<2.4')
27913e
            >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2)); tuple(map(str, part))
27913e
            ('>=1.2.3', '<3')
27913e
27913e
        Arguments:
27913e
            lower_version (Version): Version on the lower range boundary.
27913e
            upper_version (Version): Version on the upper range boundary.
27913e
27913e
        Returns:
27913e
            (VersionBoundary, VersionBoundary):
27913e
                Lower and upper boundaries of the hyphen range.
27913e
        """
27913e
27913e
        lower_boundary = cls(version=lower_version, operator=">=")
27913e
27913e
        if len(upper_version) < 3:
27913e
            upper_boundary = cls(version=upper_version.incremented(), operator="<")
27913e
        else:
27913e
            upper_boundary = cls(version=upper_version, operator="<=")
27913e
27913e
        return lower_boundary, upper_boundary
27913e
27913e
27913e
def parse_simple_seq(specifier_string):
27913e
    """Parse all specifiers from a space-separated string::
27913e
27913e
        >>> single = parse_simple_seq(">=1.2.3"); list(map(str, single))
27913e
        ['>=1.2.3']
27913e
        >>> multi = parse_simple_seq("~1.2.0 <1.2.5"); list(map(str, multi))
27913e
        ['>=1.2.0', '<1.3', '<1.2.5']
27913e
27913e
    This method implements the ``simple (' ' simple)*`` part of the grammar:
27913e
    https://docs.npmjs.com/misc/semver#range-grammar.
27913e
27913e
    Arguments:
27913e
        specifier_string (str): Space-separated string of simple version specifiers.
27913e
27913e
    Yields:
27913e
        VersionBoundary: Parsed boundaries.
27913e
    """
27913e
27913e
    # Per-operator dispatch table
27913e
    # API: Callable[[Version], Iterable[VersionBoundary]]
27913e
    handler = {
27913e
        ">": lambda v: [VersionBoundary(version=v, operator=">")],
27913e
        ">=": lambda v: [VersionBoundary(version=v, operator=">=")],
27913e
        "<=": lambda v: [VersionBoundary(version=v, operator="<=")],
27913e
        "<": lambda v: [VersionBoundary(version=v, operator="<")],
27913e
        "=": VersionBoundary.equal,
27913e
        "~": VersionBoundary.tilde,
27913e
        "^": VersionBoundary.caret,
27913e
        None: VersionBoundary.equal,
27913e
    }
27913e
27913e
    for match in RANGE_SPECIFIER_SIMPLE.finditer(specifier_string):
27913e
        operator, version_string = match.group("operator", "version")
27913e
27913e
        for boundary in handler[operator](Version.parse(version_string)):
27913e
            yield boundary
27913e
27913e
27913e
def parse_range(range_string):
27913e
    """Parse full NPM version range specification::
27913e
27913e
        >>> empty = parse_range(""); list(map(str, empty))
27913e
        []
27913e
        >>> simple = parse_range("^1.0"); list(map(str, simple))
27913e
        ['>=1.0', '<2']
27913e
        >>> hyphen = parse_range("1.0 - 2.0"); list(map(str, hyphen))
27913e
        ['>=1.0', '<2.1']
27913e
27913e
    This method implements the ``range`` part of the grammar:
27913e
    https://docs.npmjs.com/misc/semver#range-grammar.
27913e
27913e
    Arguments:
27913e
        range_string (str): The range specification to parse.
27913e
27913e
    Returns:
27913e
        Iterable[VersionBoundary]: Parsed boundaries.
27913e
27913e
    Raises:
27913e
        UnsupportedVersionToken: ``||`` is present in range_string.
27913e
    """
27913e
27913e
    HYPHEN = " - "
27913e
27913e
    # FIXME: rpm should be able to process OR in dependencies
27913e
    # This error reporting kept for backward compatibility
27913e
    if "||" in range_string:
27913e
        raise UnsupportedVersionToken(range_string)
27913e
27913e
    if HYPHEN in range_string:
27913e
        version_pair = map(Version.parse, range_string.split(HYPHEN, 2))
27913e
        return VersionBoundary.hyphen(*version_pair)
27913e
27913e
    elif range_string != "":
27913e
        return parse_simple_seq(range_string)
27913e
27913e
    else:
27913e
        return []
27913e
27913e
27913e
def unify_range(boundary_iter):
27913e
    """Calculate largest allowed continuous version range from a set of boundaries::
27913e
27913e
        >>> unify_range([])
27913e
        ()
27913e
        >>> _ = unify_range(parse_range("=1.2.3 <2")); tuple(map(str, _))
27913e
        ('>=1.2.3', '<1.2.4')
27913e
        >>> _ = unify_range(parse_range("~1.2 <1.2.5")); tuple(map(str, _))
27913e
        ('>=1.2', '<1.2.5')
27913e
27913e
    Arguments:
27913e
        boundary_iter (Iterable[VersionBoundary]): The version boundaries to unify.
27913e
27913e
    Returns:
27913e
        (VersionBoundary, VersionBoundary):
27913e
            Lower and upper boundary of the unified range.
27913e
    """
27913e
27913e
    # Drop boundaries with empty version
27913e
    boundary_iter = (
27913e
        boundary for boundary in boundary_iter if not boundary.version.empty
27913e
    )
27913e
27913e
    # Split input sequence into upper/lower boundaries
27913e
    lower_list, upper_list = [], []
27913e
    for boundary in boundary_iter:
27913e
        if boundary.lower:
27913e
            lower_list.append(boundary)
27913e
        elif boundary.upper:
27913e
            upper_list.append(boundary)
27913e
        else:
27913e
            msg = "Unsupported boundary for unify_range: {0}".format(boundary)
27913e
            raise ValueError(msg)
27913e
27913e
    # Select maximum from lower boundaries and minimum from upper boundaries
27913e
    intermediate = (
27913e
        max(lower_list) if lower_list else None,
27913e
        min(upper_list) if upper_list else None,
27913e
    )
27913e
27913e
    return tuple(filter(None, intermediate))
27913e
27913e
27913e
def rpm_format(requirement, version_spec="*"):
27913e
    """Format requirement as RPM boolean dependency::
27913e
27913e
        >>> rpm_format("nodejs(engine)")
27913e
        'nodejs(engine)'
27913e
        >>> rpm_format("npm(foo)", ">=1.0.0")
27913e
        'npm(foo) >= 1.0.0'
27913e
        >>> rpm_format("npm(bar)", "~1.2")
27913e
        '(npm(bar) >= 1.2 with npm(bar) < 1.3)'
27913e
27913e
    Arguments:
27913e
        requirement (str): The name of the requirement.
27913e
        version_spec (str): The NPM version specification for the requirement.
27913e
27913e
    Returns:
27913e
        str: Formatted requirement.
27913e
    """
27913e
27913e
    TEMPLATE = "{name} {boundary.operator} {boundary.version!s}"
27913e
27913e
    try:
27913e
        boundary_tuple = unify_range(parse_range(version_spec))
27913e
27913e
    except UnsupportedVersionToken:
27913e
        # FIXME: Typos and print behavior kept for backward compatibility
27913e
        warning_lines = [
27913e
            "WARNING: The {requirement} dependency contains an OR (||) dependency: '{version_spec}.",
27913e
            "Please manually include a versioned dependency in your spec file if necessary",
27913e
        ]
27913e
        warning = "\n".join(warning_lines).format(
27913e
            requirement=requirement, version_spec=version_spec
27913e
        )
27913e
        print(warning, end="", file=sys.stderr)
27913e
27913e
        return requirement
27913e
27913e
    formatted = [
27913e
        TEMPLATE.format(name=requirement, boundary=boundary)
27913e
        for boundary in boundary_tuple
27913e
    ]
27913e
27913e
    if len(formatted) > 1:
27913e
        return "({0})".format(" with ".join(formatted))
27913e
    elif len(formatted) == 1:
27913e
        return formatted[0]
27913e
    else:
27913e
        return requirement
27913e
27913e
27913e
def has_only_bundled_dependencies(module_dir_path):
27913e
    """Determines if the module contains only bundled dependencies.
27913e
27913e
    Dependencies are considered un-bundled when they are symlinks
27913e
    pointing outside the root module's tree.
27913e
27913e
    Arguments:
27913e
        module_dir_path (str):
27913e
            Path to the module directory (directory with ``package.json``).
27913e
27913e
    Returns:
27913e
        bool: True if all dependencies are bundled, False otherwise.
27913e
    """
27913e
27913e
    module_root_path = os.path.abspath(module_dir_path)
27913e
    dependency_root_path = os.path.join(module_root_path, "node_modules")
27913e
27913e
    try:
27913e
        dependency_path_iter = (
27913e
            os.path.join(dependency_root_path, basename)
27913e
            for basename in os.listdir(dependency_root_path)
27913e
        )
27913e
        linked_dependency_iter = (
27913e
            os.path.realpath(path)
27913e
            for path in dependency_path_iter
27913e
            if os.path.islink(path)
27913e
        )
27913e
        outside_dependency_iter = (
27913e
            path
27913e
            for path in linked_dependency_iter
27913e
            if not path.startswith(module_root_path)
27913e
        )
27913e
27913e
        return not any(outside_dependency_iter)
27913e
    except OSError:  # node_modules does not exist
27913e
        return False
27913e
27913e
27913e
def extract_dependencies(metadata_path, optional=False):
27913e
    """Extract all dependencies in RPM format from package metadata.
27913e
27913e
    Arguments:
27913e
        metadata_path (str): Path to package metadata (``package.json``).
27913e
        optional (bool):
27913e
            If True, extract ``optionalDependencies``
27913e
            instead of ``dependencies``.
27913e
27913e
    Yields:
27913e
        RPM-formatted dependencies.
27913e
27913e
    Raises:
27913e
        TypeError: Invalid dependency data type.
27913e
    """
27913e
27913e
    if has_only_bundled_dependencies(os.path.dirname(metadata_path)):
27913e
        return  # skip
27913e
27913e
    # Read metadata
27913e
    try:
27913e
        with open(metadata_path, mode="r") as metadata_file:
27913e
            metadata = json.load(metadata_file)
27913e
    except OSError:  # Invalid metadata file
27913e
        return  # skip
27913e
27913e
    # Report required NodeJS version with required dependencies
27913e
    if not optional:
27913e
        try:
27913e
            yield rpm_format("nodejs(engine)", metadata["engines"]["node"])
27913e
        except KeyError:  # NodeJS engine version unspecified
27913e
            yield rpm_format("nodejs(engine)")
27913e
27913e
    # Report listed dependencies
27913e
    kind = "optionalDependencies" if optional else "dependencies"
27913e
    container = metadata.get(kind, {})
27913e
27913e
    if isinstance(container, dict):
27913e
        for name, version_spec in container.items():
27913e
            yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name), version_spec)
27913e
27913e
    elif isinstance(container, list):
27913e
        for name in container:
27913e
            yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name))
27913e
27913e
    elif isinstance(container, str):
27913e
        yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name))
27913e
27913e
    else:
27913e
        raise TypeError("invalid package.json: dependencies not a valid type")
27913e
27913e
27913e
if __name__ == "__main__":
27913e
    nested = (
27913e
        extract_dependencies(path.strip(), optional="--optional" in sys.argv)
27913e
        for path in sys.stdin
27913e
    )
27913e
    flat = chain.from_iterable(nested)
27913e
    # Ignore parentheses around the requirements when sorting
27913e
    ordered = sorted(flat, key=lambda s: s.strip("()"))
27913e
27913e
    print(*ordered, sep="\n")