diff --git a/SOURCES/nodejs.attr b/SOURCES/nodejs.attr
index 73636ec..6e7130b 100644
--- a/SOURCES/nodejs.attr
+++ b/SOURCES/nodejs.attr
@@ -1,3 +1,3 @@
-%__nodejs8_provides  %{_rpmconfigdir}/nodejs8.prov
-%__nodejs8_requires  %{_rpmconfigdir}/nodejs8.req
-%__nodejs8_path  /usr/lib.*/node_modules/.*/package\\.json$
+%__rh_nodejs8_provides  %{_rpmconfigdir}/rh_nodejs8.prov
+%__rh_nodejs8_requires  %{_rpmconfigdir}/rh_nodejs8.req
+%__rh_nodejs8_path  ^(/opt/rh/rh-nodejs8/root)?/usr/lib(64)?/node_modules/[^/]+/package\\.json$
diff --git a/SOURCES/nodejs.prov b/SOURCES/nodejs.prov
index a60c825..27acb53 100755
--- a/SOURCES/nodejs.prov
+++ b/SOURCES/nodejs.prov
@@ -1,11 +1,8 @@
 #!/usr/bin/python
-
-"""
-Automatic provides generator for Node.js libraries.
-
-Taken from package.json.  See `man npm-json` for details.
-"""
+# -*- coding: utf-8 -*-
 # Copyright 2012 T.C. Hollingsworth <tchollingsworth@gmail.com>
+# Copyright 2017 Tomas Tomecek <ttomecek@redhat.com>
+# Copyright 2019 Jan Staněk <jstanek@redhat.com>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to
@@ -25,26 +22,100 @@ Taken from package.json.  See `man npm-json` for details.
 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 # IN THE SOFTWARE.
 
+"""Automatic provides generator for Node.js libraries.
+
+Metadata taken from package.json.  See `man npm-json` for details.
+"""
+
+from __future__ import print_function, with_statement
+
 import json
-import subprocess
+import os
 import sys
+from itertools import chain, groupby
+
+DEPENDENCY_TEMPLATE = "rh-nodejs8-npm(%(name)s) = %(version)s"
+BUNDLED_TEMPLATE = "bundled(nodejs-%(name)s) = %(version)s"
+NODE_MODULES = {"node_modules", "node_modules.bundled"}
+
+
+class PrivatePackage(RuntimeError):
+    """Private package metadata that should not be listed."""
+
+
+#: Something is wrong with the ``package.json`` file
+_INVALID_METADATA_FILE = (IOError, PrivatePackage, KeyError)
+
+
+def format_metadata(metadata, bundled=False):
+    """Format ``package.json``-like metadata into RPM dependency.
+
+    Arguments:
+        metadata (dict): Package metadata, presumably read from ``package.json``.
+        bundled (bool): Should the bundled dependency format be used?
+
+    Returns:
+        str: RPM dependency (i.e. ``npm(example) = 1.0.0``)
+
+    Raises:
+        KeyError: Expected key (i.e. ``name``, ``version``) missing in metadata.
+        PrivatePackage: The metadata indicate private (unlisted) package.
+    """
+
+    # Skip private packages
+    if metadata.get("private", False):
+        raise PrivatePackage(metadata)
+
+    template = BUNDLED_TEMPLATE if bundled else DEPENDENCY_TEMPLATE
+    return template % metadata
+
+
+def generate_dependencies(module_path, module_dir_set=NODE_MODULES):
+    """Generate RPM dependency for a module and all it's dependencies.
+
+    Arguments:
+        module_path (str): Path to a module directory or it's ``package.json``
+        module_dir_set (set): Base names of directories to look into
+            for bundled dependencies.
+
+    Yields:
+        str: RPM dependency for the module and each of it's (public) bundled dependencies.
+
+    Raises:
+        ValueError: module_path is not valid module or ``package.json`` file
+    """
+
+    # Determine paths to root module directory and package.json
+    if os.path.isdir(module_path):
+        root_dir = module_path
+    elif os.path.basename(module_path) == "package.json":
+        root_dir = os.path.dirname(module_path)
+    else:  # Invalid metadata path
+        raise ValueError("Invalid module path '%s'" % module_path)
+
+    for dir_path, subdir_list, __ in os.walk(root_dir):
+        # Currently in node_modules (or similar), continue to subdirs
+        if os.path.basename(dir_path) in module_dir_set:
+            continue
 
-paths = [path.rstrip() for path in sys.stdin.readlines()]
+        # Read and format metadata
+        metadata_path = os.path.join(dir_path, "package.json")
+        bundled = dir_path != root_dir
+        try:
+            with open(metadata_path, mode="r") as metadata_file:
+                metadata = json.load(metadata_file)
+            yield format_metadata(metadata, bundled=bundled)
+        except _INVALID_METADATA_FILE:
+            pass  # Ignore
 
-for path in paths:
-    if path.endswith('package.json'):
-        fh = open(path)
-        metadata = json.load(fh)
-        fh.close()
+        # Only visit subdirectories in module_dir_set
+        subdir_list[:] = list(module_dir_set & set(subdir_list))
 
-        if 'name' in metadata and not ('private' in metadata and metadata['private']):
-            print 'rh-nodejs8-npm(' + metadata['name'] + ')',
 
-            if 'version' in metadata:
-                print '= ' + metadata['version']
-            else:
-                print
+if __name__ == "__main__":
+    module_paths = (path.strip() for path in sys.stdin)
+    provides = chain.from_iterable(generate_dependencies(m) for m in module_paths)
 
