Skip to content

Commit 94841da

Browse files
committed
Add option to use ManifestStaticFilesStorage
1 parent 7da2caa commit 94841da

File tree

9 files changed

+210
-14
lines changed

9 files changed

+210
-14
lines changed

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ You can continue to load other resources such as CSS files as before using the `
6161

6262

6363
NPM.JS dependencies
64-
-----------
64+
-------------------
6565

6666
1. Add package.json files into one or more of your apps. All package.json files will be merged.
6767

@@ -72,7 +72,7 @@ NPM.JS dependencies
7272
4. Run `./manage.py runserver`.
7373

7474
Referring to the transpile version within JavaScript sources
75-
------
75+
------------------------------------------------------------
7676

7777
In your JavaScript sources, you can refer to the version string of the last transpile run like this::
7878

@@ -81,3 +81,20 @@ In your JavaScript sources, you can refer to the version string of the last tran
8181
For example::
8282

8383
let downloadJS = `download.js?v=${transpile.VERSION}` // Latest version of transpiled version of download.mjs
84+
85+
86+
ManifestStaticFilesStorage
87+
--------------------------
88+
If you use `ManifestStaticFilesStorage`, import it from `npm_mjs.storage` like this:
89+
90+
```py
91+
from npm_mjs.storage import ManifestStaticFilesStorage
92+
```
93+
94+
If you use that version, you can refer to other static files within your JavaScript files using the `staticUrl()` function like this:
95+
96+
```js
97+
const cssUrl = staticUrl('/css/document.css')
98+
```
99+
100+
Note that you will need to use absolute paths starting from the `STATIC_ROOT` for the `staticUrl()` function. Different from the default `ManifestStaticFilesStorage`, our version will generally interprete file urls starting with a slash as being relative to the `STATIC_ROOT`.

npm_mjs/management/commands/collectstatic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from django.contrib.staticfiles.management.commands.collectstatic import (
22
Command as CollectStaticCommand,
33
)
4+
from django.apps import apps
45

56

67
class Command(CollectStaticCommand):
8+
79
def set_options(self, *args, **options):
810
return_value = super(Command, self).set_options(*args, **options)
911
self.ignore_patterns += [
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os
2+
import tempfile
3+
import shutil
4+
import urllib
5+
6+
from django.core.management.commands import makemessages
7+
from django.core.management import call_command
8+
from django.core.management.utils import popen_wrapper
9+
10+
from base.management import BaseCommand
11+
12+
# This makes makemessages create both translations for Python and JavaScript
13+
# code in one go.
14+
#
15+
# Additionally, it works around xgettext not being able to work with ES2015
16+
# template strings [1] by transpiling the JavaScript files in a special way to
17+
# replace all template strings.
18+
#
19+
# [1] https://savannah.gnu.org/bugs/?50920
20+
21+
22+
class Command(makemessages.Command, BaseCommand):
23+
def handle(self, *args, **options):
24+
call_command("transpile")
25+
options["ignore_patterns"] += [
26+
"venv",
27+
".direnv",
28+
"node_modules",
29+
"static-transpile",
30+
]
31+
options["domain"] = "django"
32+
super().handle(*args, **options)
33+
options["domain"] = "djangojs"
34+
self.temp_dir_out = tempfile.mkdtemp()
35+
self.temp_dir_in = tempfile.mkdtemp()
36+
super().handle(*args, **options)
37+
shutil.rmtree(self.temp_dir_in)
38+
shutil.rmtree(self.temp_dir_out)
39+
40+
def process_locale_dir(self, locale_dir, files):
41+
if self.domain == "djangojs":
42+
for file in files:
43+
# We need to copy the JS files first, as otherwise babel will
44+
# attempt to read package.json files in subdirs, such as
45+
# base/package.json
46+
in_path = urllib.parse.urljoin(
47+
self.temp_dir_in + "/", file.dirpath
48+
)
49+
os.makedirs(in_path, exist_ok=True)
50+
in_file = urllib.parse.urljoin(in_path + "/", file.file)
51+
shutil.copy2(file.path, in_file)
52+
out_path = urllib.parse.urljoin(
53+
self.temp_dir_out + "/", file.dirpath
54+
)
55+
file.dirpath = out_path
56+
os.chdir(".transpile/")
57+
out, err, status = popen_wrapper(
58+
[
59+
"npm",
60+
"run",
61+
"babel-transform-template-literals",
62+
"--",
63+
"--out-dir",
64+
self.temp_dir_out,
65+
self.temp_dir_in,
66+
]
67+
)
68+
os.chdir("../")
69+
70+
super().process_locale_dir(locale_dir, files)
71+
72+
def write_po_file(self, potfile, locale):
73+
if self.domain == "djangojs":
74+
with open(potfile, encoding="utf-8") as fp:
75+
msgs = fp.read()
76+
# Remove temp dir path info
77+
msgs = msgs.replace(self.temp_dir_out + "/", "")
78+
with open(potfile, "w", encoding="utf-8") as fp:
79+
fp.write(msgs)
80+
super().write_po_file(potfile, locale)

npm_mjs/management/commands/transpile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
import json
99
from npm_mjs.paths import PROJECT_PATH, TRANSPILE_CACHE_PATH, STATIC_ROOT
10-
from npm_mjs.tools import get_last_run, set_last_run
10+
from npm_mjs.tools import set_last_run
1111

1212
from urllib.parse import urljoin
1313
from django.core.management.base import BaseCommand
@@ -237,7 +237,7 @@ def handle(self, *args, **options):
237237
)
238238
transpile = {
239239
"OUT_DIR": out_dir,
240-
"VERSION": get_last_run("transpile"),
240+
"VERSION": start,
241241
"BASE_URL": transpile_base_url,
242242
"ENTRIES": entries,
243243
"STATIC_FRONTEND_FILES": list(

npm_mjs/management/commands/webpack.config.template.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ const baseRule = {
1414
}
1515
}
1616

17+
const predefinedVariables = {
18+
"transpile_VERSION": transpile.VERSION
19+
}
20+
1721
if (settings.DEBUG) {
1822
baseRule.exclude = /node_modules/
23+
predefinedVariables.staticUrl = `(url => (${settings.STATIC_URL} + url).replace(/[\/]+/g, '/'))`
24+
} else if (settings.STATICFILES_STORAGE !== "npm_mjs.storage.ManifestStaticFilesStorage") {
25+
predefinedVariables.staticUrl = `(url => (${settings.STATIC_URL} + url).replace(/[\/]+/g, '/') + "?v=" + transpile_VERSION)`
1926
}
2027

2128
module.exports = { // eslint-disable-line no-undef
@@ -29,9 +36,7 @@ module.exports = { // eslint-disable-line no-undef
2936
publicPath: transpile.BASE_URL
3037
},
3138
plugins: [
32-
new webpack.DefinePlugin({
33-
"transpile_VERSION": transpile.VERSION
34-
})
39+
new webpack.DefinePlugin(predefinedVariables)
3540
],
3641
entry: transpile.ENTRIES
3742
}

npm_mjs/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
"description": "Install dependencies for ES6 transpilation",
66
"private": true,
77
"dependencies": {
8-
"@babel/preset-env": "^7.12.1",
9-
"@babel/core": "7.14.6",
8+
"@babel/preset-env": "^7.19.1",
9+
"@babel/core": "7.19.1",
10+
"@babel/cli": "7.18.10",
1011
"@babel/plugin-syntax-dynamic-import": "7.8.3",
1112
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
12-
"babel-loader": "8.2.2",
13-
"webpack": "5.44.0",
14-
"webpack-cli": "4.7.2"
13+
"@babel/plugin-transform-template-literals": "^7.18.6",
14+
"babel-loader": "8.2.5",
15+
"webpack": "5.74.0",
16+
"webpack-cli": "4.10.0"
1517
}
1618
}

npm_mjs/storage.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
import posixpath
3+
import re
4+
from urllib.parse import unquote, urldefrag, urljoin
5+
6+
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage as DefaultManifestStaticFilesStorage
7+
from django.contrib.staticfiles.storage import HashedFilesMixin
8+
from django.conf import settings
9+
10+
def add_js_static_pattern(pattern):
11+
if pattern[0] == "*.js":
12+
templates = pattern[1] + ((
13+
'(?P<matched>staticUrl\\([\'"]{0,1}\\s*(?P<url>.*?)["\']{0,1}\\))',
14+
"'%(url)s'",
15+
))
16+
pattern = (pattern[0], templates)
17+
return pattern
18+
19+
20+
class ManifestStaticFilesStorage(DefaultManifestStaticFilesStorage):
21+
patterns = tuple(map(add_js_static_pattern, HashedFilesMixin.patterns))
22+
23+
def url_converter(self, name, hashed_files, template=None):
24+
"""
25+
Return the custom URL converter for the given file name.
26+
"""
27+
# Modified from
28+
# https://github.com/django/django/blob/main/django/contrib/staticfiles/storage.py
29+
# to handle absolute URLS
30+
31+
if template is None:
32+
template = self.default_template
33+
34+
def converter(matchobj):
35+
"""
36+
Convert the matched URL to a normalized and hashed URL.
37+
This requires figuring out which files the matched URL resolves
38+
to and calling the url() method of the storage.
39+
"""
40+
matches = matchobj.groupdict()
41+
matched = matches["matched"]
42+
url = matches["url"]
43+
44+
# Ignore absolute/protocol-relative and data-uri URLs.
45+
if re.match(r"^[a-z]+:", url):
46+
return matched
47+
48+
# Strip off the fragment so a path-like fragment won't interfere.
49+
url_path, fragment = urldefrag(url)
50+
51+
# Ignore URLs without a path
52+
if not url_path:
53+
return matched
54+
55+
if url_path.startswith("/"):
56+
# Absolute paths are assumed to have their root at STATIC_ROOT
57+
target_name = url_path[1:]
58+
else:
59+
# We're using the posixpath module to mix paths and URLs conveniently.
60+
source_name = name if os.sep == "/" else name.replace(os.sep, "/")
61+
target_name = posixpath.join(posixpath.dirname(source_name), url_path)
62+
63+
# Determine the hashed name of the target file with the storage backend.
64+
hashed_url = self._url(
65+
self._stored_name,
66+
unquote(target_name),
67+
force=True,
68+
hashed_files=hashed_files,
69+
)
70+
71+
transformed_url = "/".join(
72+
url_path.split("/")[:-1] + hashed_url.split("/")[-1:]
73+
)
74+
75+
# Restore the fragment that was stripped off earlier.
76+
if fragment:
77+
transformed_url += ("?#" if "?#" in url else "#") + fragment
78+
79+
if url_path.startswith("/"):
80+
transformed_url = urljoin(settings.STATIC_URL, transformed_url[1:])
81+
82+
# Return the hashed version to the file
83+
matches["url"] = unquote(transformed_url)
84+
return template % matches
85+
86+
return converter

npm_mjs/templatetags/transpile.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django import template
55
from django.templatetags.static import StaticNode, PrefixNode
66
from django.apps import apps
7+
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
78

89
from npm_mjs.tools import get_last_run
910

@@ -16,8 +17,10 @@ def handle_simple(cls, path):
1617
path = re.sub(r"^js/(.*)\.mjs", r"js/\1.js", path)
1718
if apps.is_installed("django.contrib.staticfiles"):
1819
from django.contrib.staticfiles.storage import staticfiles_storage
19-
20-
return staticfiles_storage.url(path) + "?v=%s" % get_last_run("transpile")
20+
if isinstance(staticfiles_storage, ManifestStaticFilesStorage):
21+
return staticfiles_storage.url(path)
22+
else:
23+
return staticfiles_storage.url(path) + "?v=%s" % get_last_run("transpile")
2124
else:
2225
return urljoin(
2326
PrefixNode.handle_simple("STATIC_URL"), quote(path)

npm_mjs/tools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def load_last_name():
1515

1616

1717
def get_last_run(name):
18+
global _last_run
1819
if name not in _last_run:
1920
load_last_name()
2021
if name not in _last_run:

0 commit comments

Comments
 (0)