-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
asset_manifest.py
125 lines (105 loc) · 5.5 KB
/
asset_manifest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import os
import json
import re
from django.conf import settings
from cra_helper.logging import logger
from cra_helper.server_check import hosted_by_liveserver
_asset_filename = 'asset-manifest.json'
# Sanitize characters that Django's `static` template tag can't handle
def clean_file_key(filename: str) -> str:
cleaned = filename.replace('.', '_')
cleaned = cleaned.replace('/', '_')
cleaned = cleaned.replace('~', '_')
cleaned = cleaned.replace('-', '_')
return cleaned
# Create a dictionary of keys that can be passed to the `static` template tag to load CRA assets
# from Django's STATIC_ROOT directory
def generate_manifest(cra_url: str, app_dir: str) -> dict:
manifest = {}
# The ability to access this file means the create-react-app liveserver is running
bundle_path = '{}/static/js/bundle.js'.format(cra_url)
# Check if Create-React-App live server is up and running
is_server_live = hosted_by_liveserver(bundle_path)
# Prepare references to various files frontend
if is_server_live:
logger.info('Create-React-App liveserver is running')
# Earlier versions of CRA included everything in a single bundle.js...
manifest['bundle_js'] = [
bundle_path,
]
# ...while more recent versions of CRA use code-splitting and serve additional files
liveserver_bundles = [
# These two files will alternate being loaded into the page
'{}/static/js/0.chunk.js'.format(cra_url),
'{}/static/js/1.chunk.js'.format(cra_url),
# This bundle seems to contain some vendor files
'{}/static/js/main.chunk.js'.format(cra_url)
]
for url in liveserver_bundles:
if hosted_by_liveserver(url):
manifest['bundle_js'].append(url)
else:
logger.info('Create-React-App liveserver is not running')
build_dir = os.path.join(app_dir, 'build')
# Add the CRA static directory to STATICFILES_DIRS so collectstatic can grab files in there
static_dir = os.path.join(build_dir, 'static')
settings.STATICFILES_DIRS += [static_dir]
# CRA generates a JSON file that maps typical filenames to their hashed filenames
manifest_path = os.path.join(build_dir, _asset_filename)
# Try to load the JSON manifest from the React build directory first
try:
with open(manifest_path) as data_file:
logger.info('found manifest in React build files')
data = json.load(data_file)
except Exception as e:
# If that doesn't work, try to load it from the Django project's static files directory
try:
static_manifest_path = os.path.join(settings.STATIC_ROOT, _asset_filename)
with open(static_manifest_path) as data_file:
logger.info('found manifest in static files')
data = json.load(data_file)
except Exception as e:
logger.error('can\'t load static asset manifest: {}'.format(e))
return {}
# Older versions of Create-React-App (through v2.1.8) had flat manifests. If a "files"
# property doesn't exist in the manifest JSON, then we're probably working with an older
# version of CRA.
if data.get('files') is None:
manifest_items = data.items()
else:
manifest_items = data.get('files').items()
# Prepare a regex that'll help us detect and remove the leading "/static/" from manifest
# filepaths. Support relative path prefixes that might also prefix "/static/".
static_base_path = re.compile(r'^(?:/[\w.-]+)?/static/', re.IGNORECASE)
for file_key, path in manifest_items:
# Don't include index.html, serviceWorker.js, etc...
if static_base_path.match(path):
# Generate paths relative to our bundled assets
# Ex: /static/css/main.99358b65.chunk.css => css/main.99358b65.chunk.css
manifest[clean_file_key(file_key)] = re.sub(static_base_path, '', path)
# Later versions of Create-React-App (starting with v3.2.0) will tell us which files to
# load via an "entrypoints" array
entrypoints = data.get('entrypoints')
if entrypoints is not None:
# Prepare to group entrypoint files by extension
manifest['entrypoints'] = {
'js': [],
'css': [],
};
# Map manifest files by their path
mapped_manifest_items = {}
for file_key, path in manifest_items:
# Paths in "entrypoints" in asset-manifest.json don't include the relative path
# that might be set to "homepage" in package.json. Convert the asset file paths in
# "files" in asset-manifest.json from `/frontend/static/...` to `static/...` so
# that our truncated manifest file paths generated above can be matched to
# the "entrypoints" paths
normalized_path = re.sub(static_base_path, 'static/', path)
mapped_manifest_items[normalized_path] = clean_file_key(file_key)
for path in entrypoints:
rel_static_path = manifest[mapped_manifest_items[path]]
if path.endswith('.js'):
manifest['entrypoints']['js'].append(rel_static_path)
elif path.endswith('.css'):
manifest['entrypoints']['css'].append(rel_static_path)
return manifest