-# invoke the regular RPM provides generator                
-p = subprocess.Popen(['/usr/lib/rpm/find-provides'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
-print p.communicate(input='\n'.join(paths))[0]
+    # sort|uniq
+    for provide, __ in groupby(sorted(provides)):
+        print(provide)
diff --git a/SOURCES/nodejs.req b/SOURCES/nodejs.req
index abbaec9..665c433 100755
--- a/SOURCES/nodejs.req
+++ b/SOURCES/nodejs.req
@@ -1,12 +1,7 @@
 #!/usr/bin/python
-
-"""
-Automatic dependency generator for Node.js libraries.
-
-Parsed from package.json.  See `man npm-json` for details.
-"""
-
+# -*- coding: utf-8 -*-
 # Copyright 2012, 2013 T.C. Hollingsworth <tchollingsworth@gmail.com>
+# Copyright 2019 Jan Staněk <jstanek@redat.com>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to
@@ -17,7 +12,7 @@ Parsed from package.json.  See `man npm-json` for details.
 #
 # The above copyright notice and this permission notice shall be included in
 # all copies or substantial portions of the Software.
-# 
+#
 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -26,157 +21,693 @@ Parsed from package.json.  See `man npm-json` for details.
 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 # IN THE SOFTWARE.
 
-from __future__ import unicode_literals
+""" Automatic dependency generator for Node.js libraries.
+
+Metadata parsed from package.json.  See `man npm-json` for details.
+"""
+
+from __future__ import print_function
+from __future__ import with_statement
+
 import json
+import operator
 import os
 import re
-import subprocess
 import sys
+from collections import namedtuple
+from itertools import chain
+from itertools import takewhile
+
+# Python version detection
+_PY2 = sys.version_info[0] <= 2
+_PY3 = sys.version_info[0] >= 3
+
+if _PY2:
+    from future_builtins import map, filter
+
+
+#: Name format of the requirements
+REQUIREMENT_NAME_TEMPLATE = "rh-nodejs8-npm({name})"
+
+#: ``simple`` product of the NPM semver grammar.
+RANGE_SPECIFIER_SIMPLE = re.compile(
+    r"""
+    (?P<operator>
+        <= | >= | < | > | =     # primitive
+        | ~ | \^                # tilde/caret operators
+    )?
+    \s*(?P<version>\S+)\s*  # version specifier
+    """,
+    flags=re.VERBOSE,
+)
+
+
+class UnsupportedVersionToken(ValueError):
+    """Version specifier contains token unsupported by the parser."""
+
+
+class Version(tuple):
+    """Normalized RPM/NPM version.
+
+    The version has up to 3 components – major, minor, patch.
+    Any part set to None is treated as unspecified.
+
+    ::
+
+        1.2.3 == Version(1, 2, 3)
+        1.2   == Version(1, 2)
+        1     == Version(1)
+        *     == Version()
+    """
+
+    __slots__ = ()
+
+    #: Version part meaning 'Any'
+    #: ``xr`` in https://docs.npmjs.com/misc/semver#range-grammar
+    _PART_ANY = re.compile(r"^[xX*]$")
+    #: Numeric version part
+    #: ``nr`` in https://docs.npmjs.com/misc/semver#range-grammar
+    _PART_NUMERIC = re.compile(r"0|[1-9]\d*")
+
+    def __new__(cls, *args):
+        """Create new version.
+
+        Arguments:
+            Version components in the order of "major", "minor", "patch".
+            All parts are optional::
+
+                >>> Version(1, 2, 3)
+                Version(1, 2, 3)
+                >>> Version(1)
+                Version(1)
+                >>> Version()
+                Version()
+
+        Returns:
+            New Version.
+        """
+
+        if len(args) > 3:
+            raise ValueError("Version has maximum of 3 components")
+        return super(Version, cls).__new__(cls, map(int, args))
+
+    def __repr__(self):
+        """Pretty debugging format."""
+
+        return "{0}({1})".format(self.__class__.__name__, ", ".join(map(str, self)))
+
+    def __str__(self):
+        """RPM version format."""
+
+        return ".".join(format(part, "d") for part in self)
+
+    @property
+    def major(self):
+        """Major version number, if any."""
+        return self[0] if len(self) > 0 else None
+
+    @property
+    def minor(self):
+        """Major version number, if any."""
+        return self[1] if len(self) > 1 else None
+
+    @property
+    def patch(self):
+        """Major version number, if any."""
+        return self[2] if len(self) > 2 else None
+
+    @property
+    def empty(self):
+        """True if the version contains nothing but zeroes."""
+        return not any(self)
+
+    @classmethod
+    def parse(cls, version_string):
+        """Parse individual version string (like ``1.2.3``) into Version.
+
+        This is the ``partial`` production in the grammar:
+        https://docs.npmjs.com/misc/semver#range-grammar
+
+        Examples::
+
+            >>> Version.parse("1.2.3")
+            Version(1, 2, 3)
+            >>> Version.parse("v2.x")
+            Version(2)
+            >>> Version.parse("")
+            Version()
+
+        Arguments:
+            version_string (str): The version_string to parse.
+
+        Returns:
+            Version: Parsed result.
+        """
+
+        # Ignore leading ``v``, if any
+        version_string = version_string.lstrip("v")
+
+        part_list = version_string.split(".", 2)
+        # Use only parts up to first "Any" indicator
+        part_list = list(takewhile(lambda p: not cls._PART_ANY.match(p), part_list))
+
+        if not part_list:
+            return cls()
+
+        # Strip off and discard any pre-release or build qualifiers at the end.
+        # We can get away with this, because there is no sane way to represent
+        # these kinds of version requirements in RPM, and we generally expect
+        # the distro will only carry proper releases anyway.
+        try:
+            part_list[-1] = cls._PART_NUMERIC.match(part_list[-1]).group()
+        except AttributeError:  # no match
+            part_list.pop()
+
+        # Extend with ``None``s at the end, if necessary
+        return cls(*part_list)
+
+    def incremented(self):
+        """Increment the least significant part of the version::
+
+            >>> Version(1, 2, 3).incremented()
+            Version(1, 2, 4)
+            >>> Version(1, 2).incremented()
+            Version(1, 3)
+            >>> Version(1).incremented()
+            Version(2)
+            >>> Version().incremented()
+            Version()
+
+        Returns:
+            Version: New incremented Version.
+        """
+
+        if len(self) == 0:
+            return self.__class__()
+        else:
+            args = self[:-1] + (self[-1] + 1,)
+            return self.__class__(*args)
+
+
+class VersionBoundary(namedtuple("VersionBoundary", ("version", "operator"))):
+    """Normalized version range boundary."""
+
+    __slots__ = ()
+
+    #: Ordering of primitive operators.
+    #: Operators not listed here are handled specially; see __compare below.
+    #: Convention: Lower boundary < 0, Upper boundary > 0
+    _OPERATOR_ORDER = {"<": 2, "<=": 1, ">=": -1, ">": -2}
+
+    def __str__(self):
+        """Pretty-print the boundary"""
+
+        return "{0.operator}{0.version}".format(self)
+
+    def __compare(self, other, operator):
+        """Compare two boundaries with provided operator.
+
+        Boundaries compare same as (version, operator_order) tuple.
+        In case the boundary operator is not listed in _OPERATOR_ORDER,
+        it's order is treated as 0.
+
+        Arguments:
+            other (VersionBoundary): The other boundary to compare with.
+            operator (Callable[[VersionBoundary, VersionBoundary], bool]):
+                Comparison operator to delegate to.
+
+        Returns:
+            bool: The result of the operator's comparison.
+        """
+
+        ORDER = self._OPERATOR_ORDER
+
+        lhs = self.version, ORDER.get(self.operator, 0)
+        rhs = other.version, ORDER.get(other.operator, 0)
+        return operator(lhs, rhs)
+
+    def __eq__(self, other):
+        return self.__compare(other, operator.eq)
+
+    def __lt__(self, other):
+        return self.__compare(other, operator.lt)
+
+    def __le__(self, other):
+        return self.__compare(other, operator.le)
+
+    def __gt__(self, other):
+        return self.__compare(other, operator.gt)
+
+    def __ge__(self, other):
+        return self.__compare(other, operator.ge)
+
+    @property
+    def upper(self):
+        """True if self is upper boundary."""
+        return self._OPERATOR_ORDER.get(self.operator, 0) > 0
+
+    @property
+    def lower(self):
+        """True if self is lower boundary."""
+        return self._OPERATOR_ORDER.get(self.operator, 0) < 0
+
+    @classmethod
+    def equal(cls, version):
+        """Normalize single samp:`={version}` into equivalent x-range::
+
+            >>> empty = VersionBoundary.equal(Version()); tuple(map(str, empty))
+            ()
+            >>> patch = VersionBoundary.equal(Version(1, 2, 3)); tuple(map(str, patch))
+            ('>=1.2.3', '<1.2.4')
+            >>> minor = VersionBoundary.equal(Version(1, 2)); tuple(map(str, minor))
+            ('>=1.2', '<1.3')
+            >>> major = VersionBoundary.equal(Version(1)); tuple(map(str, major))
+            ('>=1', '<2')
+
+        See `X-Ranges <https://docs.npmjs.com/misc/semver#x-ranges-12x-1x-12->`_
+        for details.
+
+        Arguments:
+            version (Version): The version the x-range should be equal to.
+
+        Returns:
+            (VersionBoundary, VersionBoundary):
+                Lower and upper bound of the x-range.
+            (): Empty tuple in case version is empty (any version matches).
+        """
+
+        if version:
+            return (
+                cls(version=version, operator=">="),
+                cls(version=version.incremented(), operator="<"),
+            )
+        else:
+            return ()
+
+    @classmethod
+    def tilde(cls, version):
+        """Normalize :samp:`~{version}` into equivalent range.
+
+        Tilde allows patch-level changes if a minor version is specified.
+        Allows minor-level changes if not::
+
+            >>> with_minor = VersionBoundary.tilde(Version(1, 2, 3)); tuple(map(str, with_minor))
+            ('>=1.2.3', '<1.3')
+            >>> no_minor = VersionBoundary.tilde(Version(1)); tuple(map(str, no_minor))
+            ('>=1', '<2')
+
+        Arguments:
+            version (Version): The version to tilde-expand.
 
-RE_VERSION = re.compile(r'\s*v?([<>=~^]{0,2})\s*([0-9][0-9\.\-]*)\s*')
+        Returns:
+            (VersionBoundary, VersionBoundary):
+                The lower and upper boundary of the tilde range.
+        """
 
-def main():
-    #npm2rpm uses functions here to write BuildRequires so don't print anything
-    #until the very end
-    deps = []
+        # Fail on ``~*`` or similar nonsense specifier
+        assert version.major is not None, "Nonsense '~*' specifier"
 
-    #it's highly unlikely that we'll ever get more than one file but we handle
-    #this like all RPM automatic dependency generation scripts anyway
-    paths = [path.rstrip() for path in sys.stdin.readlines()]
+        lower_boundary = cls(version=version, operator=">=")
 
-    for path in paths:
-        if not path.endswith('package.json'):
-            continue
+        if version.minor is None:
+            upper_boundary = cls(version=Version(version.major + 1), operator="<")
+        else:
+            upper_boundary = cls(
+                version=Version(version.major, version.minor + 1), operator="<"
+            )
+
+        return lower_boundary, upper_boundary
+
+    @classmethod
+    def caret(cls, version):
+        """Normalize :samp:`^{version}` into equivalent range.
+
+        Caret allows changes that do not modify the left-most non-zero digit
+        in the ``(major, minor, patch)`` tuple.
+        In other words, this allows
+        patch and minor updates for versions 1.0.0 and above,
+        patch updates for versions 0.X >=0.1.0,
+        and no updates for versions 0.0.X::
+
+            >>> major = VersionBoundary.caret(Version(1, 2, 3)); tuple(map(str, major))
+            ('>=1.2.3', '<2')
+            >>> minor = VersionBoundary.caret(Version(0, 2, 3)); tuple(map(str, minor))
+            ('>=0.2.3', '<0.3')
+            >>> patch = VersionBoundary.caret(Version(0, 0, 3)); tuple(map(str, patch))
+            ('>=0.0.3', '<0.0.4')
+
+        When parsing caret ranges, a missing patch value desugars to the number 0,
+        but will allow flexibility within that value,
+        even if the major and minor versions are both 0::
+
+            >>> rel = VersionBoundary.caret(Version(1, 2)); tuple(map(str, rel))
+            ('>=1.2', '<2')
+            >>> pre = VersionBoundary.caret(Version(0, 0)); tuple(map(str, pre))
+            ('>=0.0', '<0.1')
+
+        A missing minor and patch values will desugar to zero,
+        but also allow flexibility within those values,
+        even if the major version is zero::
+
+            >>> rel = VersionBoundary.caret(Version(1)); tuple(map(str, rel))
+            ('>=1', '<2')
+            >>> pre = VersionBoundary.caret(Version(0)); tuple(map(str, pre))
+            ('>=0', '<1')
+
+        Arguments:
+            version (Version): The version to range-expand.
+
+        Returns:
+            (VersionBoundary, VersionBoundary):
+                The lower and upper boundary of caret-range.
+        """
+
+        # Fail on ^* or similar nonsense specifier
+        assert len(version) != 0, "Nonsense '^*' specifier"
+
+        lower_boundary = cls(version=version, operator=">=")
+
+        # Increment left-most non-zero part
+        for idx, part in enumerate(version):
+            if part != 0:
+                upper_version = Version(*(version[:idx] + (part + 1,)))
+                break
+        else:  # No non-zero found; increment last specified part
+            upper_version = version.incremented()
+
+        upper_boundary = cls(version=upper_version, operator="<")
+
+        return lower_boundary, upper_boundary
+
+    @classmethod
+    def hyphen(cls, lower_version, upper_version):
+        """Construct hyphen range (inclusive set)::
+
+            >>> full = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3, 4)); tuple(map(str, full))
+            ('>=1.2.3', '<=2.3.4')
+
+        If a partial version is provided as the first version in the inclusive range,
+        then the missing pieces are treated as zeroes::
+
+            >>> part = VersionBoundary.hyphen(Version(1, 2), Version(2, 3, 4)); tuple(map(str, part))
+            ('>=1.2', '<=2.3.4')
+
+        If a partial version is provided as the second version in the inclusive range,
+        then all versions that start with the supplied parts of the tuple are accepted,
+        but nothing that would be greater than the provided tuple parts::
 
-        # we only want the package.json in the toplevel module directory
-        pathparts = path.split(os.sep)
-        if not pathparts[-5:-2] == ['usr', 'lib', 'node_modules']:
-            continue
-        
-        fh = open(path)
-        metadata = json.load(fh)
-        fh.close()
+            >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3)); tuple(map(str, part))
+            ('>=1.2.3', '<2.4')
+            >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2)); tuple(map(str, part))
+            ('>=1.2.3', '<3')
 
-        #write out the node.js interpreter dependency
-        req = 'rh-nodejs8-nodejs(engine)'
+        Arguments:
+            lower_version (Version): Version on the lower range boundary.
+            upper_version (Version): Version on the upper range boundary.
 
-        if 'engines' in metadata and isinstance(metadata['engines'], dict) \
-                                            and 'node' in metadata['engines']:
-            deps += process_dep(req, metadata['engines']['node'])
+        Returns:
+            (VersionBoundary, VersionBoundary):
+                Lower and upper boundaries of the hyphen range.
+        """
+
+        lower_boundary = cls(version=lower_version, operator=">=")
+
+        if len(upper_version) < 3:
+            upper_boundary = cls(version=upper_version.incremented(), operator="<")
         else:
-            deps.append(req)
-
-        if 'dependencies' in metadata:
-            if isinstance(metadata['dependencies'], dict):
-                for name, version in metadata['dependencies'].iteritems():
-                    req = 'rh-nodejs8-npm(' + name + ')'
-                    deps += process_dep(req, version)
-            elif isinstance(metadata['dependencies'], list):
-                for name in metadata['dependencies']:
-                    req = 'rh-nodejs8-npm(' + name + ')'
-                    deps.append(req)
-            elif isinstance(metadata['dependencies'], basestring):
-                req = 'rh-nodejs8-npm(' + metadata['dependencies'] + ')'
-                deps.append(req)
-            else:
-                raise TypeError('invalid package.json: dependencies not a valid type')
-
-    print '\n'.join(deps)
-    
-    # invoke the regular RPM requires generator                
-    p = subprocess.Popen(['/usr/lib/rpm/find-requires'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
-    print p.communicate(input='\n'.join(paths))[0]
-
-def process_dep(req, version):
-    """Converts an individual npm dependency into RPM dependencies"""
-    
-    deps = []
-    
-    #there's no way RPM can do anything like an OR dependency
-    if '||' in version:
-        sys.stderr.write("WARNING: The {0} dependency contains an ".format(req) +
-            "OR (||) dependency: '{0}.\nPlease manually include ".format(version) +
-            "a versioned dependency in your spec file if necessary")
-        deps.append(req)
-            
-    elif ' - ' in version:
-        gt, lt = version.split(' - ')
-        deps.append(req + ' >= ' + gt)
-        deps.append(req + ' <= ' + lt)
-        
+            upper_boundary = cls(version=upper_version, operator="<=")
+
+        return lower_boundary, upper_boundary
+
+
+def parse_simple_seq(specifier_string):
+    """Parse all specifiers from a space-separated string::
+
+        >>> single = parse_simple_seq(">=1.2.3"); list(map(str, single))
+        ['>=1.2.3']
+        >>> multi = parse_simple_seq("~1.2.0 <1.2.5"); list(map(str, multi))
+        ['>=1.2.0', '<1.3', '<1.2.5']
+
+    This method implements the ``simple (' ' simple)*`` part of the grammar:
+    https://docs.npmjs.com/misc/semver#range-grammar.
+
+    Arguments:
+        specifier_string (str): Space-separated string of simple version specifiers.
+
+    Yields:
+        VersionBoundary: Parsed boundaries.
+    """
+
+    # Per-operator dispatch table
+    # API: Callable[[Version], Iterable[VersionBoundary]]
+    handler = {
+        ">": lambda v: [VersionBoundary(version=v, operator=">")],
+        ">=": lambda v: [VersionBoundary(version=v, operator=">=")],
+        "<=": lambda v: [VersionBoundary(version=v, operator="<=")],
+        "<": lambda v: [VersionBoundary(version=v, operator="<")],
+        "=": VersionBoundary.equal,
+        "~": VersionBoundary.tilde,
+        "^": VersionBoundary.caret,
+        None: VersionBoundary.equal,
+    }
+
+    for match in RANGE_SPECIFIER_SIMPLE.finditer(specifier_string):
+        operator, version_string = match.group("operator", "version")
+
+        for boundary in handler[operator](Version.parse(version_string)):
+            yield boundary
+
+
+def parse_range(range_string):
+    """Parse full NPM version range specification::
+
+        >>> empty = parse_range(""); list(map(str, empty))
+        []
+        >>> simple = parse_range("^1.0"); list(map(str, simple))
+        ['>=1.0', '<2']
+        >>> hyphen = parse_range("1.0 - 2.0"); list(map(str, hyphen))
+        ['>=1.0', '<2.1']
+
+    This method implements the ``range`` part of the grammar:
+    https://docs.npmjs.com/misc/semver#range-grammar.
+
+    Arguments:
+        range_string (str): The range specification to parse.
+
+    Returns:
+        Iterable[VersionBoundary]: Parsed boundaries.
+
+    Raises:
+        UnsupportedVersionToken: ``||`` is present in range_string.
+    """
+
+    HYPHEN = " - "
+
+    # FIXME: rpm should be able to process OR in dependencies
+    # This error reporting kept for backward compatibility
+    if "||" in range_string:
+        raise UnsupportedVersionToken(range_string)
+
+    if HYPHEN in range_string:
+        version_pair = map(Version.parse, range_string.split(HYPHEN, 2))
+        return VersionBoundary.hyphen(*version_pair)
+
+    elif range_string != "":
+        return parse_simple_seq(range_string)
+
     else:
-        m = re.match(RE_VERSION, version)
+        return []
+
+
+def unify_range(boundary_iter):
+    """Calculate largest allowed continuous version range from a set of boundaries::
+
+        >>> unify_range([])
+        ()
+        >>> _ = unify_range(parse_range("=1.2.3 <2")); tuple(map(str, _))
+        ('>=1.2.3', '<1.2.4')
+        >>> _ = unify_range(parse_range("~1.2 <1.2.5")); tuple(map(str, _))
+        ('>=1.2', '<1.2.5')
 
-        if m:
-            deps += convert_dep(req, m.group(1), m.group(2))
+    Arguments:
+        boundary_iter (Iterable[VersionBoundary]): The version boundaries to unify.
 
-            #There could be up to two versions here (e.g.">1.0 <3.1")
-            if len(version) > m.end():
-                m = re.match(RE_VERSION, version[m.end():])
+    Returns:
+        (VersionBoundary, VersionBoundary):
+            Lower and upper boundary of the unified range.
+    """
 
-                if m:
-                    deps += convert_dep(req, m.group(1), m.group(2))
+    # Drop boundaries with empty version
+    boundary_iter = (
+        boundary for boundary in boundary_iter if not boundary.version.empty
+    )
+
+    # Split input sequence into upper/lower boundaries
+    lower_list, upper_list = [], []
+    for boundary in boundary_iter:
+        if boundary.lower:
+            lower_list.append(boundary)
+        elif boundary.upper:
+            upper_list.append(boundary)
         else:
-            deps.append(req)
+            msg = "Unsupported boundary for unify_range: {0}".format(boundary)
+            raise ValueError(msg)
+
+    # Select maximum from lower boundaries and minimum from upper boundaries
+    intermediate = (
+        max(lower_list) if lower_list else None,
+        min(upper_list) if upper_list else None,
+    )
+
+    return tuple(filter(None, intermediate))
+
+
+def rpm_format(requirement, version_spec="*"):
+    """Format requirement as RPM boolean dependency::
 
-    return deps
-            
-def convert_dep(req, operator, version):
-    """Converts one of the two possibly listed versions into an RPM dependency"""
-    
-    deps = []
+        >>> rpm_format("nodejs(engine)")
+        'nodejs(engine)'
+        >>> rpm_format("npm(foo)", ">=1.0.0")
+        'npm(foo) >= 1.0.0'
+        >>> rpm_format("npm(bar)", "~1.2")
+        '(npm(bar) >= 1.2 with npm(bar) < 1.3)'
 
-    #any version will do
-    if not version or version == '*':
-        deps.append(req)
+    Arguments:
+        requirement (str): The name of the requirement.
+        version_spec (str): The NPM version specification for the requirement.
 
-    #any prefix but ~ makes things dead simple
-    elif operator in ['>', '<', '<=', '>=', '=']:
-        deps.append(' '.join([req, operator, version]))
+    Returns:
+        str: Formatted requirement.
+    """
 
-    #oh boy, here we go...
+    TEMPLATE = "{name} {boundary.operator} {boundary.version!s}"
+
+    try:
+        boundary_tuple = unify_range(parse_range(version_spec))
+
+    except UnsupportedVersionToken:
+        # FIXME: Typos and print behavior kept for backward compatibility
+        warning_lines = [
+            "WARNING: The {requirement} dependency contains an OR (||) dependency: '{version_spec}.",
+            "Please manually include a versioned dependency in your spec file if necessary",
+        ]
+        warning = "\n".join(warning_lines).format(
+            requirement=requirement, version_spec=version_spec
+        )
+        print(warning, end="", file=sys.stderr)
+
+        return requirement
+
+    formatted = [
+        TEMPLATE.format(name=requirement, boundary=boundary)
+        for boundary in boundary_tuple
+    ]
+
+    if len(formatted) > 1:
+        return "({0})".format(" with ".join(formatted))
+    elif len(formatted) == 1:
+        return formatted[0]
     else:
-        #split the dotted portions into a list (handling trailing dots properly)
-        parts = [part if part else 'x' for part in version.split('.')]
-        parts = [int(part) if part != 'x' and not '-' in part
-                                                    else part for part in parts]
-
-        # 1 or 1.x or 1.x.x or ~1 or ^1
-        if len(parts) == 1 or parts[1] == 'x':
-            if parts[0] != 0:
-                deps.append('{0} >= {1}'.format(req, parts[0]))
-            deps.append('{0} < {1}'.format(req, parts[0]+1))
-
-        # 1.2.3 or 1.2.3-4 or 1.2.x or ~1.2.3 or ^1.2.3 or 1.2
-        elif len(parts) == 3 or operator != '~':
-            # 1.2.x or 1.2
-            if len(parts) == 2 or parts[2] == 'x':
-                deps.append('{0} >= {1}.{2}'.format(req, parts[0], parts[1]))
-                deps.append('{0} < {1}.{2}'.format(req, parts[0], parts[1]+1))
-            # ~1.2.3 or ^0.1.2 (zero is special with the caret operator)
-            elif operator == '~' or (operator == '^' and parts[0] == 0 and parts[1] > 0):
-                deps.append('{0} >= {1}'.format(req, version))
-                deps.append('{0} < {1}.{2}'.format(req, parts[0], parts[1]+1))
-            #^1.2.3
-            elif operator == '^' and parts[0:1] != [0,0]:
-                deps.append('{0} >= {1}'.format(req, version))
-                deps.append('{0} < {1}'.format(req, parts[0]+1))
-            # 1.2.3 or 1.2.3-4 or ^0.0.3
-            else:
-                deps.append('{0} = {1}'.format(req, version))
-
-        # ~1.2
-        elif operator == '~':
-            deps.append('{0} >= {1}'.format(req, version))
-            deps.append('{0} < {1}'.format(req, parts[0]+1))
-        
-        #^1.2
-        elif operator == '^':
-            deps.append('{0} >= {1}'.format(req, version))
-            deps.append('{0} < {1}'.format(req, parts[0]+1))
-            
-
-    return deps
-            
-if __name__ == '__main__':
-    main()
+        return requirement
+
+
+def has_only_bundled_dependencies(module_dir_path):
+    """Determines if the module contains only bundled dependencies.
+
+    Dependencies are considered un-bundled when they are symlinks
+    pointing outside the root module's tree.
+
+    Arguments:
+        module_dir_path (str):
+            Path to the module directory (directory with ``package.json``).
+
+    Returns:
+        bool: True if all dependencies are bundled, False otherwise.
+    """
+
+    module_root_path = os.path.abspath(module_dir_path)
+    dependency_root_path = os.path.join(module_root_path, "node_modules")
+
+    try:
+        dependency_path_iter = (
+            os.path.join(dependency_root_path, basename)
+            for basename in os.listdir(dependency_root_path)
+        )
+        linked_dependency_iter = (
+            os.path.realpath(path)
+            for path in dependency_path_iter
+            if os.path.islink(path)
+        )
+        outside_dependency_iter = (
+            path
+            for path in linked_dependency_iter
+            if not path.startswith(module_root_path)
+        )
+
+        return not any(outside_dependency_iter)
+    except OSError:  # node_modules does not exist
+        return False
+
+
+def extract_dependencies(metadata_path, optional=False):
+    """Extract all dependencies in RPM format from package metadata.
+
+    Arguments:
+        metadata_path (str): Path to package metadata (``package.json``).
+        optional (bool):
+            If True, extract ``optionalDependencies``
+            instead of ``dependencies``.
+
+    Yields:
+        RPM-formatted dependencies.
+
+    Raises:
+        TypeError: Invalid dependency data type.
+    """
+
+    if has_only_bundled_dependencies(os.path.dirname(metadata_path)):
+        return  # skip
+
+    # Read metadata
+    try:
+        with open(metadata_path, mode="r") as metadata_file:
+            metadata = json.load(metadata_file)
+    except OSError:  # Invalid metadata file
+        return  # skip
+
+    # Report required NodeJS version with required dependencies
+    if not optional:
+        try:
+            yield rpm_format("nodejs(engine)", metadata["engines"]["node"])
+        except KeyError:  # NodeJS engine version unspecified
+            yield rpm_format("nodejs(engine)")
+
+    # Report listed dependencies
+    kind = "optionalDependencies" if optional else "dependencies"
+    container = metadata.get(kind, {})
+
+    if isinstance(container, dict):
+        for name, version_spec in container.items():
+            yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name), version_spec)
+
+    elif isinstance(container, list):
+        for name in container:
+            yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name))
+
+    elif isinstance(container, str):
+        yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name))
+
+    else:
+        raise TypeError("invalid package.json: dependencies not a valid type")
+
+
+if __name__ == "__main__":
+    nested = (
+        extract_dependencies(path.strip(), optional="--optional" in sys.argv)
+        for path in sys.stdin
+    )
+    flat = chain.from_iterable(nested)
+    # Ignore parentheses around the requirements when sorting
+    ordered = sorted(flat, key=lambda s: s.strip("()"))
+
+    print(*ordered, sep="\n")
diff --git a/SOURCES/nodejs_native.attr b/SOURCES/nodejs_native.attr
index 0527af6..26ed6f2 100644
--- a/SOURCES/nodejs_native.attr
+++ b/SOURCES/nodejs_native.attr
@@ -1,2 +1,2 @@
-%__nodejs_native_requires %{_rpmconfigdir}/nodejs_native.req
-%__nodejs_native_path ^/usr/lib.*/node_modules/.*\\.node$
+%__rh_nodejs8_native_requires %{_rpmconfigdir}/rh_nodejs8_native.req
+%__rh_nodejs8_native_path ^(/opt/rh/rh-nodejs8/root)?/usr/lib.*/node_modules/.*\\.node$
diff --git a/SPECS/rh-nodejs8.spec b/SPECS/rh-nodejs8.spec
index f0fabad..a0e3ca5 100644
--- a/SPECS/rh-nodejs8.spec
+++ b/SPECS/rh-nodejs8.spec
@@ -2,19 +2,21 @@
 %global scl_name_prefix rh-
 %global scl_name_base nodejs
 %global scl_name_version 8
