diff --git a/djblets/pipeline/compilers/rollup.py b/djblets/pipeline/compilers/rollup.py
new file mode 100644
index 0000000000000000000000000000000000000000..df571b0b1f487456b730bd925624b737a3426bb1
--- /dev/null
+++ b/djblets/pipeline/compilers/rollup.py
@@ -0,0 +1,207 @@
+"""Pipeline compiler for bundling modern JavaScript with rollup.js.
+
+Version Added:
+    4.0
+"""
+
+import os
+import re
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+from pipeline.conf import settings
+from pipeline.compilers import SubProcessCompiler
+
+from djblets.pipeline.compilers.mixins import SourceMapStaleCheckMixin
+
+
+#: The default regular expression used for rollup.js index files.
+#:
+#: Version Added:
+#:     4.0
+DEFAULT_FILE_PATTERN = r'index\.(js|es6|es6\.js|ts)$'
+
+
+_match_regex = re.compile(settings.get('ROLLUP_FILE_PATTERN') or
+                          DEFAULT_FILE_PATTERN)
+
+
+class RollupCompiler(SourceMapStaleCheckMixin, SubProcessCompiler):
+    """A Pipeline compiler for interfacing with rollup.js.
+
+    `rollup.js <https://rollupjs.org/>`_ is a module bundler for JavaScript
+    that compiles down a JavaScript codebase, linked together through module
+    imports (ES6, CommonJS etc.), into a single JavaScript file. It can manage
+    compilation with Babel or TypeScript, and supports a wide variety of
+    plugins.
+
+    This compiler makes it easy to develop modern JavaScript while still
+    taking advantage of the best of Pipeline, such as managing separate
+    bundles, loading bundles into Django templates, and triggering automatic
+    recompilation on demand if any files (including those imported via modules)
+    in the bundle are stale.
+
+    To use this compiler, you will need an ``index`` file in your Pipeline
+    JavaScript bundle, which imports any other modules that should be part
+    of that compiled rollup.js bundle. An ``index`` file can be in the
+    following forms:
+
+    * :file:`index.js`
+    * :file:`index.es6`
+    * :file:`index.es6.js`
+    * :file:`index.ts`
+
+    Custom filename patterns can be specified by setting
+    ``settings.PIPELINE['ROLLUP_FILE_PATTERN']`` to a regular expression string
+    (defaults to :py:data:`DEFAULT_FILE_PATTERN`).
+
+    Your Pipeline bundle may consist of just this one ``index`` file, or it
+    may include other JavaScript as well. Each ``index`` will be compiled as
+    its own rollup.js bundle, and its compiled contents included as part of the
+    Pipeline bundle.
+
+    Sourcemaps are used to check if any parts of the bundle are stale,
+    triggering automatic recompilation when loading pages. Please note that
+    any modules that you want included but are not actively being used within
+    that rollup.js bundle may be excluded from automatic recompilation
+    detection if you have treeshaking enabled (which is enabled by default).
+
+    You will need to set ``settings.PIPELINE['ROLLUP_BINARY']`` to the path
+    of :file:`rollup` and then set any command line arguments needed
+    (such as a path to your configuration file) in
+    ``settings.PIPELINE['ROLLUP_ARGUMENTS']``.
+
+    The arguments should *not* include ``-c`` / ``--config`` to specify the
+    ``rollup.config.js`` path. This will be computed automatically, in order
+    to ensure the right file is used based on whichever source tree may be
+    hosting the input file (such as when a project is consuming another
+    project's source files and compiling them).
+
+    Version Added:
+        4.0
+    """
+
+    output_extension = 'js'
+
+    def match_file(
+        self,
+        path: str,
+    ) -> bool:
+        """Return whether this compiler matches a source file.
+
+        This will look for the following filenames by default:
+
+        * :file:`index.js`
+        * :file:`index.es6`
+        * :file:`index.es6.js`
+        * :file:`index.ts`
+
+        To customize this, set ``settings.PIPELINE['ROLLUP_FILE_PATTERN']``
+        to a regular expression string.
+
+        Args:
+            path (str):
+                The path to the source file being considered.
+
+        Returns:
+            bool:
+            ``True`` if this compiler will invoke rollup.js on this file.
+            ``False`` if it will not.
+        """
+        return _match_regex.match(os.path.basename(path)) is not None
+
+    def compile_file(
+        self,
+        infile: str,
+        outfile: str,
+        outdated: bool = False,
+        force: bool = False,
+    ) -> None:
+        """Compile a file using rollup.js.
+
+        This will cause the file and anything it imports in the tree into
+        a rollup.js bundle, for inclusion in the Pipeline bundle.
+
+        Args:
+            infile (str):
+                The source file.
+
+            outfile (str):
+                The destination file.
+
+            outdated (bool, optional):
+                Whether the destination file is known to be outdated.
+
+            force (bool, optional):
+                Whether to force re-running rollup.js on this file.
+        """
+        if outdated or force:
+            # Look for the root of a tree, containing any of a number of
+            # build configuration files.
+            tree_root, rollup_config_path = \
+                self._find_tree_root(os.path.dirname(infile))
+
+            args: List[str] = [
+                settings.ROLLUP_BINARY,
+            ]
+
+            if rollup_config_path:
+                # We found a rollup.config.js. Make sure we tell rollup.js to
+                # use it explicitly.
+                args += [
+                    '--bundleConfigAsCjs',
+                    '-c',
+                    rollup_config_path,
+                ]
+
+            args += [
+                settings.ROLLUP_ARGUMENTS,
+                '--sourcemap',
+                '-i',
+                infile,
+                '-o',
+                outfile,
+            ]
+
+            self.execute_command(args, cwd=tree_root)
+
+    def _find_tree_root(
+        self,
+        start_dir: str,
+    ) -> Tuple[Optional[str], Optional[str]]:
+        """Return the root of a source tree for an input file.
+
+        This will scan up the tree, looking for a :file:`rollup.config.js` or
+        :file:`.babelrc`. This is used to try to find the proper working
+        directory needed to successfully compile a file.
+
+        This is important when a project is consuming and building another
+        project's static media, so that the consumed project's configuration
+        is applied.
+
+        Args:
+            start_dir (str):
+                The starting directory for the search.
+
+        Returns:
+            str:
+            The root of the source tree, or ``None`` if it could not be found.
+        """
+        path = Path(start_dir)
+        root = path.root
+
+        while path != root:
+            rollup_config_path = Path(path / 'rollup.config.js')
+
+            if rollup_config_path.exists():
+                # This is the ideal result. We found the top-level of the
+                # tree and the rollup.config.js file.
+                return str(path), str(rollup_config_path)
+            elif Path(path / '.babelrc').exists():
+                # We didn't find rollup.config.js, but we found .babelrc.
+                # Consider this the top of the tree.
+                return str(path), None
+
+            path = path.parent
+
+        return None, None
diff --git a/djblets/pipeline/settings.py b/djblets/pipeline/settings.py
index 1926426f430f622a4862a4381986fb66828991ed..7281d43bd1d471d6c9df77c130aa16af1a3f114f 100644
--- a/djblets/pipeline/settings.py
+++ b/djblets/pipeline/settings.py
@@ -9,35 +9,46 @@ Version Added:
 """
 
 import os
+from typing import Dict, List
 
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.encoding import force_str
+from djblets.deprecation import (RemovedInDjblets50Warning,
+                                 deprecate_non_keyword_only_args)
 
 
 #: Default list of compilers used by Djblets.
-DEFAULT_PIPELINE_COMPILERS = [
+DEFAULT_PIPELINE_COMPILERS: List[str] = [
     'djblets.pipeline.compilers.es6.ES6Compiler',
     'djblets.pipeline.compilers.typescript.TypeScriptCompiler',
     'djblets.pipeline.compilers.less.LessCompiler',
 ]
 
 
-def build_pipeline_settings(pipeline_enabled,
-                            node_modules_path,
-                            static_root,
-                            javascript_bundles=[],
-                            stylesheet_bundles=[],
-                            compilers=DEFAULT_PIPELINE_COMPILERS,
-                            babel_extra_plugins=[],
-                            babel_extra_args=[],
-                            less_extra_args=[],
-                            validate_paths=True):
+@deprecate_non_keyword_only_args(RemovedInDjblets50Warning)
+def build_pipeline_settings(
+    *,
+    pipeline_enabled: bool,
+    node_modules_path: str,
+    static_root: str,
+    javascript_bundles: Dict = {},
+    stylesheet_bundles: Dict = {},
+    compilers: List[str] = DEFAULT_PIPELINE_COMPILERS,
+    babel_extra_plugins: List[str] = [],
+    babel_extra_args: List[str] = [],
+    less_extra_args: List[str] = [],
+    validate_paths: bool = True,
+    use_rollup: bool = True,
+    rollup_extra_args: List[str] = [],
+    extra_config: Dict = {},
+) -> Dict:
     """Build a standard set of Pipeline settings.
 
     This can be used to create a ``PIPELINE`` settings dictionary in a
