Skip to content

Commit 7e59b16

Browse files
committed
Removed automatic translation. Added component bundling and mount functionality.
Babel translation is now opt-in, rather than the default. Rather than using Babel's require hook to load components, it is now used as part of the bundling process. This removes the massive overhead that it introduced when loading files in the same process as the renderer. It also removes the requirement for translated files to use the `jsx` extension, which had been used as a whitelist hack to get around the aforementioned overhead. If you want to continue as before, just add `translate=True` to your render_component calls. The bundling/mounting functionality is similar to what previously existed when django-react and django-webpack were more tightly coupled. The primary difference is that it is now opt-in, rather than the default. Re markfinger#24
1 parent 6f7b0cf commit 7e59b16

14 files changed

Lines changed: 487 additions & 375 deletions

File tree

django_react/bundle.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import os
2+
import re
3+
import tempfile
4+
from django_webpack.compiler import webpack
5+
6+
COMPONENT_CONFIG_FILES = {}
7+
8+
9+
def bundle_component(path, translate=None, var=None, watch=None):
10+
filename = get_component_config_filename(path, translate, var)
11+
return webpack(filename)
12+
13+
14+
def get_var_from_path(path):
15+
var = '{parent_dir}__{filename}'.format(
16+
parent_dir=os.path.basename(os.path.dirname(path)),
17+
filename=os.path.splitext(os.path.basename(path))[0]
18+
)
19+
return re.sub(r'\W+', '_', var)
20+
21+
22+
def get_webpack_config(path, translate=None, var=None):
23+
config = 'module.exports = {'
24+
25+
if var is None:
26+
var = get_var_from_path(path)
27+
28+
config += '''
29+
context: '{dir}',
30+
entry: '{file}',
31+
output: {{
32+
path: '[bundle_dir]/components',
33+
filename: '{var}-[hash].js',
34+
libraryTarget: 'umd',
35+
library: '{var}'
36+
}},
37+
externals: ['react'],
38+
devtool: 'eval\''''.format(
39+
dir=os.path.dirname(path),
40+
file='./' + os.path.basename(path),
41+
var=var,
42+
)
43+
44+
if translate:
45+
# JSX + ES6/7 support
46+
config += ''',
47+
module: {{
48+
loaders: [{{
49+
test: /\{ext}$/,
50+
exclude: /node_modules/,
51+
loader: 'babel-loader'
52+
}}]
53+
}},
54+
resolveLoader: {{
55+
root: '{node_modules}'
56+
}}'''.format(
57+
ext=os.path.splitext(path)[-1],
58+
node_modules=os.path.join(os.path.dirname(__file__), 'services', 'node_modules')
59+
)
60+
61+
return config + '\n};'
62+
63+
64+
def get_component_config_filename(path, translate=None, var=None):
65+
cache_key = (path, translate, var)
66+
if cache_key in COMPONENT_CONFIG_FILES:
67+
return COMPONENT_CONFIG_FILES[cache_key]
68+
69+
config = get_webpack_config(path, translate, var)
70+
filename = tempfile.mkstemp(suffix='.webpack.config.js')[1]
71+
with open(filename, 'w') as config_file:
72+
config_file.write(config)
73+
74+
COMPONENT_CONFIG_FILES[cache_key] = filename
75+
76+
return filename

django_react/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ class ComponentSourceFileNotFound(Exception):
33

44

55
class ComponentRenderingError(Exception):
6+
pass
7+
8+
9+
class ComponentWasNotBundled(Exception):
610
pass

django_react/render.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,97 @@
33
from django.contrib.staticfiles import finders
44
from django.core.serializers.json import DjangoJSONEncoder
55
from django.utils.safestring import mark_safe
6-
from .exceptions import ComponentSourceFileNotFound
6+
from .exceptions import ComponentSourceFileNotFound, ComponentWasNotBundled
77
from .services import RenderService
88
from .settings import WATCH_SOURCE
9+
from .bundle import bundle_component
910

1011
service = RenderService()
1112

1213

1314
class RenderedComponent(object):
14-
def __init__(self, output, path_to_source, props, serialized_props, watch_source):
15-
self.output = output
15+
def __init__(self, markup, path_to_source, props, serialized_props, watch_source, bundle, to_static_markup):
16+
self.markup = markup
1617
self.path_to_source = path_to_source
1718
self.props = props
1819
self.serialized_props = serialized_props
1920
self.watch_source = watch_source
21+
self.bundle = bundle
22+
self.to_static_markup = to_static_markup
2023

2124
def __str__(self):
22-
return mark_safe(self.output)
25+
return self.render_markup()
2326

2427
def __unicode__(self):
25-
return mark_safe(self.output)
28+
return self.render_markup()
29+
30+
def render_markup(self):
31+
markup = self.markup
32+
if self.bundle and not self.to_static_markup:
33+
markup = '<span id="{id}">{markup}</span>'.format(
34+
id=self.get_container_id(),
35+
markup=markup,
36+
)
37+
return mark_safe(markup)
2638

2739
def render_props(self):
2840
if self.serialized_props:
2941
return mark_safe(self.serialized_props)
3042
return ''
3143