- 
+
 %global scl %{scl_name_prefix}%{scl_name_base}%{scl_name_version}
 
 %scl_package %scl
 %global install_scl 1
 
+%global rpm_magic_name %{lua: name, _ = string.gsub(rpm.expand("%{scl_name}"), "-", "_"); print(name)}
+
 # do not produce empty debuginfo package
 %global debug_package %{nil}
 
 Summary: %scl Software Collection
 Name: %scl_name
 Version: 3.0
-Release: 3%{?dist}
+Release: 5%{?dist}
 
 Source1: macros.nodejs
 Source2: nodejs.attr
@@ -58,7 +60,7 @@ Package shipping essential configuration macros to build %scl Software Collectio
 
 %package scldevel
 Summary: Package shipping development files for %scl
- 
+
 %description scldevel
 Package shipping development files, especially usefull for development of
 packages depending on %scl Software Collection.
@@ -101,12 +103,12 @@ EOF
 
 # install rpm magic
 install -Dpm0644 %{SOURCE1} %{buildroot}%{_root_sysconfdir}/rpm/macros.%{name}
-install -Dpm0644 %{SOURCE2} %{buildroot}%{_rpmconfigdir}/fileattrs/%{name}.attr
-install -pm0755 %{SOURCE3} %{buildroot}%{_rpmconfigdir}/%{name}.prov
-install -pm0755 %{SOURCE4} %{buildroot}%{_rpmconfigdir}/%{name}.req
+install -Dpm0644 %{SOURCE2} %{buildroot}%{_rpmconfigdir}/fileattrs/%{rpm_magic_name}.attr
+install -pm0755 %{SOURCE3} %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}.prov
+install -pm0755 %{SOURCE4} %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}.req
 install -pm0755 %{SOURCE5} %{buildroot}%{_rpmconfigdir}/%{name}-symlink-deps
 install -pm0755 %{SOURCE6} %{buildroot}%{_rpmconfigdir}/%{name}-fixdep
