Skip to content

Commit

Permalink
SSR fixes and reorg.
Browse files Browse the repository at this point in the history
  • Loading branch information
silviogutierrez committed Jul 26, 2018
1 parent 9c01f0c commit 06cb61b
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 37 deletions.
7 changes: 5 additions & 2 deletions client/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import React from 'react';
import {hydrate} from 'react-dom';
import {setStylesTarget} from "typestyle";

import FormView from './templates/FormView';

const props = (window as any).__PRELOADED_STATE__;

if ((module as any).hot) {
(module as any).hot.accept()
}

const Template = require('./templates/' + props.template_name + '.tsx').default;

hydrate(<Template {...props} />, document.getElementById('root'));

setStylesTarget(document.getElementById('styles-target')!);
2 changes: 1 addition & 1 deletion client/components/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import {FormType} from '../models';
import {FormType} from '../../models';
import {Widget, WidgetType} from './Widget';

/*
Expand Down
37 changes: 31 additions & 6 deletions client/components/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface BaseWidget {
interface TextInput extends BaseWidget {
type: 'text';
template_name: 'django/forms/widgets/text.html';
value: string;
value: string|null;
attrs: BaseWidget['attrs'] & {
maxlength?: number;
}
Expand All @@ -23,12 +23,34 @@ interface TextInput extends BaseWidget {
interface NumberInput extends BaseWidget {
type: 'number';
template_name: 'django/forms/widgets/number.html';
value: string;
value: string|null;
attrs: BaseWidget['attrs'] & {
step: string;
}
}

interface PasswordInput extends BaseWidget {
type: 'password';
template_name: 'django/forms/widgets/password.html';
value: string|null;
}

interface EmailInput extends BaseWidget {
type: 'email';
template_name: 'django/forms/widgets/email.html';
value: string|null;
}

interface Textarea extends BaseWidget {
template_name: 'django/forms/widgets/textarea.html';
value: string|null;
attrs: BaseWidget['attrs'] & {
cols: string;
rows: string;
}
}


type Optgroup = [
null,
[
Expand Down Expand Up @@ -63,7 +85,7 @@ interface SelectMultiple extends Select {
}
}

export type WidgetType = TextInput|NumberInput|Select|SelectMultiple;
export type WidgetType = TextInput|NumberInput|PasswordInput|EmailInput|Textarea|Select|SelectMultiple;

interface Props {
widget: WidgetType;
Expand Down Expand Up @@ -106,15 +128,18 @@ export const Widget = (props: Props) => {
)}
</select>;
}
case "django/forms/widgets/textarea.html":
return <textarea name={widget.name} defaultValue={widget.value || ""} />
case "django/forms/widgets/number.html":
case "django/forms/widgets/text.html": {
case "django/forms/widgets/text.html":
case "django/forms/widgets/password.html":
case "django/forms/widgets/email.html": {
return <input type={widget.type} defaultValue={widget.value || ""} name={widget.name} />;
// return <div>I am a text</div>;
}
default: {
console.log(widget);
const _exhaustiveCheck: never = widget;
throw new Error('Cannot happen');
throw new Error('Cannot happen, unknown widget type: \n' + JSON.stringify(widget, null, 2));
}
}
};
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
"typestyle": "^2.0.1",
"webpack": "^4.15.1",
"webpack-cli": "^3.1.0",
"webpack-dev-middleware": "^3.1.3"
"webpack-dev-middleware": "^3.1.3",
"webpack-dev-server": "^3.1.5"
},
"devDependencies": {},
"scripts": {
"build:schema": "../../bin/python manage.py generate_types_schema | json2ts > exports.tsx",
"build": "npm run build:schema && webpack -p",
"start": "NODE_PATH=./ nodemon --exec node_modules/.bin/ts-node --watch server/ --watch client/ -e ts,tsx index.tsx",
"start:server": "NODE_PATH=./ nodemon --exec node -r ts-node/register --preserve-symlinks --watch server/ --watch exports.tsx -e ts,tsx index.tsx",
"start:client": "webpack-dev-server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
Expand Down
19 changes: 14 additions & 5 deletions server/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import httpProxy, {ServerOptions} from 'http-proxy';

import webpackConfig from '../webpack.config';

/*
const compiler = webpack({
...webpackConfig,
mode: 'development',
});
*/

const app = express();