44+
def get_bundle(self):
45+
if not self.bundle:
46+
raise ComponentWasNotBundled((
47+
'The component "{path}" was not bundled during the rendering process. '
48+
'Call render_component with `bundle`, `translate`, or `watch_source` '
49+
'keyword arguments set to `True` to ensure that it is bundled.'
50+
).format(path=self.path_to_source))
51+
return self.bundle
52+
53+
def get_var(self):
54+
return self.get_bundle().get_library()
55+
56+
def get_container_id(self):
57+
return 'reactComponent-' + self.get_var()
58+
59+
def get_props_var(self):
60+
return self.get_var() + '__props'
3261

33-
def render_component(path_to_source, props=None, to_static_markup=None, watch_source=None, json_encoder=None):
62+
def render_mount_js(self):
63+
mount_js = '''if (typeof React === 'undefined') throw new Error('Cannot find `React` global variable. Have you added a script element to this page which points to React?');
64+
if (typeof {var} === 'undefined') throw new Error('Cannot find component variable `{var}`');
65+
(function(React, component, containerId) {{
66+
var props = {props};
67+
var element = React.createElement(component, props);
68+
var container = document.getElementById(containerId);
69+
if (!container) throw new Error('Cannot find the container element `#{container_id}` for component `{var}`');
70+
React.render(element, container);
71+
}})(React, {var}, '{container_id}');'''
72+
return mark_safe(
73+
mount_js.format(
74+
var=self.get_var(),
75+
props=self.serialized_props or 'null',
76+
container_id=self.get_container_id()
77+
)
78+
)
79+
80+
def render_js(self):
81+
return mark_safe(
82+
'\n{bundle}\n<script>\n{mount_js}\n</script>\n'.format(
83+
bundle=self.get_bundle().render(),
84+
mount_js=self.render_mount_js(),
85+
)
86+
)
87+
88+
89+
def render_component(
90+
# Rendering options
91+
path_to_source, props=None, to_static_markup=None,
92+
# Bundling options
93+
bundle=None, translate=None, watch_source=None,
94+
# Prop handling
95+
json_encoder=None
96+
):
3497
if not os.path.isabs(path_to_source):
3598
absolute_path_to_source = finders.find(path_to_source)
3699
if not absolute_path_to_source:
@@ -40,6 +103,11 @@ def render_component(path_to_source, props=None, to_static_markup=None, watch_so
40103
if not os.path.exists(path_to_source):
41104
raise ComponentSourceFileNotFound(path_to_source)
42105

106+
bundled_component = None
107+
if bundle or translate or watch_source:
108+
bundled_component = bundle_component(path_to_source, translate=translate, watch=watch_source)
109+
path_to_source = bundled_component.get_assets()[0]['path']
110+
43111
if watch_source is None:
44112
watch_source = WATCH_SOURCE
45113

@@ -51,6 +119,8 @@ def render_component(path_to_source, props=None, to_static_markup=None, watch_so
51119
else:
52120
serialized_props = None
53121

54-
output = service.render(path_to_source, serialized_props, to_static_markup, watch_source)
122+
markup = service.render(path_to_source, serialized_props, to_static_markup)
55123

56-
return RenderedComponent(output, path_to_source, props, serialized_props, watch_source)
124+
return RenderedComponent(
125+
markup, path_to_source, props, serialized_props, watch_source, bundled_component, to_static_markup
126+
)

django_react/services/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ class RenderService(BaseService):
1010
path_to_source = os.path.join(os.path.dirname(__file__), 'render.js')
1111
package_dependencies = os.path.dirname(__file__)
1212

13-
def render(self, path_to_source, serialized_props, to_static_markup, watch_source):
13+
def render(self, path_to_source, serialized_props, to_static_markup):
1414
try:
1515
response = self.send(
16-
path_to_source=path_to_source,
17-
serialized_props=serialized_props,
18-
to_static_markup=to_static_markup,
19-
watch_source=watch_source,
16+
path=path_to_source,
17+
serializedProps=serialized_props,
18+
toStaticMarkup=to_static_markup
2019
)
2120
except NodeServiceError as e:
2221
six.reraise(ComponentRenderingError, ComponentRenderingError(*e.args), sys.exc_info()[2])

django_react/services/package.json

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
{
22
"private": true,
33
"dependencies": {
4-
"babel": "^4.7.5",
5-
"babel-core": "^4.7.5",
6-
"babel-loader": "^4.1.0",
7-
"lodash": "^3.5.0",
8-
"resolve": "^1.1.5",
9-
"tmp": "0.0.25",
10-
"webpack": "^1.7.2",
11-
"webpack-watcher": "git://github.com/markfinger/webpack-watcher#b95b031434e4cdb52f2640d56be28b95429bc9ce"
4+
"babel-core": "^5.0.12",
5+
"babel-loader": "^5.0.0",
6+
"react": "^0.13.1",
7+
"react-render": "^0.1.0",
8+
"webpack": "^1.8.4"
129
}
1310
}

0 commit comments

Comments
 (0)