def generate_manifest(cra_url: str, app_dir: str, cra_server_url: Optional[str] = None) -> dict: if cra_server_url is None: cra_server_url = cra_url 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 bundle_server_path = '{}/static/js/bundle.js'.format(cra_server_url) is_server_live = hosted_by_liveserver(bundle_server_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, ] as_client_server_tuple: Callable[[str], Tuple[ str, str]] = lambda url: (url.format(cra_url), url.format(cra_server_url)) # ...while more recent versions of CRA use code-splitting and serve additional files liveserver_bundles = map( as_client_server_tuple, [ # These two files will alternate being loaded into the page '{}/static/js/0.chunk.js', '{}/static/js/1.chunk.js', # This bundle seems to contain some vendor files '{}/static/js/main.chunk.js', ]) for url, server_url in liveserver_bundles: if hosted_by_liveserver(server_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
def test_returns_false_when_cra_not_running(self, mock_urlopen): mock_urlopen.side_effect = url_error.URLError('CRA not running') self.assertFalse(server_check.hosted_by_liveserver(CRA_URL))
def test_returns_false_when_cra_errors(self, mock_urlopen): mock_urlopen.return_value = MagicMock(status=500) self.assertFalse(server_check.hosted_by_liveserver(CRA_URL))
def test_returns_false_when_cra_running_and_production(self, mock_urlopen): mock_urlopen.return_value = MagicMock(status=200) setattr(settings, 'DEBUG', False) self.assertFalse(server_check.hosted_by_liveserver(CRA_URL))
def test_returns_true_when_cra_running_and_debug(self, mock_urlopen): mock_urlopen.return_value = MagicMock(status=200) self.assertTrue(server_check.hosted_by_liveserver(CRA_URL))
def test_returns_boolean(self): self.assertIsInstance(server_check.hosted_by_liveserver(CRA_URL), bool)