diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index e97355831b..f957a667b2 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -57,6 +57,15 @@ class HashedFilesMixin: r'(?P)^(//# (?-i:sourceMappingURL)=(?P.*))$', '//# sourceMappingURL=%(url)s', ), + ( + r"""(?Pimport\s+(?s:(?P.*?))\s*from\s*["'](?P.*?)["'])""", + 'import %(imports)s from "%(url)s"', + ), + ( + r"""(?Pexport\s+(?s:(?P.*?))\s*from\s*["'](?P.*?)["'])""", + 'export %(exports)s from "%(url)s"', + ), + (r"""(?Pimport\(["'](?P.*?)["']\))""", 'import("%(url)s")'), )), ) keep_intermediate_files = True diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index f22f32c78d..14c739b3ea 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`_. * The `source map`_ comment in JavaScript. +* The `modules import`_ in JavaScript. +* The `modules aggregation`_ in JavaScript. For example, the ``'css/styles.css'`` file with this content: @@ -315,6 +317,9 @@ For example, the ``'css/styles.css'`` file with this content: Support for finding paths in the source map comments was added. + 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 @@ -368,6 +373,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.0.txt b/docs/releases/4.0.txt index eecbc9e4d8..7695ce95a2 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -149,6 +149,10 @@ Minor features replaces paths to JavaScript source map references with their hashed counterparts. +* :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..7380ad7856 --- /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 { + firstVar as firstVarAlias, + secondVar 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..b832b4e8a7 --- /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 firstVar = "test_1"; +export var SecondVar = "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 6c877a271a..db655f2453 100644 --- a/tests/staticfiles_tests/test_storage.py +++ b/tests/staticfiles_tests/test_storage.py @@ -159,6 +159,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.91b9cf9935da.js') + tests = [ + # Relative imports. + b'import testConst from "./module_test.d489af3cf882.js";', + b'import relativeModule from "../nested/js/nested.866475c46bb4.js";', + b'import { firstConst, secondConst } from "./module_test.d489af3cf882.js";', + # Absolute import. + b'import rootConst from "/static/absolute_root.5586327fe78c.js";', + # Dynamic import. + b'const dynamicModule = import("./module_test.d489af3cf882.js");', + # Creating a module object. + b'import * as NewModule from "./module_test.d489af3cf882.js";', + # Aliases. + b'import { testConst as alias } from "./module_test.d489af3cf882.js";', + b'import {\n' + b' firstVar as firstVarAlias,\n' + b' secondVar as secondVarAlias\n' + b'} from "./module_test.d489af3cf882.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.91b9cf9935da.js') + tests = [ + b'export * from "./module_test.d489af3cf882.js";', + b'export { testConst } from "./module_test.d489af3cf882.js";', + b'export {\n' + b' firstVar as firstVarAlias,\n' + b' secondVar as secondVarAlias\n' + b'} from "./module_test.d489af3cf882.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'],