-    :file:`settings.py` file based on the standard Djblets Pipeline settings,
-    which makes use of Babel, LessCSS, and UglifyJS, along with a preset
-    list of plugins.
+    :file:`settings.py` file based on the standard Djblets Pipeline settings.
+
+    By default, this makes use of Babel, LessCSS, and UglifyJS, along with a
+    preset list of plugins.
 
     The following base set of Babel plugins are used:
 
@@ -50,8 +61,32 @@ def build_pipeline_settings(pipeline_enabled,
     * `autoprefix
       <https://www.npmjs.com/package/@beanbag/less-plugin-autoprefix>`_
 
-    This will also set the value of ``node_modules_path`` in the
-    :envvar:`NODE_PATH` environment variable.
+    Optionally, `rollup.js <https://www.rollupjs.org>` can be used by
+    setting ``use_rollup=True``. This will make use of
+    :py:class:`djblets.pipeline.rollup.RollupCompiler`, configuring it to
+    automatically compile any of the following files:
+
+    * :file:`index.js`
+    * :file:`index.es6`,
+    * :file:`index.es6.js`
+    * :file:`index.ts`
+
+    These modules can make use of modern JavaScript ``import``/``export``
+    statements. Any relatively-imported modules will be rolled up during
+    compilation for the ``index`` file.
+
+    Note:
+        These files should **not** be specified as part of the Pipeline
+        bundle! Rollup will instead bundle them into the compiled index file.
+
+    As a convenience, this function will also set the value of
+    ``node_modules_path`` in the :envvar:`NODE_PATH` environment variable.
+
+    Version Changed:
+        4.0:
+        * Added support for `rollup.js <https://www.rollupjs.org>`.
+        * Added ``extra_config``, ``use_rollup`, and ``rollup_extra_args``
+          parameters.
 
     Args:
         pipeline_enabled (bool):
@@ -101,29 +136,36 @@ def build_pipeline_settings(pipeline_enabled,
     bin_path = os.path.join(node_modules_path, '.bin')
     babel_bin_path = os.path.join(bin_path, 'babel')
     lessc_bin_path = os.path.join(bin_path, 'lessc')
+    rollup_bin_path = os.path.join(bin_path, 'rollup')
     uglifyjs_bin_path = os.path.join(bin_path, 'uglifyjs')
 
+    use_lessc = (
+        'djblets.pipeline.compilers.less.LessCompiler' in compilers or
+        'pipeline.compilers.less.LessCompiler' in compilers
+    )
+
+    if use_rollup:
+        # Make sure that rollup is before any other JavaScript compilers.
+        compilers = [
+            'djblets.pipeline.compilers.rollup.RollupCompiler',
+        ] + compilers
+
     if (validate_paths and
         os.environ.get('DJBLETS_SKIP_PIPELINE_VALIDATION') != '1'):
+        # Validate that the necessary dependencies exist for Pipeline.
         if not os.path.exists(node_modules_path):
             raise ImproperlyConfigured(
                 'node_modules could not be found at %s'
                 % node_modules_path)
 
-        if not os.path.exists(babel_bin_path):
-            raise ImproperlyConfigured(
-                'The babel binary could not be found at %s'
-                % babel_bin_path)
-
-        if not os.path.exists(lessc_bin_path):
-            raise ImproperlyConfigured(
-                'The lessc binary could not be found at %s'
-                % lessc_bin_path)
-
-        if not os.path.exists(uglifyjs_bin_path):
-            raise ImproperlyConfigured(
-                'The uglifyjs binary could not be found at %s'
-                % uglifyjs_bin_path)
+        for binary_path, check in ((babel_bin_path, True),
+                                   (uglifyjs_bin_path, True),
+                                   (lessc_bin_path, use_lessc),
+                                   (rollup_bin_path, use_rollup)):
+            if not os.path.exists(binary_path):
+                raise ImproperlyConfigured(
+                    'The "%s" binary could not be found at %s'
+                    % (os.path.basename(binary_path), binary_path))
 
     os.environ[str('NODE_PATH')] = force_str(node_modules_path)
 
@@ -132,7 +174,7 @@ def build_pipeline_settings(pipeline_enabled,
         'django-gettext',
     ] + babel_extra_plugins
 
-    return {
+    config = {
         'PIPELINE_ENABLED': bool(pipeline_enabled),
         'COMPILERS': compilers,
         'CSS_COMPRESSOR': None,
@@ -145,13 +187,27 @@ def build_pipeline_settings(pipeline_enabled,
             '--plugins', ','.join(babel_plugins),
             '-s', 'true',
         ] + babel_extra_args,
-        'LESS_BINARY': lessc_bin_path,
-        'LESS_ARGUMENTS': [
-            '--include-path=%s' % static_root,
-            '--no-color',
-            '--source-map',
-            '--js',
-            '--plugin=@beanbag/less-plugin-autoprefix',
-        ] + less_extra_args,
         'UGLIFYJS_BINARY': uglifyjs_bin_path,
     }
+
+    if use_lessc:
+        config.update({
+            'LESS_BINARY': lessc_bin_path,
+            'LESS_ARGUMENTS': [
+                '--include-path=%s' % static_root,
+                '--no-color',
+                '--source-map',
+                '--js',
+                '--plugin=@beanbag/less-plugin-autoprefix',
+            ] + less_extra_args,
+        })
+
+    if use_rollup:
+        config.update({
+            'ROLLUP_ARGUMENTS': rollup_extra_args,
+            'ROLLUP_BINARY': rollup_bin_path,
+        })
+
+    config.update(extra_config)
+
+    return config
diff --git a/docs/djblets/coderef/index.rst b/docs/djblets/coderef/index.rst
index 73aa4d9df429fb363e638788dfc88bccf61fba8d..3426feea427fb7ab80e327e3c6f3b765edc434ae 100644
--- a/docs/djblets/coderef/index.rst
+++ b/docs/djblets/coderef/index.rst
@@ -319,8 +319,10 @@ Django Pipeline Additions
 .. autosummary::
    :toctree: python
 
-   djblets.pipeline.compilers.es6.ES6Compiler
-   djblets.pipeline.compilers.less.LessCompiler
+   djblets.pipeline.compilers.es6
+   djblets.pipeline.compilers.less
+   djblets.pipeline.compilers.mixins
+   djblets.pipeline.compilers.rollup
    djblets.pipeline.settings
 
 