-install -Dpm0644 %{SOURCE7} %{buildroot}%{_rpmconfigdir}/fileattrs/%{name}_native.attr
+install -Dpm0644 %{SOURCE7} %{buildroot}%{_rpmconfigdir}/fileattrs/%{rpm_magic_name}_native.attr
 install -Dpm0644 %{SOURCE10} %{buildroot}%{_datadir}/node/multiver_modules
 
 cat >> %{buildroot}%{_root_sysconfdir}/rpm/macros.%{scl_name_base}-scldevel << EOF
@@ -116,12 +118,12 @@ EOF
 
 # ensure Requires are added to every native module that match the Provides from
 # the nodejs build in the buildroot
-cat << EOF > %{buildroot}%{_rpmconfigdir}/%{name}_native.req
+cat << EOF > %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}_native.req
 #!/bin/sh
-echo 'nodejs6-nodejs(abi) = %nodejs_abi'
-echo 'nodejs6-nodejs(v8-abi) = %v8_abi'
+echo 'nodejs8-nodejs(abi) = %nodejs_abi'
+echo 'nodejs8-nodejs(v8-abi) = %v8_abi'
 EOF
-chmod 0755 %{buildroot}%{_rpmconfigdir}/%{name}_native.req
+chmod 0755 %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}_native.req
 
 cat << EOF > %{buildroot}%{_rpmconfigdir}/%{name}-require.sh
 #!/bin/sh
