diff --git a/CHANGELOG.md b/CHANGELOG.md
index f67c9639..8d613134 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Release notes
+## v0.132
+
+#### Feat
+
+- Allow to use glob patterns as paths for additional JS / CSS in
+ `Component.Media.js` and `Component.Media.css`
+
+ ```py
+ class MyComponent(Component):
+ class Media:
+ js = ["*.js"]
+ css = ["*.css"]
+ ```
+
## v0.131
#### Feat
diff --git a/docs/concepts/fundamentals/defining_js_css_html_files.md b/docs/concepts/fundamentals/defining_js_css_html_files.md
index b5da3535..24f754e5 100644
--- a/docs/concepts/fundamentals/defining_js_css_html_files.md
+++ b/docs/concepts/fundamentals/defining_js_css_html_files.md
@@ -143,17 +143,22 @@ However, there's a few differences from Django's Media class:
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
+3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
+4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
+ it should be a list of [`Component`](../../../reference/api/#django_components.Component) classes.
```py
class MyTable(Component):
class Media:
js = [
"path/to/script.js",
+ "path/to/*.js", # Or as a glob
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
]
css = {
"all": [
"path/to/style.css",
+ "path/to/*.css", # Or as a glob
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
],
"print": ["path/to/style2.css"],
diff --git a/docs/getting_started/adding_js_and_css.md b/docs/getting_started/adding_js_and_css.md
index 6635c173..0b097935 100644
--- a/docs/getting_started/adding_js_and_css.md
+++ b/docs/getting_started/adding_js_and_css.md
@@ -230,7 +230,9 @@ with a few differences:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, [`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
-3. If you set `Media.extend` to a list, it should be a list of `Component` classes.
+3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
+4. If you set [`Media.extend`](../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
+ it should be a list of [`Component`](../../reference/api/#django_components.Component) classes.
[Learn more](../../concepts/fundamentals/defining_js_css_html_files) about using Media.
@@ -245,10 +247,12 @@ class Calendar(Component):
class Media: # <--- new
js = [
"path/to/shared.js",
+ "path/to/*.js", # Or as a glob
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
]
css = [
"path/to/shared.css",
+ "path/to/*.css", # Or as a glob
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # Tailwind
]
diff --git a/src/django_components/component_media.py b/src/django_components/component_media.py
index 0f5d22f6..2070dc95 100644
--- a/src/django_components/component_media.py
+++ b/src/django_components/component_media.py
@@ -1,10 +1,26 @@
+import glob
import os
import sys
from collections import deque
from copy import copy
from dataclasses import dataclass
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ Literal,
+ Optional,
+ Protocol,
+ Sequence,
+ Tuple,
+ Type,
+ Union,
+ cast,
+)
from weakref import WeakKeyDictionary
from django.contrib.staticfiles import finders
@@ -16,7 +32,7 @@ from django.utils.safestring import SafeData
from django_components.util.loader import get_component_dirs, resolve_file
from django_components.util.logger import logger
-from django_components.util.misc import get_import_path
+from django_components.util.misc import flatten, get_import_path, get_module_info, is_glob
if TYPE_CHECKING:
from django_components.component import Component
@@ -649,19 +665,19 @@ def _normalize_media(media: Type[ComponentMediaInput]) -> None:
_map_media_filepaths(media, _normalize_media_filepath)
-def _map_media_filepaths(media: Type[ComponentMediaInput], map_fn: Callable[[Any], Any]) -> None:
+def _map_media_filepaths(media: Type[ComponentMediaInput], map_fn: Callable[[Sequence[Any]], Iterable[Any]]) -> None:
if hasattr(media, "css") and media.css:
if not isinstance(media.css, dict):
raise ValueError(f"Media.css must be a dict, got {type(media.css)}")
for media_type, path_list in media.css.items():
- media.css[media_type] = list(map(map_fn, path_list)) # type: ignore[assignment]
+ media.css[media_type] = list(map_fn(path_list)) # type: ignore[assignment]
if hasattr(media, "js") and media.js:
if not isinstance(media.js, (list, tuple)):
raise ValueError(f"Media.css must be a list, got {type(media.css)}")
- media.js = list(map(map_fn, media.js))
+ media.js = list(map_fn(media.js))
def _is_media_filepath(filepath: Any) -> bool:
@@ -683,26 +699,33 @@ def _is_media_filepath(filepath: Any) -> bool:
return False
-def _normalize_media_filepath(filepath: ComponentMediaInputPath) -> Union[str, SafeData]:
- if callable(filepath):
- filepath = filepath()
+def _normalize_media_filepath(filepaths: Sequence[ComponentMediaInputPath]) -> List[Union[str, SafeData]]:
+ normalized: List[Union[str, SafeData]] = []
+ for filepath in filepaths:
+ if callable(filepath):
+ filepath = filepath()
- if isinstance(filepath, SafeData) or hasattr(filepath, "__html__"):
- return filepath
+ if isinstance(filepath, SafeData) or hasattr(filepath, "__html__"):
+ normalized.append(filepath)
+ continue
- if isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"):
- # In case of Windows OS, convert to forward slashes
- filepath = Path(filepath.__fspath__()).as_posix()
+ if isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"):
+ # In case of Windows OS, convert to forward slashes
+ filepath = Path(filepath.__fspath__()).as_posix()
- if isinstance(filepath, bytes):
- filepath = filepath.decode("utf-8")
+ if isinstance(filepath, bytes):
+ filepath = filepath.decode("utf-8")
- if isinstance(filepath, str):
- return filepath
+ if isinstance(filepath, str):
+ normalized.append(filepath)
+ continue
- raise ValueError(
- "Unknown filepath. Must be str, bytes, PathLike, SafeString, or a function that returns one of the former"
- )
+ raise ValueError(
+ f"Unknown filepath {filepath} of type {type(filepath)}. Must be str, bytes, PathLike, SafeString,"
+ " or a function that returns one of the former"
+ )
+
+ return normalized
def _resolve_component_relative_files(
@@ -730,12 +753,9 @@ def _resolve_component_relative_files(
return
component_name = comp_cls.__qualname__
- # Derive the full path of the file where the component was defined
- module_name = comp_cls.__module__
- module_obj = sys.modules[module_name]
- file_path = module_obj.__file__
-
- if not file_path:
+ # Get the full path of the file where the component was defined
+ module, module_name, module_file_path = get_module_info(comp_cls)
+ if not module_file_path:
logger.debug(
f"Could not resolve the path to the file for component '{component_name}'."
" Paths for HTML, JS or CSS templates will NOT be resolved relative to the component file."
@@ -743,83 +763,147 @@ def _resolve_component_relative_files(
return
# Get the directory where the component class is defined
- try:
- comp_dir_abs, comp_dir_rel = _get_dir_path_from_component_path(file_path, comp_dirs)
- except RuntimeError:
- # If no dir was found, we assume that the path is NOT relative to the component dir
+ matched_component_dir = _find_component_dir_containing_file(comp_dirs, module_file_path)
+
+ # If no dir was found (e.g. the component was defined at runtime), we assume that the media paths
+ # are NOT relative.
+ if matched_component_dir is None:
logger.debug(
- f"No component directory found for component '{component_name}' in {file_path}"
+ f"No component directory found for component '{component_name}' in {module_file_path}"
" If this component defines HTML, JS or CSS templates relatively to the component file,"
" then check that the component's directory is accessible from one of the paths"
" specified in the Django's 'COMPONENTS.dirs' settings."
)
return
+ matched_component_dir_abs = os.path.abspath(matched_component_dir)
+ # Derive the path from matched `COMPONENTS.dirs` to the dir where the current component file is.
+ component_module_dir_path_abs = os.path.dirname(module_file_path)
+
# Check if filepath refers to a file that's in the same directory as the component class.
# If yes, modify the path to refer to the relative file.
# If not, don't modify anything.
- def resolve_media_file(filepath: Union[str, SafeData]) -> Union[str, SafeData]:
- if isinstance(filepath, str):
- filepath_abs = os.path.join(comp_dir_abs, filepath)
- # NOTE: The paths to resources need to use POSIX (forward slashes) for Django to wor
- # See https://github.com/django-components/django-components/issues/796
- filepath_rel_to_comp_dir = Path(os.path.join(comp_dir_rel, filepath)).as_posix()
-
- if os.path.isfile(filepath_abs):
- # NOTE: It's important to use `repr`, so we don't trigger __str__ on SafeStrings
- logger.debug(
- f"Interpreting template '{repr(filepath)}' of component '{module_name}'"
- " relatively to component file"
- )
-
- return filepath_rel_to_comp_dir
-
- # If resolved absolute path does NOT exist or filepath is NOT a string, then return as is
- logger.debug(
- f"Interpreting template '{repr(filepath)}' of component '{module_name}'"
- " relatively to components directory"
+ def resolve_relative_media_file(
+ filepath: Union[str, SafeData],
+ allow_glob: bool,
+ ) -> List[Union[str, SafeData]]:
+ resolved_filepaths, has_matched = resolve_media_file(
+ filepath,
+ allow_glob,
+ static_files_dir=matched_component_dir_abs,
+ media_root_dir=component_module_dir_path_abs,
)
- return filepath
+
+ # NOTE: It's important to use `repr`, so we don't trigger __str__ on SafeStrings
+ if has_matched:
+ logger.debug(
+ f"Interpreting file '{repr(filepath)}' of component '{module_name}'" " relatively to component file"
+ )
+ else:
+ logger.debug(
+ f"Interpreting file '{repr(filepath)}' of component '{module_name}'"
+ " relatively to components directory"
+ )
+
+ return resolved_filepaths
+
+ # Check if filepath is a glob pattern that points to files relative to the components directory
+ # (as defined by `COMPONENTS.dirs` and `COMPONENTS.app_dirs` settings) in which the component is defined.
+ # If yes, modify the path to refer to the globbed files.
+ # If not, don't modify anything.
+ def resolve_static_media_file(
+ filepath: Union[str, SafeData],
+ allow_glob: bool,
+ ) -> List[Union[str, SafeData]]:
+ resolved_filepaths, _ = resolve_media_file(
+ filepath,
+ allow_glob,
+ static_files_dir=matched_component_dir_abs,
+ media_root_dir=matched_component_dir_abs,
+ )
+ return resolved_filepaths
# Check if template name is a local file or not
if getattr(comp_media, "template_file", None):
- comp_media.template_file = resolve_media_file(comp_media.template_file)
+ comp_media.template_file = resolve_relative_media_file(comp_media.template_file, False)[0]
if getattr(comp_media, "js_file", None):
- comp_media.js_file = resolve_media_file(comp_media.js_file)
+ comp_media.js_file = resolve_relative_media_file(comp_media.js_file, False)[0]
if getattr(comp_media, "css_file", None):
- comp_media.css_file = resolve_media_file(comp_media.css_file)
+ comp_media.css_file = resolve_relative_media_file(comp_media.css_file, False)[0]
if hasattr(comp_media, "Media") and comp_media.Media:
- _map_media_filepaths(comp_media.Media, resolve_media_file)
-
-
-def _get_dir_path_from_component_path(
- abs_component_file_path: str,
- candidate_dirs: Union[List[str], List[Path]],
-) -> Tuple[str, str]:
- comp_dir_path_abs = os.path.dirname(abs_component_file_path)
-
- # From all dirs defined in settings.COMPONENTS.dirs, find one that's the parent
- # to the component file.
- root_dir_abs = None
- for candidate_dir in candidate_dirs:
- candidate_dir_abs = os.path.abspath(candidate_dir)
- if comp_dir_path_abs.startswith(candidate_dir_abs):
- root_dir_abs = candidate_dir_abs
- break
-
- if root_dir_abs is None:
- raise RuntimeError(
- f"Failed to resolve template directory for component file '{abs_component_file_path}'",
+ _map_media_filepaths(
+ comp_media.Media,
+ # Media files can be defined as a glob patterns that match multiple files.
+ # Thus, flatten the list of lists returned by `resolve_relative_media_file`.
+ lambda filepaths: flatten(resolve_relative_media_file(f, True) for f in filepaths),
)
- # Derive the path from matched COMPONENTS.dirs to the dir where the current component file is.
- comp_dir_path_rel = os.path.relpath(comp_dir_path_abs, candidate_dir_abs)
+ # Go over the JS / CSS media files again, but this time, if there are still any globs,
+ # try to resolve them relative to the root directory (as defined by `COMPONENTS.dirs
+ # and `COMPONENTS.app_dirs` settings).
+ _map_media_filepaths(
+ comp_media.Media,
+ # Media files can be defined as a glob patterns that match multiple files.
+ # Thus, flatten the list of lists returned by `resolve_static_media_file`.
+ lambda filepaths: flatten(resolve_static_media_file(f, True) for f in filepaths),
+ )
- # Return both absolute and relative paths:
- # - Absolute path is used to check if the file exists
- # - Relative path is used for defining the import on the component class
- return comp_dir_path_abs, comp_dir_path_rel
+
+# Check if filepath refers to a file that's in the same directory as the component class.
+# If yes, modify the path to refer to the relative file.
+# If not, don't modify anything.
+def resolve_media_file(
+ filepath: Union[str, SafeData],
+ allow_glob: bool,
+ static_files_dir: str,
+ media_root_dir: str,
+) -> Tuple[List[Union[str, SafeData]], bool]:
+ # If filepath is NOT a string, then return as is
+ if not isinstance(filepath, str):
+ return [filepath], False
+
+ filepath_abs_or_glob = os.path.join(media_root_dir, filepath)
+
+ # The path may be a glob. So before we check if the file exists,
+ # we need to resolve the glob.
+ if allow_glob and is_glob(filepath_abs_or_glob):
+ matched_abs_filepaths = glob.glob(filepath_abs_or_glob)
+ else:
+ matched_abs_filepaths = [filepath_abs_or_glob]
+
+ # If there are no matches, return the original filepath
+ if not matched_abs_filepaths:
+ return [filepath], False
+
+ resolved_filepaths: List[str] = []
+ for matched_filepath_abs in matched_abs_filepaths:
+ # Derive the path from matched `COMPONENTS.dirs` to the media file.
+ # NOTE: The paths to resources need to use POSIX (forward slashes) for Django to work
+ # See https://github.com/django-components/django-components/issues/796
+ # NOTE: Since these paths matched the glob, we know that these files exist.
+ filepath_rel_to_comp_dir = Path(os.path.relpath(matched_filepath_abs, static_files_dir)).as_posix()
+ resolved_filepaths.append(filepath_rel_to_comp_dir)
+
+ return resolved_filepaths, True
+
+
+def _find_component_dir_containing_file(
+ component_dirs: Sequence[Union[str, Path]],
+ target_file_path: str,
+) -> Optional[Union[str, Path]]:
+ """
+ From all directories that may contain components (such as those defined in `COMPONENTS.dirs`),
+ find the one that's the parent to the given file.
+ """
+ abs_target_file_path = os.path.abspath(target_file_path)
+
+ for component_dir in component_dirs:
+ component_dir_abs = os.path.abspath(component_dir)
+ if abs_target_file_path.startswith(component_dir_abs):
+ return component_dir
+
+ return None
def _get_asset(
diff --git a/src/django_components/util/misc.py b/src/django_components/util/misc.py
index 605fb6a6..1ac8698f 100644
--- a/src/django_components/util/misc.py
+++ b/src/django_components/util/misc.py
@@ -2,8 +2,9 @@ import re
import sys
from hashlib import md5
from importlib import import_module
+from itertools import chain
from types import ModuleType
-from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Type, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Tuple, Type, TypeVar, Union
from django_components.util.nanoid import generate
@@ -115,3 +116,15 @@ def hash_comp_cls(comp_cls: Type["Component"]) -> str:
full_name = get_import_path(comp_cls)
comp_cls_hash = md5(full_name.encode()).hexdigest()[0:6]
return comp_cls.__name__ + "_" + comp_cls_hash
+
+
+# String is a glob if it contains at least one of `?`, `*`, or `[`
+is_glob_re = re.compile(r"[?*[]")
+
+
+def is_glob(filepath: str) -> bool:
+ return is_glob_re.search(filepath) is not None
+
+
+def flatten(lst: Iterable[Iterable[T]]) -> List[T]:
+ return list(chain.from_iterable(lst))
diff --git a/tests/components/glob/glob.py b/tests/components/glob/glob.py
new file mode 100644
index 00000000..6c5352ca
--- /dev/null
+++ b/tests/components/glob/glob.py
@@ -0,0 +1,23 @@
+
+from django_components import Component
+
+
+# The Media JS / CSS glob and are relative to the component directory
+class GlobComponent(Component):
+ template = """
+ {% load component_tags %}
+ {% component_js_dependencies %}
+ {% component_css_dependencies %}
+ """
+
+ class Media:
+ css = "glob_*.css"
+ js = "glob_*.js"
+
+
+# The Media JS / CSS glob and are relative to the directory given in
+# `COMPONENTS.dirs` and `COMPONENTS.app_dirs`
+class GlobComponentRootDir(GlobComponent):
+ class Media:
+ css = "glob/glob_*.css"
+ js = "glob/glob_*.js"
diff --git a/tests/components/glob/glob_1.css b/tests/components/glob/glob_1.css
new file mode 100644
index 00000000..3b07560d
--- /dev/null
+++ b/tests/components/glob/glob_1.css
@@ -0,0 +1,3 @@
+.html-css-only {
+ color: blue;
+}
diff --git a/tests/components/glob/glob_1.js b/tests/components/glob/glob_1.js
new file mode 100644
index 00000000..5a73c14a
--- /dev/null
+++ b/tests/components/glob/glob_1.js
@@ -0,0 +1 @@
+console.log("JS file");
diff --git a/tests/components/glob/glob_2.css b/tests/components/glob/glob_2.css
new file mode 100644
index 00000000..3b07560d
--- /dev/null
+++ b/tests/components/glob/glob_2.css
@@ -0,0 +1,3 @@
+.html-css-only {
+ color: blue;
+}
diff --git a/tests/components/glob/glob_2.js b/tests/components/glob/glob_2.js
new file mode 100644
index 00000000..5a73c14a
--- /dev/null
+++ b/tests/components/glob/glob_2.js
@@ -0,0 +1 @@
+console.log("JS file");
diff --git a/tests/test_component_media.py b/tests/test_component_media.py
index 6fb2caa1..eb71e51d 100644
--- a/tests/test_component_media.py
+++ b/tests/test_component_media.py
@@ -386,6 +386,34 @@ class TestComponentMedia:
assertInHTML('', rendered)
assertInHTML('', rendered)
+ @djc_test(
+ django_settings={
+ "INSTALLED_APPS": ("django_components", "tests"),
+ }
+ )
+ def test_glob_pattern_relative_to_component(self):
+ from tests.components.glob.glob import GlobComponent
+ rendered = GlobComponent.render()
+
+ assertInHTML('', rendered)
+ assertInHTML('', rendered)
+ assertInHTML('', rendered)
+ assertInHTML('', rendered)
+
+ @djc_test(
+ django_settings={
+ "INSTALLED_APPS": ("django_components", "tests"),
+ }
+ )
+ def test_glob_pattern_relative_to_root_dir(self):
+ from tests.components.glob.glob import GlobComponentRootDir
+ rendered = GlobComponentRootDir.render()
+
+ assertInHTML('', rendered)
+ assertInHTML('', rendered)
+ assertInHTML('', rendered)
+ assertInHTML('', rendered)
+
@djc_test
class TestMediaPathAsObject:
diff --git a/tests/test_loader.py b/tests/test_loader.py
index 927bf219..227ec04d 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -247,6 +247,7 @@ class TestComponentFiles:
assert dot_paths == [
"components",
+ "components.glob.glob",
"components.multi_file.multi_file",
"components.relative_file.relative_file",
"components.relative_file_pathobj.relative_file_pathobj",
@@ -260,15 +261,16 @@ class TestComponentFiles:
# NOTE: Compare parts so that the test works on Windows too
assert file_paths[0].parts[-3:] == ("tests", "components", "__init__.py")
- assert file_paths[1].parts[-4:] == ("tests", "components", "multi_file", "multi_file.py")
- assert file_paths[2].parts[-4:] == ("tests", "components", "relative_file", "relative_file.py")
- assert file_paths[3].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.py")
- assert file_paths[4].parts[-3:] == ("tests", "components", "single_file.py")
- assert file_paths[5].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.py")
- assert file_paths[6].parts[-3:] == ("tests", "components", "urls.py")
- assert file_paths[7].parts[-3:] == ("django_components", "components", "__init__.py")
- assert file_paths[8].parts[-3:] == ("django_components", "components", "dynamic.py")
- assert file_paths[9].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
+ assert file_paths[1].parts[-4:] == ("tests", "components", "glob", "glob.py")
+ assert file_paths[2].parts[-4:] == ("tests", "components", "multi_file", "multi_file.py")
+ assert file_paths[3].parts[-4:] == ("tests", "components", "relative_file", "relative_file.py")
+ assert file_paths[4].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.py")
+ assert file_paths[5].parts[-3:] == ("tests", "components", "single_file.py")
+ assert file_paths[6].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.py")
+ assert file_paths[7].parts[-3:] == ("tests", "components", "urls.py")
+ assert file_paths[8].parts[-3:] == ("django_components", "components", "__init__.py")
+ assert file_paths[9].parts[-3:] == ("django_components", "components", "dynamic.py")
+ assert file_paths[10].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.py")
@djc_test(
django_settings={
@@ -282,6 +284,8 @@ class TestComponentFiles:
file_paths = [f.filepath for f in files]
assert dot_paths == [
+ "components.glob.glob_1",
+ "components.glob.glob_2",
"components.relative_file.relative_file",
"components.relative_file_pathobj.relative_file_pathobj",
"components.staticfiles.staticfiles",
@@ -289,10 +293,12 @@ class TestComponentFiles:
]
# NOTE: Compare parts so that the test works on Windows too
- assert file_paths[0].parts[-4:] == ("tests", "components", "relative_file", "relative_file.js")
- assert file_paths[1].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.js")
- assert file_paths[2].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.js")
- assert file_paths[3].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.js")
+ assert file_paths[0].parts[-4:] == ("tests", "components", "glob", "glob_1.js")
+ assert file_paths[1].parts[-4:] == ("tests", "components", "glob", "glob_2.js")
+ assert file_paths[2].parts[-4:] == ("tests", "components", "relative_file", "relative_file.js")
+ assert file_paths[3].parts[-4:] == ("tests", "components", "relative_file_pathobj", "relative_file_pathobj.js")
+ assert file_paths[4].parts[-4:] == ("tests", "components", "staticfiles", "staticfiles.js")
+ assert file_paths[5].parts[-5:] == ("tests", "test_app", "components", "app_lvl_comp", "app_lvl_comp.js")
@djc_test