diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index 5c0159c0b4..eae25ba737 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -72,6 +72,28 @@ class HashedFilesMixin: r"(?m)(?P)^(//# (?-i:sourceMappingURL)=(?P.*))$", "//# sourceMappingURL=%(url)s", ), + ( + ( + r"""(?Pimport(?s:(?P[\s\{].*?))""" + r"""\s*from\s*['"](?P[\.\/].*?)["']\s*;)""" + ), + """import%(import)s from "%(url)s";""", + ), + ( + ( + r"""(?Pexport(?s:(?P[\s\{].*?))""" + r"""\s*from\s*["'](?P[\.\/].*?)["']\s*;)""" + ), + """export%(exports)s from "%(url)s";""", + ), + ( + r"""(?Pimport\s*['"](?P[\.\/].*?)["']\s*;)""", + """import"%(url)s";""", + ), + ( + r"""(?Pimport\(["'](?P.*?)["']\))""", + """import("%(url)s")""", + ), ), ), ) diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 152838736c..0f0e8d8001 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/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`` ---------------------- diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 7e2aa6e5d6..77d4c8594c 100644 --- a/docs/releases/4.2.txt +++ b/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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/staticfiles_tests/project/documents/absolute_root.js b/tests/staticfiles_tests/project/documents/absolute_root.js new file mode 100644 index 0000000000..4561b0389b --- /dev/null +++ b/tests/staticfiles_tests/project/documents/absolute_root.js @@ -0,0 +1,2 @@ +const rootConst = "root"; +export default rootConst; diff --git a/tests/staticfiles_tests/project/documents/cached/module.js b/tests/staticfiles_tests/project/documents/cached/module.js new file mode 100644 index 0000000000..7764e740d6 --- /dev/null +++ b/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"; diff --git a/tests/staticfiles_tests/project/documents/cached/module_test.js b/tests/staticfiles_tests/project/documents/cached/module_test.js new file mode 100644 index 0000000000..219372f891 --- /dev/null +++ b/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"; diff --git a/tests/staticfiles_tests/project/documents/nested/js/nested.js b/tests/staticfiles_tests/project/documents/nested/js/nested.js new file mode 100644 index 0000000000..7646bbd17d --- /dev/null +++ b/tests/staticfiles_tests/project/documents/nested/js/nested.js @@ -0,0 +1 @@ +export default null; diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py index 16bb556d4f..077d14bcc4 100644 --- a/tests/staticfiles_tests/test_storage.py +++ b/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"],