@@ -149,6 +151,19 @@ install -m 644 %{scl_name}.7 %{buildroot}%{_mandir}/man7/%{scl_name}.7
 # own license dir (RHBZ#1420294)
 mkdir -p %{buildroot}%{_datadir}/licenses/
 
+%check
+# Assert that installed file attributes are named appropriately
+for attr_file in %{buildroot}%{_rpmconfigdir}/fileattrs/*.attr; do
+    macro_name="$(basename "$attr_file"|sed -E 's|\.attr$||')"
+
+    # Delete comments and empty lines
+    # Select all remaining lines that start unexpectedly
+    # Fail if any such line is found, cotinue otherwise
+    sed -E -e '/^$/d;/^#/d' "$attr_file" \
+    | grep -E -v "^%%__${macro_name}_" \
+    && exit 1 || :
+done
+
 %files
 
 %files -f filesystem runtime
@@ -156,8 +171,8 @@ mkdir -p %{buildroot}%{_datadir}/licenses/
 %{!?_licensedir:%global license %%doc}
 %license LICENSE
 %scl_files
-%dir %{_scl_root}%{python_sitelib}
-%dir %{_scl_root}/usr/lib/python2.7
+#%dir %{_scl_root}%{python_sitelib}
+%dir %{_prefix}/lib/python2.*
 %dir %{_libdir}/pkgconfig
 %dir %{_datadir}/licenses
 %{_datadir}/node/multiver_modules
@@ -167,13 +182,20 @@ mkdir -p %{buildroot}%{_datadir}/licenses/
 %files build
 %{_root_sysconfdir}/rpm/macros.%{scl}-config
 %{_root_sysconfdir}/rpm/macros.%{name}
-%{_rpmconfigdir}/fileattrs/%{name}*.attr
+%{_rpmconfigdir}/fileattrs/%{rpm_magic_name}*.attr
 %{_rpmconfigdir}/%{name}*
+%{_rpmconfigdir}/%{rpm_magic_name}*
 
 %files scldevel
 %{_root_sysconfdir}/rpm/macros.%{scl_name_base}-scldevel
 
 %changelog
+* Fri Sep 06 2019 Jan Staněk <jstanek@redhat.com> - 3.0-5
+- Import enhanced Provides/Requires generators from Fedora
+
+* Thu Jul 13 2017 Zuzana Svetlikova <zsvetlik@redhat.com> - 3.0-4
+- Use macro for python sitelib in doc
+
 * Mon Jul 03 2017 Zuzana Svetlikova <zsvetlik@redhat.com> - 3.0-3
 - Fix typo in symlink script
 
@@ -232,7 +254,7 @@ mkdir -p %{buildroot}%{_datadir}/licenses/
 - Own python modules directory
 
 * Wed Oct 08 2014 Tomas Hrcka <thrcka@redhat.com> - 1.2-29
-- Require scriptlet scl_devel from root_bindir not scl_bindir 
+- Require scriptlet scl_devel from root_bindir not scl_bindir
 
 * Mon Oct 06 2014 Tomas Hrcka <thrcka@redhat.com> - 1.2-28
 - bump scl version
@@ -249,10 +271,10 @@ mkdir -p %{buildroot}%{_datadir}/licenses/
   Related: #1061452
 
 * Mon Feb 17 2014 Tomas Hrcka <thrcka@redhat.com> - 1.1-24
-- Require version of scl-utils 
+- Require version of scl-utils
 
 * Wed Feb 12 2014 Tomas Hrcka <thrcka@redhat.com> - 1.1-23
-- Define scl_name_base and scl_name_version macros 
+- Define scl_name_base and scl_name_version macros
 
 * Wed Feb 12 2014 Honza Horak <hhorak@redhat.com> - 1.1-22
 - Some more grammar fixes in README
@@ -275,7 +297,7 @@ mkdir -p %{buildroot}%{_datadir}/licenses/
 - clean up after previous fix
 
 * Fri Aug 09 2013 thrcka@redhat.com - 1-16
-- RHBZ#993425 - nodejs010.req fails when !noarch 
+- RHBZ#993425 - nodejs010.req fails when !noarch
 
 * Mon Jun 03 2013 thrcka@redhat.com - 1-15
 - Changed licence to MIT