Expand All @@ -36,7 +38,7 @@ export const renderPage = ({html, css, props}: {html: string, css: string, props
</head>
<body>
<div id="root">${html}</div>
<script src="/media/dist/bundle.js"></script>
<script src="http://localhost:8080/media/dist/bundle.js"></script>
</body>
</html>
`;
Expand Down Expand Up @@ -69,24 +71,31 @@ export default {
const proxy = httpProxy.createProxyServer();

proxy.on('proxyRes', (proxyRes, req, res) => {
let body = Buffer.from('', 'utf8');
let body = Buffer.from('') //, 'utf8');

proxyRes.on('data', function (data) {
body = Buffer.concat([body, data as Buffer]);
});
proxyRes.on('end', function () {
const response = body.toString('utf8');
const response = body // .toString('utf8');

if ('raw' in (req as any).query || proxyRes.headers['content-type'] !== 'application/ssr+json') {
res.writeHead(proxyRes.statusCode!, proxyRes.headers)
res.end(response);
}
else {
const props = JSON.parse(response);
let body;

try {
const Template = require(`${process.cwd()}/client/templates/${props.template_name}.tsx`).default;
const props = JSON.parse(response.toString('utf8'));
const template_path = `${process.cwd()}/client/templates/${props.template_name}.tsx`;

// TODO: disable this in production.
if (process.env.NODE_ENV !== 'production') {
delete require.cache[template_path];
}

const Template = require(template_path).default;
const rendered = ReactDOMServer.renderToString(<Template {...props} />);
const css = getStyles();

Expand Down
38 changes: 37 additions & 1 deletion server/apps/ssr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django import forms
from django.http import HttpRequest, HttpResponse
from django.middleware.csrf import get_token

from typing import Any, Dict, Tuple, Union, Sequence, Mapping, TypeVar, Callable, Type, overload, Optional, cast, List
from typing import Any, Dict, Tuple, Union, Sequence, Mapping, TypeVar, Callable, Type, overload, Optional, cast, List, NamedTuple

from mypy_extensions import TypedDict, Arg, KwArg

Expand Down Expand Up @@ -244,3 +245,38 @@ def generate_schema() -> str:
'required': [name for name in type_registry.keys()],
}
return simplejson.dumps(schema, indent=4)


class WidgetType(TypeHint):
name = 'WidgetType'


class FieldType(NamedTuple):
name: str
label: str
widget: WidgetType


class FormType(NamedTuple):
errors: Dict[str, Optional[List[str]]]
fields: List[FieldType]


class SSRFormRenderer:
def render(self, template_name, context, request=None):
return simplejson.dumps(context)


def serialize_form(form: forms.BaseForm) -> FormType:
form.renderer = SSRFormRenderer()

return FormType(
errors=form.errors,
fields=[
FieldType(
widget=simplejson.loads(str(field))['widget'],
name=field.name,
label=str(field.label), # This can be a lazy proxy, so we must call str on it.
) for field in form
],
)
39 changes: 24 additions & 15 deletions server/apps/ssr/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,34 @@ class Template:
def __init__(self, jsx_template_name):
self.jsx_template_name = jsx_template_name

def get_props(self, context=None, request=None):
from server.apps.ssr import serialize_form

if self.jsx_template_name == 'registration/login.tsx':
return {
'form': serialize_form(context['form']),
}
elif self.jsx_template_name == 'flatpages/default.tsx':
flatpage = context['flatpage']

return {
'flatpage': {
'title': flatpage.title,
'url': flatpage.url,
'content': markdown.markdown(flatpage.content),
},
}
else:
assert False


def render(self, context=None, request=None):
flatpage = context['flatpage']
request._is_jsx_response = True

props = self.get_props(context, request)

return simplejson.dumps({
'csrf_token': get_token(request),
'template_name': self.jsx_template_name.replace('.tsx', ''),
'flatpage': {
'title': flatpage.title,
'url': flatpage.url,
'content': markdown.markdown(flatpage.content),
},
**props,
})
"""
if context is None:
context = {}
if request is not None:
context['request'] = request
context['csrf_input'] = csrf_input_lazy(request)
context['csrf_token'] = csrf_token_lazy(request)
return self.template.render(context)
"""
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"esModuleInterop": true,
"lib": ["dom", "es6"]
},
"include": [
"**/*.tsx"
"files": [
"client/app.tsx"
]
}
50 changes: 47 additions & 3 deletions webpack.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import webpack from 'webpack';
import path from 'path';

export default {
mode: 'development',
entry: './client/app.tsx',
module: {
rules: [
Expand All @@ -12,10 +14,52 @@ export default {
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
extensions: [ '.tsx', '.ts', '.js' ],
symlinks: false,
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
filename: 'dist/bundle.js',
publicPath: '/media/',
},
serve: {
devMiddleware: {
publicPath: '/media/',
},
},

devServer: {
// disableHostCheck: true,
// port: (DEBUG_PORT + 100),
// host: process.env.HOSTNAME || '0.0.0.0',
// public: '192.168.1.67:8080',
hot: true,
inline: true,
// progress: true,

// Display only errors to reduce the amount of output.
// stats: 'errors-only',
// https: isProductionBuild ? null : {
// cert: fs.readFileSync(path.resolve(__dirname, 'cert.crt')),
// key: fs.readFileSync(path.resolve(__dirname, 'cert.key')),
// },
// cert: '/Users/silviogutierrez/Sites/django/master.joyapp.com/src/master.joyapp.com/cert.crt',
// key: '/Users/silviogutierrez/Sites/django/master.joyapp.com/src/master.joyapp.com/cert.key',
proxy: {
'**': {
target: 'http://localhost:3001',
// secure: false,
// Lie and fake the https in our referer so that we can run
// webpack without SSL, but run jk
/*
onProxyReq: function(proxyReq, req) {
var referer = req.headers['referer'];
proxyReq.setHeader('referer', referer.replace('http:', 'https:'));
},
*/
},
},
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
};

0 comments on commit 06cb61b

Please sign in to comment.