Repository: django-components/django-components
Pull request #1067: refactor: fix component media URLs
Author: login: JuroOravec
Created: 2025-03-23T20:52:53Z | Merged: 2025-03-23T21:32:18Z

Further fix for https://github.com/django-components/django-components/issues/1062, as raised in https://github.com/django-components/django-components/pull/1065.

@DNIIBOY Could you test this PR locally to see if it works for your case? IMO it should work, but better to double check.

And sorry for making yet another PR, I don't want to modify other people's PRs (unless it's tiny things) to avoid confusion.

Supersedes https://github.com/django-components/django-components/pull/1065.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 036d47e97..3dcc0404c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,23 @@
 # Release notes
 
+## v0.134
+
+#### Fix
+
+- HOTFIX: Fix the use of URLs in `Component.Media.js` and `Component.Media.css`
+
 ## v0.133
 
+⚠️ Attention ⚠️ - Please update to v0.134 to fix bugs introduced in v0.132.
+
 #### Fix
 
 - HOTFIX: Fix the use of URLs in `Component.Media.js` and `Component.Media.css`
 
 ## v0.132
 
+⚠️ Attention ⚠️ - Please update to v0.134 to fix bugs introduced in v0.132.
+
 #### Feat
 
 - Allow to use glob patterns as paths for additional JS / CSS in
diff --git a/pyproject.toml b/pyproject.toml
index 6bda9c449..29572bbd3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "django_components"
-version = "0.133"
+version = "0.134"
 requires-python = ">=3.8, <4.0"
 description = "A way to create simple reusable template components in Django."
 keywords = ["django", "components", "css", "js", "html"]
diff --git a/src/django_components/component_media.py b/src/django_components/component_media.py
index eeefc15dd..7612f2ae0 100644
--- a/src/django_components/component_media.py
+++ b/src/django_components/component_media.py
@@ -868,11 +868,15 @@ def resolve_media_file(
     # If the path is a URL, don't resolve it
     # (e.g. https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js)
     # This is defined based on Django's `Media.absolute_path()` method.
-    is_url_path = filepath.startswith(("http://", "https", "/"))
+    is_url_path = filepath.startswith(("http://", "https://", "://", "/"))
+
+    # If the path is a URL, don't resolve it
+    if is_url_path:
+        return [filepath], False
 
     # 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) and not is_url_path:
+    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]
diff --git a/tests/components/glob/glob.py b/tests/components/glob/glob.py
index 6c5352cac..22a2198e1 100644
--- a/tests/components/glob/glob.py
+++ b/tests/components/glob/glob.py
@@ -21,3 +21,28 @@ class GlobComponentRootDir(GlobComponent):
     class Media:
         css = "glob/glob_*.css"
         js = "glob/glob_*.js"
+
+
+# The Media JS / CSS are NOT globs, but URLs.
+class UrlComponent(Component):
+    template = """
+        {% load component_tags %}
+        {% component_js_dependencies %}
+        {% component_css_dependencies %}
+    """
+
+    class Media:
+        css = [
+            "https://cdnjs.cloudflare.com/example/style.min.css",
+            "http://cdnjs.cloudflare.com/example/style.min.css",
+            # :// is not a valid URL - will be resolved as static path
+            "://cdnjs.cloudflare.com/example/style.min.css",
+            "/path/to/style.css",
+        ]
+        js = [
+            "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js",
+            "http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js",
+            # :// is not a valid URL - will be resolved as static path
+            "://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js",
+            "/path/to/script.js",
+        ]
diff --git a/tests/test_component_media.py b/tests/test_component_media.py
index 165d2c068..962c0d54e 100644
--- a/tests/test_component_media.py
+++ b/tests/test_component_media.py
@@ -415,40 +415,18 @@ def test_glob_pattern_relative_to_root_dir(self):
         assertInHTML('<script src="glob/glob_2.js"></script>', rendered)
 
     def test_glob_pattern_does_not_break_urls(self):
-        class MyComponent(Component):
-            template = """
-                {% load component_tags %}
-                {% component_js_dependencies %}
-                {% component_css_dependencies %}
-            """
-
-            class Media:
-                css = [
-                    "https://cdnjs.cloudflare.com/example/style.min.css",
-                    "http://cdnjs.cloudflare.com/example/style.min.css",
-                    # :// is not a valid URL - will be resolved as static path
-                    "://cdnjs.cloudflare.com/example/style.min.css",
-                    "/path/to/style.css",
-                ]
-                js = [
-                    "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js",
-                    "http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js",
-                    # :// is not a valid URL - will be resolved as static path
-                    "://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js",
-                    "/path/to/script.js",
-                ]
-
-        rendered = MyComponent.render()
+        from tests.components.glob.glob import UrlComponent
+        rendered = UrlComponent.render()
 
         assertInHTML('<link href="https://cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
         assertInHTML('<link href="http://cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
-        # `://` is escaped because the path was resolved with Django's `static()`
+        # `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL
         assertInHTML('<link href="%3A//cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
         assertInHTML('<link href="/path/to/style.css" media="all" rel="stylesheet">', rendered)
 
         assertInHTML('<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
         assertInHTML('<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
-        # `://` is escaped because the path was resolved with Django's `static()`
+        # `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL
         assertInHTML('<script src="%3A//cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
         assertInHTML('<script src="/path/to/script.js"></script>', rendered)
 


Reviews:
user: login: DNIIBOY
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-23T21:00:30Z
user: login: JuroOravec
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T21:07:09Z
user: login: JuroOravec
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T21:22:14Z

Review comments:
diff_hunk: @@ -868,7 +868,11 @@ def resolve_media_file(
     # If the path is a URL, don't resolve it
     # (e.g. https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js)
     # This is defined based on Django's `Media.absolute_path()` method.
-    is_url_path = filepath.startswith(("http://", "https", "/"))
+    is_url_path = filepath.startswith(("http://", "https://", "://", "/"))
+
+    # If the path is a URL, don't resolve it
+    if is_url_path:
path: src/django_components/component_media.py
user: login: DNIIBOY
body: ```python
if allow_glob and is_glob(filepath_abs_or_glob) and not is_url_path:
```
We can remove the `and not is_url_path` from the following if statement for clarity
created_at: 2025-03-23T21:00:30Z
author_association: NONE
diff_hunk: @@ -415,41 +415,19 @@ def test_glob_pattern_relative_to_root_dir(self):
         assertInHTML('<script src="glob/glob_2.js"></script>', rendered)
 
     def test_glob_pattern_does_not_break_urls(self):
-        class MyComponent(Component):
path: tests/test_component_media.py
user: login: JuroOravec
body: This is why the edge case slipped the test - When a component is defined within a test file, the test file is not within the directories in `COMPONENTS.dirs`, and so the file paths are not even attempted to be resolved.

So after I moved the component class to be defined in a separate file, which is within `COMPONENTS.dirs`, then that triggered to try to resolve the file paths relative to the component file, which revealed the bug.
created_at: 2025-03-23T21:07:09Z
author_association: COLLABORATOR
diff_hunk: @@ -868,7 +868,11 @@ def resolve_media_file(
     # If the path is a URL, don't resolve it
     # (e.g. https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js)
     # This is defined based on Django's `Media.absolute_path()` method.
-    is_url_path = filepath.startswith(("http://", "https", "/"))
+    is_url_path = filepath.startswith(("http://", "https://", "://", "/"))
+
+    # If the path is a URL, don't resolve it
+    if is_url_path:
path: src/django_components/component_media.py
user: login: JuroOravec
body: Good catch, thanks!
created_at: 2025-03-23T21:22:14Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-23T20:54:55Z
body: ## Performance Benchmark Results

Comparing PR changes against master branch:


Benchmarks that have stayed the same:

| Change   | Before [35444022]    | After [7213ef52]    |   Ratio | Benchmark (Parameter)                                             |
|----------|----------------------|---------------------|---------|-------------------------------------------------------------------|
|          | 69.3±1ms             | 68.9±0.8ms          |    0.99 | Components vs Django.timeraw_render_lg_first('django')            |
|          | 257±2ms              | 255±2ms             |    0.99 | Components vs Django.timeraw_render_lg_first('django-components') |

author_association: CONTRIBUTOR
user: login: DNIIBOY
created_at: 2025-03-23T20:56:32Z
body: This works in my case, and is a better solution than #1065 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-23T21:23:48Z
body: ## Performance Benchmark Results

Comparing PR changes against master branch:


Benchmarks that have stayed the same:

| Change   | Before [35444022]    | After [94bcbea0]    |   Ratio | Benchmark (Parameter)                                             |
|----------|----------------------|---------------------|---------|-------------------------------------------------------------------|
|          | 69.3±0.5ms           | 69.8±0.7ms          |    1.01 | Components vs Django.timeraw_render_lg_first('django')            |
|          | 256±2ms              | 256±3ms             |    1    | Components vs Django.timeraw_render_lg_first('django-components') |

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-23T21:26:06Z
body: ## Performance Benchmark Results

Comparing PR changes against master branch:


Benchmarks that have stayed the same:

| Change   | Before [35444022]    | After [92a8e613]    |   Ratio | Benchmark (Parameter)                                             |
|----------|----------------------|---------------------|---------|-------------------------------------------------------------------|
|          | 70.8±0.9ms           | 71.5±1ms            |    1.01 | Components vs Django.timeraw_render_lg_first('django')            |
|          | 262±2ms              | 264±3ms             |    1.01 | Components vs Django.timeraw_render_lg_first('django-components') |

author_association: CONTRIBUTOR
user: login: JuroOravec
created_at: 2025-03-23T21:32:01Z
body: cc @EmilStenstrom
author_association: COLLABORATOR
user: login: JuroOravec
created_at: 2025-03-23T21:38:00Z
body: @DNIIBOY Thanks again for raising this and your help! This is now released in [0.134](https://github.com/django-components/django-components/releases/tag/0.134)
author_association: COLLABORATOR