Browse Source

Fixed #32319 -- Added ES module support to ManifestStaticFilesStorage.

Co-authored-by: James Bligh <james.bligh@silvercloudhealth.com>
James Bligh 2 years ago
parent
commit
e44d348c99

+ 22 - 0
django/contrib/staticfiles/storage.py

@@ -72,6 +72,28 @@ class HashedFilesMixin:
                     r"(?m)(?P<matched>)^(//# (?-i:sourceMappingURL)=(?P<url>.*))$",
                     "//# sourceMappingURL=%(url)s",
                 ),
+                (
+                    (
+                        r"""(?P<matched>import(?s:(?P<import>[\s\{].*?))"""
+                        r"""\s*from\s*['"](?P<url>[\.\/].*?)["']\s*;)"""
+                    ),
+                    """import%(import)s from "%(url)s";""",
+                ),
+                (
+                    (
+                        r"""(?P<matched>export(?s:(?P<exports>[\s\{].*?))"""
+                        r"""\s*from\s*["'](?P<url>[\.\/].*?)["']\s*;)"""
+                    ),
+                    """export%(exports)s from "%(url)s";""",
+                ),
+                (
+                    r"""(?P<matched>import\s*['"](?P<url>[\.\/].*?)["']\s*;)""",
+                    """import"%(url)s";""",
+                ),
+                (
+                    r"""(?P<matched>import\(["'](?P<url>.*?)["']\))""",
+                    """import("%(url)s")""",
+                ),
             ),
         ),
     )

+ 9 - 0
docs/ref/contrib/staticfiles.txt

@@ -295,6 +295,8 @@ method). The regular expressions used to find those paths
 
 * The `@import`_ rule and `url()`_ statement of `Cascading Style Sheets`_.
 * `Source map`_ comments in CSS and JavaScript files.
+* The `modules import`_ in JavaScript.
+* The `modules aggregation`_ in JavaScript.
 
 For example, the ``'css/styles.css'`` file with this content:
 
@@ -329,6 +331,11 @@ argument. For example::
 
     Support for finding paths in CSS source map comments was added.
 
+.. versionchanged:: 4.2
+
+    Support for finding paths to JavaScript modules in ``import`` and
+    ``export`` statements was added.
+
 .. attribute:: storage.ManifestStaticFilesStorage.max_post_process_passes
 
 Since static files might reference other static files that need to have their
@@ -382,6 +389,8 @@ hashing algorithm.
 .. _`url()`: https://www.w3.org/TR/CSS2/syndata.html#uri
 .. _`Cascading Style Sheets`: https://www.w3.org/Style/CSS/
 .. _`source map`: https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map
+.. _`modules import`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_features_into_your_script
+.. _`modules aggregation`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#aggregating_modules
 
 ``ManifestFilesMixin``
 ----------------------

+ 3 - 1
docs/releases/4.2.txt

@@ -131,7 +131,9 @@ Minor features
 :mod:`django.contrib.staticfiles`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* :class:`~django.contrib.staticfiles.storage.ManifestStaticFilesStorage` now
+  replaces paths to JavaScript modules in ``import`` and ``export`` statements
+  with their hashed counterparts.
 
 :mod:`django.contrib.syndication`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 2 - 0
tests/staticfiles_tests/project/documents/absolute_root.js

@@ -0,0 +1,2 @@
+const rootConst = "root";
+export default rootConst;

+ 22 - 0
tests/staticfiles_tests/project/documents/cached/module.js

@@ -0,0 +1,22 @@
+// Static imports.
+import rootConst from "/static/absolute_root.js";
+import testConst from "./module_test.js";
+import * as NewModule from "./module_test.js";
+import { testConst as alias } from "./module_test.js";
+import { firstConst, secondConst } from "./module_test.js";
+import {
+    firstVar1 as firstVarAlias,
+    $second_var_2 as secondVarAlias
+} from "./module_test.js";
+import relativeModule from "../nested/js/nested.js";
+
+// Dynamic imports.
+const dynamicModule = import("./module_test.js");
+
+// Modules exports to aggregate modules.
+export * from "./module_test.js";
+export { testConst } from "./module_test.js";
+export {
+    firstVar as firstVarAlias,
+    secondVar as secondVarAlias
+} from "./module_test.js";

+ 5 - 0
tests/staticfiles_tests/project/documents/cached/module_test.js

@@ -0,0 +1,5 @@
+export const testConst = "test";
+export const firstConst = "first";
+export const secondConst = "second";
+export var firstVar1 = "test_1";
+export var SecondVar2 = "test_2";

+ 1 - 0
tests/staticfiles_tests/project/documents/nested/js/nested.js

@@ -0,0 +1 @@
+export default null;

+ 46 - 0
tests/staticfiles_tests/test_storage.py

@@ -177,6 +177,52 @@ class TestHashedFiles:
             self.assertIn(b"https://", relfile.read())
         self.assertPostCondition()
 
+    def test_module_import(self):
+        relpath = self.hashed_file_path("cached/module.js")
+        self.assertEqual(relpath, "cached/module.55fd6938fbc5.js")
+        tests = [
+            # Relative imports.
+            b'import testConst from "./module_test.477bbebe77f0.js";',
+            b'import relativeModule from "../nested/js/nested.866475c46bb4.js";',
+            b'import { firstConst, secondConst } from "./module_test.477bbebe77f0.js";',
+            # Absolute import.
+            b'import rootConst from "/static/absolute_root.5586327fe78c.js";',
+            # Dynamic import.
+            b'const dynamicModule = import("./module_test.477bbebe77f0.js");',
+            # Creating a module object.
+            b'import * as NewModule from "./module_test.477bbebe77f0.js";',
+            # Aliases.
+            b'import { testConst as alias } from "./module_test.477bbebe77f0.js";',
+            b"import {\n"
+            b"    firstVar1 as firstVarAlias,\n"
+            b"    $second_var_2 as secondVarAlias\n"
+            b'} from "./module_test.477bbebe77f0.js";',
+        ]
+        with storage.staticfiles_storage.open(relpath) as relfile:
+            content = relfile.read()
+            for module_import in tests:
+                with self.subTest(module_import=module_import):
+                    self.assertIn(module_import, content)
+        self.assertPostCondition()
+
+    def test_aggregating_modules(self):
+        relpath = self.hashed_file_path("cached/module.js")
+        self.assertEqual(relpath, "cached/module.55fd6938fbc5.js")
+        tests = [
+            b'export * from "./module_test.477bbebe77f0.js";',
+            b'export { testConst } from "./module_test.477bbebe77f0.js";',
+            b"export {\n"
+            b"    firstVar as firstVarAlias,\n"
+            b"    secondVar as secondVarAlias\n"
+            b'} from "./module_test.477bbebe77f0.js";',
+        ]
+        with storage.staticfiles_storage.open(relpath) as relfile:
+            content = relfile.read()
+            for module_import in tests:
+                with self.subTest(module_import=module_import):
+                    self.assertIn(module_import, content)
+        self.assertPostCondition()
+
     @override_settings(
         STATICFILES_DIRS=[os.path.join(TEST_ROOT, "project", "loop")],
         STATICFILES_FINDERS=["django.contrib.staticfiles.finders.FileSystemFinder"],