diff --git a/actions.js b/actions.js index 1955bc1..639dca6 100644 --- a/actions.js +++ b/actions.js @@ -2,7 +2,8 @@ const {invokeHookSerial} = require('@truss/truss'); const {Responder} = require('./response.dev'); -const responder = new Responder(); +let responder = new Responder(); +responder.start(); module.exports = () => ({ @@ -12,10 +13,10 @@ module.exports = () => ({ delete headers.host; delete headers.connection; - return responder.respond(action).then((html) => { + return responder.respond(action).then(({html, headers: resHeaders}) => { return invokeHookSerial('truss/web-response', { status: 200, - headers: defaultHeaders(), + headers: {...defaultResponseHeaders(), ...resHeaders}, html, }, headers); }); @@ -26,10 +27,17 @@ module.exports = () => ({ }, }); -function defaultHeaders() { +function defaultResponseHeaders() { return { - Date: (new Date()).toUTCString(), - Connection: 'keep-alive', - 'Transfer-Encoding': 'chunked', + date: (new Date()).toUTCString(), + connection: 'keep-alive', }; } + +if (module.hot) { + module.hot.accept('./response.dev', () => { + responder.stop(); + responder = new require('./response.dev').Responder(); + responder.start(); + }); +} diff --git a/frontend/root/app.js b/frontend/app/index.js similarity index 78% rename from frontend/root/app.js rename to frontend/app/index.js index 18de741..d6b2dcb 100644 --- a/frontend/root/app.js +++ b/frontend/app/index.js @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React from 'react'; import classnames from 'classnames'; import {Route, Switch} from 'react-router'; @@ -16,10 +16,10 @@ function App() { ; } +import contempo from 'contempo'; +App = contempo(App, require('./index.scss')); + import {hot} from 'react-hot-loader'; App = hot(module)(App); -// import {styler} from '@truss/react' -// App = styler App, require './app.scss' - export default App; diff --git a/frontend/root/app.scss b/frontend/app/index.scss similarity index 100% rename from frontend/root/app.scss rename to frontend/app/index.scss diff --git a/frontend/root/welcome.js b/frontend/app/welcome.js similarity index 63% rename from frontend/root/welcome.js rename to frontend/app/welcome.js index 7589f1b..215c003 100644 --- a/frontend/root/welcome.js +++ b/frontend/app/welcome.js @@ -1,10 +1,8 @@ -// import {pkgman} from '@truss/core'; - import React from 'react'; + class Welcome extends React.Component { render() { - return

@@ -18,19 +16,13 @@ class Welcome extends React.Component { } thingsToDo() { - - // things = for key, value of pkgman.invoke 'perseaThingsToDo' - //
{value}
- - // if things.length is 0 const things =

You have nothing to do!

; - return
{things}
; } } -// import {styler} from '@truss/react' -// Welcome = styler Welcome, require './welcome.scss' +import contempo from 'contempo'; +Welcome = contempo(Welcome, require('./welcome.scss')); export default Welcome; diff --git a/frontend/root/welcome.scss b/frontend/app/welcome.scss similarity index 100% rename from frontend/root/welcome.scss rename to frontend/app/welcome.scss diff --git a/frontend/index.html b/frontend/index.html index de5139a..f30cb82 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@ - Truss + Persea
diff --git a/frontend/root/index.dev.js b/frontend/root/index.dev.js index 35cd4c0..591db2e 100644 --- a/frontend/root/index.dev.js +++ b/frontend/root/index.dev.js @@ -4,7 +4,8 @@ import {BrowserRouter} from 'react-router-dom'; import DevTools from './dev-tools'; -import App from './app'; +import App from '../app'; + function Root ({store}) { return @@ -19,7 +20,7 @@ function Root ({store}) { ; }; -// import {styler} from '@truss/react' -// Root = styler Root, require './index.scss' +import contempo from 'contempo'; +Root = contempo(Root, require('./index.scss')); export default Root; diff --git a/frontend/root/index.js b/frontend/root/index.js index 026a88d..f669d43 100644 --- a/frontend/root/index.js +++ b/frontend/root/index.js @@ -1,4 +1,3 @@ - if ('production' === process.env.NODE_ENV) { module.exports = require('./index.prod'); } diff --git a/frontend/root/index.prod.js b/frontend/root/index.prod.js index 13d5720..9dda690 100644 --- a/frontend/root/index.prod.js +++ b/frontend/root/index.prod.js @@ -2,7 +2,8 @@ import React from 'react'; import {Provider} from 'react-redux'; import {BrowserRouter} from 'react-router-dom'; -import App from './app'; +import App from '../app'; + function Root ({store}) { return @@ -12,7 +13,7 @@ function Root ({store}) { ; }; -// import {styler} from '@truss/react' -// Root = styler Root, require './index.scss' +import contempo from 'contempo'; +Root = contempo(Root, require('./index.scss')); export default Root; diff --git a/frontend/root/reducer.js b/frontend/root/reducer.js index 64e9be1..8e5d6de 100644 --- a/frontend/root/reducer.js +++ b/frontend/root/reducer.js @@ -1,20 +1,9 @@ import {combineReducers} from 'redux'; import {reducer as formReducer} from 'redux-form/immutable'; -// import {pkgman} from '@truss/core'; - -// import {createReducer as createResourceReducer} from '../resource' - export default function createRootReducer() { - // dynamicReducers = pkgman.invokeFlat('perseaReducers').reduce( - // ((reducers, map) -> {reducers..., map...}), {} - // ) - return combineReducers({ form: formReducer, - // resource: createResourceReducer() - - // dynamicReducers... }); } diff --git a/frontend/root/saga.js b/frontend/root/saga.js index 8d64e9c..10168d5 100644 --- a/frontend/root/saga.js +++ b/frontend/root/saga.js @@ -1,10 +1,8 @@ import {all, call} from 'redux-saga/effects' -function createRootSaga() { +export default function createRootSaga() { return function*() { return yield all([ ].map(call)); }; } - -export default createRootSaga; diff --git a/frontend/root/store.js b/frontend/root/store.js index 6323a4f..a5bfb6c 100644 --- a/frontend/root/store.js +++ b/frontend/root/store.js @@ -1,4 +1,3 @@ - if ('production' === process.env.NODE_ENV) { module.exports = require('./store.prod'); } diff --git a/frontend/root/store.prod.js b/frontend/root/store.prod.js index 930faa3..3981ee7 100644 --- a/frontend/root/store.prod.js +++ b/frontend/root/store.prod.js @@ -3,16 +3,19 @@ import {compose, createStore, applyMiddleware} from 'redux'; import createRootReducer from './reducer'; import createRootSaga from './saga'; + export default function createRootStore() { - // sagaMiddleware = createSagaMiddleware() + const sagaMiddleware = createSagaMiddleware() - // store = createStore createRootReducer(), compose( - // applyMiddleware sagaMiddleware - // ) - store = createStore(createRootReducer()); + const store = createStore( + createRootReducer(), + compose( + applyMiddleware(sagaMiddleware) + ) + ); - // sagaMiddleware.run(createRootSaga()); + sagaMiddleware.run(createRootSaga()); return store } diff --git a/package.json b/package.json index ff20792..46510ac 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@truss/truss": "1.x", "classnames": "^2.2.6", + "contempo": "1.x", "debug": "3.1.0", "html-webpack-plugin": "3.2.0", "immutable": "^3.8.2", diff --git a/response.dev.js b/response.dev.js index a85f2b9..9ace1e8 100644 --- a/response.dev.js +++ b/response.dev.js @@ -22,14 +22,19 @@ exports.Responder = class Responder { const address = this.address = httpServer.address(); const config = webpackConfig(); - config.entry.push( - `webpack-hot-middleware/client?path=http://${ + for (const i in config.entry) { + config.entry[i].unshift(`webpack-hot-middleware/client?path=http://${ frontendHostname}:${address.port - }/__webpack_hmr` - ); + }/__webpack_hmr`); + } const compiler = webpack(config); this.dm = wdm(compiler, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", + "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization" + }, stats: { chunkModules: true, colors: true, @@ -58,11 +63,15 @@ exports.Responder = class Responder { httpServer.on('error', console.error); - httpServer.listen(backendPort); + httpServer.on('close', () => httpServer.removeAllListeners()); + } + + start() { + this.server.listen(backendPort); } respond({payload: {headers, url}}) { - headers = Object.assign({}, headers); + headers = {...headers}; delete headers.host; delete headers.connection; @@ -85,10 +94,17 @@ exports.Responder = class Responder { data += chunk; }); res.on('end', () => { - resolve(data); + resolve({ + html: data, + headers: res.headers + }); }); }); client.on('error', reject); }); } + + stop() { + this.server.close(); + } } diff --git a/styles/forms.scss b/styles/forms.scss new file mode 100644 index 0000000..70e10fa --- /dev/null +++ b/styles/forms.scss @@ -0,0 +1,94 @@ +@import './mixins.scss'; + +button, input, select { + background-color: #333; + color: #CCC; +} + +button { + border: 1px solid #333; + padding: 0; + + &:hover { + background-color: #555; + } +} + +label { + border: none; + border-bottom: 1px solid #333; + margin: 0.25em 0; + display: block; + font-size: 0.8em; + padding: 0.2em; +} + +input { + border: 1px solid #333; + text-align: right; +} + +.aside { + padding: 0.2em; +} + +select { + height: 1.5em; + line-height: 0; + text-align-last: right; +} + +button, select, input { + border: 1px solid #333; +} + +form { + margin-top: 2em; + + @media (min-width: 1024px) { + margin-top: 0; + } +} + +.form-row { + @include group; + margin-bottom: 1em; + + > .control { + float: left; + } +} + +.control { + @include group; + + margin-left: 0.75em; + margin-right: 0.75em; + text-align: right; + + label { + text-align: left; + } + + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } +} + +.truss-number { + + button { + background-color: #333; + + &:hover { + background-color: #555; + } + + span { + color: #ccc; + } + } +} diff --git a/styles/global.scss b/styles/global.scss new file mode 100644 index 0000000..a96633f --- /dev/null +++ b/styles/global.scss @@ -0,0 +1,55 @@ +html, body, .app, .root { + width: 100%; + height: 100%; +} + +.aside { + font-size: 0.8em; +} + +button { + &:hover { + background-color: #555; + } +} + +button, input, select, option { + + &[disabled] { + filter: grayscale(1); + opacity: 0.5; + + &:hover { + background-color: transparent; + } + } + + &:focus { + outline: none; + box-shadow: inset 0 0 1pt 0.5pt #CCC, 0 0 1pt 0.5pt #CCC + } +} + +hr { + border: none; + border-bottom: 1px solid #333; + margin: 1em 0; +} + +.loading { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + width: 100%; + + svg { + height: 25%; + width: 25%; + } +} + +.editor.avocado-environment { + height: 100%; + width: 100%; +} diff --git a/styles/mixins.scss b/styles/mixins.scss new file mode 100644 index 0000000..6e7d0e3 --- /dev/null +++ b/styles/mixins.scss @@ -0,0 +1,8 @@ +@mixin group { + &:after { + content: ""; + display: table; + clear: both; + } +} + diff --git a/styles/reset.css b/styles/reset.css new file mode 100644 index 0000000..a583b75 --- /dev/null +++ b/styles/reset.css @@ -0,0 +1,60 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + box-sizing: border-box; + color: #CCCCCC; + font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; + overflow: hidden; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +ins { + text-decoration: none; +} +select { + font-size: inherit; +} +button { + line-height: 0; +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 404dc3e..ab2243a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -17,20 +17,37 @@ module.exports = (config) => { const SOURCE_PATH = process.env.SOURCE_PATH || '/var/node/src'; const OUTPUT_PATH = process.env.OUTPUT_PATH || '/var/node/dist'; +const hashFormat = { + chunk: ('production' === process.env.NODE_ENV) ? '.[chunkhash:20]' : '', +} + module.exports.webpackConfig = function() { + const styleDirectory = path.join(SOURCE_PATH, 'styles'); + const cssPrefix = '!style-loader!css-loader!'; + const scssPrefix = `${cssPrefix}sass-loader!`; + const config = { mode: 'production' !== process.env.NODE_ENV ? 'development' : 'production', - entry: [ - '@babel/polyfill', - path.join(SOURCE_PATH, 'frontend', 'index.js'), - ], + entry: { + index: [ + '@babel/polyfill', + path.join(SOURCE_PATH, 'frontend', 'index.js'), + ], + style: [ + path.join(cssPrefix, styleDirectory, 'reset.css'), + path.join(scssPrefix, styleDirectory, 'global.scss'), + path.join(scssPrefix, styleDirectory, 'forms.scss'), + ], + }, optimization: {}, module: { rules: [ { test: /\.js$/, - exclude: /(node_modules|bower_components)/, + exclude: [ + /(node_modules\/(?!contempo))/, + ], use: { loader: 'babel-loader', options: { @@ -39,6 +56,7 @@ module.exports.webpackConfig = function() { ], plugins: [ '@babel/plugin-proposal-object-rest-spread', + 'react-hot-loader/babel', ], presets: [ '@babel/preset-react', @@ -47,10 +65,31 @@ module.exports.webpackConfig = function() { }, }, }, + { + test: /\.css$/, + use: [{ + loader: 'raw-loader', + }], + }, + { + test: /\.scss$/, + use: [{ + loader: 'raw-loader', + }, { + loader: 'sass-loader', + options: { + sourceMap: 'production' !== process.env.NODE_ENV, + }, + }], + }, ], }, + node: { + fs: 'empty', + }, output: { - filename: 'index.js', + filename: `[name]${hashFormat.chunk}.js`, + chunkFilename: `[id]${hashFormat.chunk}.chunk.js`, path: path.join(OUTPUT_PATH, 'frontend'), }, plugins: [ @@ -61,7 +100,10 @@ module.exports.webpackConfig = function() { ], resolve: { modules: [path.join(OUTPUT_PATH, 'node_modules')], - } + }, + resolveLoader: { + modules: [path.join(OUTPUT_PATH, 'node_modules')], + }, }; if ('production' === config.mode) { @@ -70,6 +112,7 @@ module.exports.webpackConfig = function() { ]; } else { + config.devtool = 'eval-source-map'; config.plugins.push(new webpack.HotModuleReplacementPlugin()); }