├── .gitignore ├── config.js ├── test └── index.js ├── lib ├── getDefaultExport.js ├── normalizePort.js ├── configify.js ├── getLocaleMessages.js └── startServer.js ├── createDb.js ├── transform.js ├── index.js ├── commands ├── asset.js ├── db.js └── api.js ├── createLog.js ├── createClient.js ├── browser.js ├── cli.js ├── intl.js ├── Root.js ├── createRoot.js ├── createStore.js ├── createAssetServer.js ├── createApiServer.js ├── package.json ├── createStyle.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | yarn.lock 4 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('feathers-configuration')()() 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | var dogstack 4 | test('dogstack exports', t => { 5 | dogstack = require('../') 6 | t.truthy(dogstack) 7 | }) 8 | -------------------------------------------------------------------------------- /lib/getDefaultExport.js: -------------------------------------------------------------------------------- 1 | // interop when using babel 2 | module.exports = getDefaultExport 3 | function getDefaultExport (obj) { return obj && obj.__esModule ? obj.default : obj } 4 | -------------------------------------------------------------------------------- /createDb.js: -------------------------------------------------------------------------------- 1 | const Knex = require('knex') 2 | 3 | module.exports = createDb 4 | 5 | function createDb (config) { 6 | const env = process.env.NODE_ENV || 'development' 7 | return Knex(config[env]) 8 | } 9 | -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | const configify = require('./lib/configify') 2 | module.exports = function transform (filename, options) { 3 | const { config, _flags } = options 4 | return configify(filename, Object.assign({ _flags }, config)) 5 | } 6 | -------------------------------------------------------------------------------- /lib/normalizePort.js: -------------------------------------------------------------------------------- 1 | module.exports = normalizePort 2 | 3 | function normalizePort (val) { 4 | const port = parseInt(val, 10) 5 | 6 | if (isNaN(port)) { 7 | // named pipe 8 | return val 9 | } 10 | 11 | if (port >= 0) { 12 | // port number 13 | return port 14 | } 15 | 16 | return false 17 | } 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.assign( 2 | { 3 | createClient: require('./createClient'), 4 | createDb: require('./createDb'), 5 | createLog: require('./createLog'), 6 | createApiServer: require('./createApiServer'), 7 | createAssetServer: require('./createAssetServer'), 8 | createStore: require('./createStore'), 9 | Root: require('./Root') 10 | }, 11 | require('./createStyle') 12 | ) 13 | -------------------------------------------------------------------------------- /commands/asset.js: -------------------------------------------------------------------------------- 1 | const { basename } = require('path') 2 | const { assign } = Object 3 | 4 | module.exports = { 5 | command: 'asset', 6 | description: 'start asset server', 7 | handler: argv => { 8 | if (process.env.NODE_ENV === 'development') { 9 | require('longjohn') 10 | } 11 | 12 | const createAssetServer = require('../createAssetServer') 13 | const server = createAssetServer({}) 14 | const close = server() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /createLog.js: -------------------------------------------------------------------------------- 1 | const Pino = require('pino') 2 | const prettyPino = require('pino-colada') 3 | 4 | module.exports = createLog 5 | 6 | function createLog (options = {}) { 7 | const { 8 | name, 9 | level = 'info', 10 | pretty = process.env.NODE_ENV !== 'production', 11 | } = options 12 | 13 | var stream 14 | if (pretty) { 15 | stream = prettyPino() 16 | stream.pipe(process.stdout) 17 | } 18 | 19 | return Pino({ name, level }, stream) 20 | } 21 | -------------------------------------------------------------------------------- /commands/db.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | 3 | // reference: 4 | // https://github.com/tgriesser/knex/blob/master/bin/cli.js 5 | 6 | module.exports = { 7 | command: 'db [options]', 8 | description: 'uses knex, see full cli usage at http://knexjs.org/#Migrations-CLI', 9 | builder: {}, 10 | handler: argv => { 11 | // remove sub-command 'db' from args 12 | process.argv.splice(2, 1) 13 | 14 | // add --knexfile ./db 15 | const { cwd } = argv 16 | const dbConfigPath = join(cwd, 'db/index.js') 17 | process.argv.splice(2, 0, '--knexfile', dbConfigPath) 18 | 19 | require('knex/bin/cli') 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /createClient.js: -------------------------------------------------------------------------------- 1 | const feathers = require('feathers/client') 2 | const socketio = require('feathers-socketio/client') 3 | const hooks = require('feathers-hooks') 4 | const rx = require('feathers-reactive') 5 | const Rx = require('rxjs') 6 | const io = require('socket.io-client') 7 | 8 | module.exports = createClient 9 | 10 | function createClient (options) { 11 | const { 12 | services = [], 13 | config 14 | } = options 15 | 16 | const socket = io(config.api.url) 17 | 18 | const client = feathers() 19 | .configure(socketio(socket)) 20 | .configure(hooks()) 21 | .configure(rx(Rx)) 22 | 23 | services.map(service => { 24 | client.configure(service) 25 | }) 26 | 27 | return client 28 | } 29 | -------------------------------------------------------------------------------- /lib/configify.js: -------------------------------------------------------------------------------- 1 | const stringToStream = require('string-to-stream') 2 | const staticModule = require('static-module') 3 | const config = require('../config') 4 | const { PassThrough } = require('stream') 5 | 6 | module.exports = Configify 7 | 8 | function Configify (filename, options) { 9 | if (/\.json$/i.test(filename)) return new PassThrough() 10 | 11 | const { keys } = options 12 | 13 | return staticModule({ 14 | 'dogstack/config': function () { 15 | const browserConfig = keys.reduce((sofar, key) => { 16 | sofar[key] = config[key] 17 | return sofar 18 | }, {}) 19 | return stringToStream( 20 | 'function () { return ' + 21 | JSON.stringify(browserConfig) + 22 | ' }' 23 | ) 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | const ReactDOM = require('react-dom') 2 | const h = require('react-hyperscript') 3 | 4 | const createRoot = require('./createRoot') 5 | 6 | module.exports = createBrowserEntry 7 | 8 | function createBrowserEntry (options) { 9 | const { 10 | config, 11 | store, 12 | style, 13 | client, 14 | root, 15 | intl, 16 | routes, 17 | Layout 18 | } = options 19 | 20 | document.addEventListener('DOMContentLoaded', () => { 21 | const renderRoot = createRoot({ 22 | config, 23 | store, 24 | style, 25 | client, 26 | root, 27 | intl 28 | }) 29 | 30 | const appNode = document.querySelector(root.appNode) 31 | 32 | ReactDOM.render( 33 | renderRoot([ 34 | h(Layout, { routes }) 35 | ]), 36 | appNode 37 | ) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Cli = require('yargs') 4 | 5 | const argv = Cli 6 | .usage('Usage: $0 [options]') 7 | .commandDir('commands') 8 | .options({ 9 | help: { 10 | type: 'boolean', 11 | alias: 'h', 12 | description: 'Show help' 13 | }, 14 | version: { 15 | type: 'boolean', 16 | description: 'Show current version' 17 | }, 18 | cwd: { 19 | type: 'string', 20 | default: process.cwd(), 21 | description: 'Base directory from which the relative paths are resolved' 22 | } 23 | }) 24 | // must call a subcommand, --help, or --version 25 | .check(argv => argv.help || argv.version, false) 26 | .help() 27 | .argv 28 | 29 | if (!argv._[0] && argv.version) { 30 | const pkg = require('./package.json') 31 | console.log(pkg.version) 32 | } 33 | -------------------------------------------------------------------------------- /intl.js: -------------------------------------------------------------------------------- 1 | const h = require('react-hyperscript') 2 | const { FormattedMessage: OgFormattedMessage } = require('react-intl') 3 | const { isNil, merge } = require('ramda') 4 | 5 | // GK: react-intl's FormattedMessage component can't take className as props directly: https://github.com/yahoo/react-intl/issues/704 6 | // GK: hence we use a wrapper to pass in a child function: https://github.com/yahoo/react-intl/wiki/Components#string-formatting-components 7 | 8 | const classifyIntlMessage = (className) => { 9 | return { 10 | children: (...elements) => ( 11 | h('span', { className }, elements) 12 | ) 13 | } 14 | } 15 | 16 | const FormattedMessage = (props) => { 17 | const nextProps = isNil(props.className) 18 | ? props 19 | : merge(props, classifyIntlMessage(props.className)) 20 | return h(OgFormattedMessage, nextProps) 21 | } 22 | 23 | module.exports = { 24 | FormattedMessage 25 | } 26 | -------------------------------------------------------------------------------- /commands/api.js: -------------------------------------------------------------------------------- 1 | const { join, basename } = require('path') 2 | const { assign } = Object 3 | 4 | module.exports = { 5 | command: 'api', 6 | description: 'start api server', 7 | handler: argv => { 8 | if (process.env.NODE_ENV === 'development') { 9 | require('longjohn') 10 | } 11 | 12 | const createDb = require('../createDb') 13 | const createApiServer = require('../createApiServer') 14 | 15 | const { cwd } = argv 16 | const dbConfigPath = join(cwd, 'db/index.js') 17 | const appPath = join(cwd, 'server.js') 18 | 19 | require('babel-register') 20 | const dbConfig = require(dbConfigPath) 21 | const db = createDb(dbConfig) 22 | 23 | const serverOptions = getDefaultExport(require(appPath)) 24 | const server = createApiServer(assign({ db }, serverOptions)) 25 | const close = server() 26 | } 27 | } 28 | 29 | // interop when using babel 30 | function getDefaultExport (obj) { return obj && obj.__esModule ? obj.default : obj } 31 | -------------------------------------------------------------------------------- /lib/getLocaleMessages.js: -------------------------------------------------------------------------------- 1 | const { isNil, reverse, mergeAll, any, map } = require('ramda') 2 | 3 | module.exports = getLocaleMessages 4 | 5 | function getLocaleMessages (messagesByLocale, locale) { 6 | const subLocales = getSubLocales(locale) 7 | if (!any(nextLocale => !isNil(messagesByLocale[nextLocale]))(subLocales)) { 8 | throw new Error(`patch-intl: ${locale} locale not found in locales`) 9 | } 10 | const messagesBySubLocale = map(subLocale => messagesByLocale[subLocale])(subLocales) 11 | return mergeAll(reverse(messagesBySubLocale)) 12 | } 13 | 14 | // iterate through locale and parent locales 15 | // for example: en-US -> [en-US, en] 16 | function getSubLocales (locale) { 17 | var subLocales = [locale] 18 | while (locale.indexOf('-') !== -1) { 19 | const localeTags = locale.split('-') 20 | const parentLocaleTags = localeTags.slice(0, localeTags.length - 1) 21 | const parentLocale = parentLocaleTags.join('-') 22 | subLocales.push(parentLocale) 23 | locale = parentLocale 24 | } 25 | return subLocales 26 | } 27 | -------------------------------------------------------------------------------- /Root.js: -------------------------------------------------------------------------------- 1 | const h = require('react-hyperscript') 2 | const { Provider: ReduxProvider } = require('react-redux') 3 | const { ConnectedRouter } = require('react-router-redux') 4 | const { IntlProvider } = require('react-intl') 5 | 6 | const { StyleProvider } = require('./createStyle') 7 | const getLocaleMessages = require('./lib/getLocaleMessages') 8 | 9 | module.exports = Root 10 | 11 | function Root (props) { 12 | const { 13 | history, 14 | store, 15 | locale = navigator.language, 16 | messagesByLocale, 17 | styleRenderer, 18 | styleNode, 19 | styleTheme, 20 | children 21 | } = props 22 | 23 | const messages = getLocaleMessages(messagesByLocale, locale) 24 | 25 | return ( 26 | h(ReduxProvider, { 27 | store 28 | }, [ 29 | h(StyleProvider, { 30 | renderer: styleRenderer, 31 | mountNode: styleNode, 32 | theme: styleTheme 33 | }, [ 34 | h(IntlProvider, { 35 | locale, 36 | messages 37 | }, [ 38 | h(ConnectedRouter, { 39 | history 40 | }, children) 41 | ]) 42 | ]) 43 | ]) 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /lib/startServer.js: -------------------------------------------------------------------------------- 1 | module.exports = startServer 2 | 3 | function startServer (app, cb) { 4 | const port = app.get('port') 5 | const log = app.get('log') 6 | 7 | const server = app.listen(port, cb) 8 | 9 | server.on('error', onError) 10 | server.on('listening', onListening) 11 | 12 | return (cb) => { 13 | server.close(cb) 14 | } 15 | 16 | function onError (error) { 17 | if (error.syscall !== 'listen') { 18 | throw error 19 | } 20 | 21 | const bind = typeof port === 'string' 22 | ? 'Pipe ' + port 23 | : 'Port ' + port 24 | 25 | // handle specific listen errors with friendly messages 26 | switch (error.code) { 27 | case 'EACCES': 28 | log.fatal(bind + ' requires elevated privileges') 29 | return process.exit(1) 30 | case 'EADDRINUSE': 31 | log.fatal(bind + ' is already in use') 32 | return process.exit(1) 33 | default: 34 | throw error 35 | } 36 | } 37 | 38 | function onListening () { 39 | const addr = server.address() 40 | const bind = typeof addr === 'string' 41 | ? 'pipe ' + addr 42 | : 'port ' + addr.port 43 | log.info('Listening on ' + bind) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /createRoot.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const ReactDOM = require('react-dom') 3 | const { Provider: ReduxProvider } = require('react-redux') 4 | const { Provider: FelaProvider } = require('react-fela') 5 | const { ConnectedRouter } = require('react-router-redux') 6 | const createBrowserHistory = require('history/createBrowserHistory').default 7 | const h = require('react-hyperscript') 8 | const merge = require('ramda/src/merge') 9 | 10 | const Root = require('./Root') 11 | const createStore = require('./createStore') 12 | const { createStyleRenderer } = require('./createStyle') 13 | const createClient = require('./createClient') 14 | 15 | module.exports = createRoot 16 | 17 | function createRoot (options) { 18 | const { 19 | config, 20 | store: storeOptions, 21 | style: styleOptions, 22 | client: clientOptions, 23 | root: rootOptions, 24 | intl: intlOptions 25 | } = options 26 | 27 | const state = { config } 28 | const history = createBrowserHistory() 29 | const client = createClient(clientOptions) 30 | window.client = client 31 | 32 | const store = createStore( 33 | merge( 34 | { state, history, client }, 35 | storeOptions 36 | ) 37 | ) 38 | window.store = store 39 | 40 | const styleTheme = styleOptions.theme 41 | const styleRenderer = createStyleRenderer(styleOptions) 42 | const { locale, messagesByLocale } = intlOptions 43 | 44 | return (children) => { 45 | return h(Root, { 46 | history, 47 | store, 48 | locale, 49 | messagesByLocale, 50 | styleRenderer, 51 | styleTheme 52 | }, children) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /createStore.js: -------------------------------------------------------------------------------- 1 | require('rxjs') // require before redux-observable to ensure prototype methods added 2 | const { createStore: Store, applyMiddleware } = require('redux') 3 | const { createEpicMiddleware, combineEpics } = require('redux-observable') 4 | const { routerReducer, routerMiddleware } = require('react-router-redux') 5 | const { reducer: formReducer } = require('redux-form') 6 | const { concat: concatUpdaters, updateStateAt } = require('redux-fp') 7 | const { createLogger } = require('redux-logger') 8 | const { composeWithDevTools } = require('redux-devtools-extension') 9 | 10 | module.exports = createStore 11 | 12 | function createStore (options) { 13 | const { 14 | state, 15 | updater: appUpdater, 16 | epic, 17 | middlewares = [], 18 | enhancers = [], 19 | history, 20 | client 21 | } = options 22 | 23 | const enhancer = composeWithDevTools( 24 | applyMiddleware(...[ 25 | createEpicMiddleware(epic, { dependencies: { feathers: client } }), 26 | routerMiddleware(history), 27 | ...middlewares, 28 | createLogger() 29 | ]), 30 | ...enhancers 31 | ) 32 | 33 | const routerUpdater = updateStateAt('router', reducerToUpdater(routerReducer)) 34 | const formUpdater = updateStateAt('form', reducerToUpdater(formReducer)) 35 | 36 | const updater = concatUpdaters( 37 | appUpdater, 38 | routerUpdater, 39 | formUpdater 40 | ) 41 | const reducer = updaterToReducer(updater) 42 | const store = Store(reducer, state, enhancer) 43 | 44 | return store 45 | } 46 | 47 | function reducerToUpdater (reducer) { 48 | return action => state => reducer(state, action) 49 | } 50 | 51 | function updaterToReducer (updater) { 52 | return (state, action) => updater(action)(state) 53 | } 54 | -------------------------------------------------------------------------------- /createAssetServer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const url = require('url') 3 | const assert = require('assert') 4 | const { join, basename } = require('path') 5 | const merge = require('ramda/src/merge') 6 | const feathers = require('feathers') 7 | const configuration = require('feathers-configuration') 8 | const httpLogger = require('pino-http') 9 | const compress = require('compression') 10 | const helmet = require('helmet') 11 | const favicon = require('serve-favicon') 12 | const forceSsl = require('express-enforces-ssl') 13 | const propOr = require('ramda/src/propOr') 14 | const Bundler = require('bankai/http') 15 | 16 | const createLog = require('./createLog') 17 | const normalizePort = require('./lib/normalizePort') 18 | const startServer = require('./lib/startServer') 19 | 20 | const getEntryFile = propOr('browser.js', 'entry') 21 | 22 | module.exports = createServer 23 | 24 | function createServer (options) { 25 | const { 26 | cwd = process.cwd() 27 | } = options 28 | 29 | const app = feathers() 30 | // load config from ./config 31 | app.configure(configuration()) 32 | 33 | const logConfig = app.get('log') 34 | const log = createLog({ name: basename(cwd), level: logConfig.level }) 35 | 36 | app.set('log', log) 37 | 38 | const assetConfig = app.get('asset') 39 | assert(assetConfig, 'must set `asset` in config. example: "asset"') 40 | const assetUrl = url.parse(assetConfig.url) 41 | app.set('port', assetConfig.port) 42 | app.set('host', assetUrl.hostname) 43 | 44 | // log requests and responses 45 | app.use(httpLogger({ logger: log })) 46 | 47 | // gzip compression 48 | app.use(compress()) 49 | 50 | // http security headers 51 | app.use(helmet()) 52 | 53 | // favicon 54 | const faviconConfig = app.get('favicon') 55 | assert(faviconConfig, 'must set `favicon` in config. example: "app/favicon.ico"') 56 | app.use(favicon(faviconConfig)) 57 | 58 | // static files 59 | if (assetConfig.root) { 60 | app.use('/', feathers.static(assetConfig.root, assetConfig)) 61 | } 62 | 63 | // javascript bundler 64 | const entryFile = getEntryFile(assetConfig) 65 | const entryPath = join(cwd, entryFile) 66 | const bundlerHandler = Bundler(entryPath, { 67 | dirname: cwd 68 | }) 69 | const compiler = bundlerHandler.compiler 70 | app.use(bundlerHandler) 71 | compiler.on('error', (nodeName, edgeName, err) => { 72 | log.fatal(err) 73 | }) 74 | 75 | return (cb) => { 76 | return startServer(app, cb) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /createApiServer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const url = require('url') 3 | const assert = require('assert') 4 | const { join, basename } = require('path') 5 | const merge = require('ramda/src/merge') 6 | const forEachObjIndexed = require('ramda/src/forEachObjIndexed') 7 | const feathers = require('feathers') 8 | const httpLogger = require('pino-http') 9 | const compress = require('compression') 10 | const helmet = require('helmet') 11 | const cors = require('cors') 12 | const favicon = require('serve-favicon') 13 | const errorHandler = require('feathers-errors/handler') 14 | const configuration = require('feathers-configuration') 15 | const hooks = require('feathers-hooks') 16 | const rest = require('feathers-rest') 17 | const socketio = require('feathers-socketio') 18 | const forceSsl = require('express-enforces-ssl') 19 | 20 | const createLog = require('./createLog') 21 | const normalizePort = require('./lib/normalizePort') 22 | const startServer = require('./lib/startServer') 23 | 24 | module.exports = createServer 25 | 26 | function createServer (options) { 27 | const { 28 | cwd = process.cwd(), 29 | db, 30 | services = [] 31 | } = options 32 | 33 | const app = feathers() 34 | // load config from ./config 35 | app.configure(configuration()) 36 | 37 | const logConfig = app.get('log') 38 | const log = createLog({ name: basename(cwd), level: logConfig.level }) 39 | 40 | app.set('log', log) 41 | app.set('db', db) 42 | 43 | const apiConfig = app.get('api') 44 | const apiUrl = url.parse(apiConfig.url) 45 | app.set('port', apiConfig.port) 46 | app.set('host', apiUrl.hostname) 47 | 48 | // log requests and responses 49 | app.use(httpLogger({ logger: log })) 50 | 51 | if (app.get('env') === 'production') { 52 | app.enable('trust proxy') 53 | app.use(forceSsl()) 54 | } 55 | 56 | // gzip compression 57 | app.use(compress()) 58 | 59 | // http security headers 60 | app.use(helmet()) 61 | 62 | // cors requests 63 | const assetConfig = app.get('asset') 64 | app.use(cors({ 65 | origin: url.parse(assetConfig.url) // TODO: allow for whitelist to be passed 66 | })) 67 | 68 | // feathers hooks 69 | app.configure(hooks()) 70 | 71 | // transports 72 | app.configure(rest()) 73 | app.configure(socketio({ 74 | wsEngine: 'uws' 75 | })) 76 | 77 | // services (plugins) 78 | services.forEach(service => { 79 | app.configure(service) 80 | }) 81 | 82 | // log errors 83 | app.use(function (err, req, res, next) { 84 | if (err) console.error('error', err) 85 | next(err) 86 | }) 87 | 88 | // error handler 89 | app.use(errorHandler()) 90 | 91 | return (cb) => { 92 | return startServer(app, cb) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dogstack", 3 | "version": "1.0.0", 4 | "description": "a popular-choice grab-bag framework for teams working on production web apps", 5 | "main": "index.js", 6 | "browser": "browser.js", 7 | "bin": { 8 | "dogstack": "cli.js", 9 | "dog": "cli.js" 10 | }, 11 | "scripts": { 12 | "test:deps": "dependency-check . entry && dependency-check . entry --extra --no-dev -i es2040", 13 | "test:lint": "standard", 14 | "test": "npm-run-all -s test:node test:lint test:deps" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/enspiral-root-systems/dogstack.git" 19 | }, 20 | "keywords": [ 21 | "feathers", 22 | "redux", 23 | "react", 24 | "crud", 25 | "frp", 26 | "full", 27 | "stack", 28 | "framework", 29 | "production", 30 | "team" 31 | ], 32 | "author": "Root Systems ", 33 | "license": "Apache-2.0", 34 | "bugs": { 35 | "url": "https://github.com/enspiral-root-systems/dogstack/issues" 36 | }, 37 | "homepage": "https://dogstack.js.org", 38 | "browserify": { 39 | "transform": [ 40 | "es2040" 41 | ] 42 | }, 43 | "dependencies": { 44 | "@material-ui/core": "^1.0.0", 45 | "babel-register": "^6.24.1", 46 | "bankai": "^9.12.0", 47 | "compression": "^1.6.2", 48 | "config": "^1.30.0", 49 | "cors": "^2.8.4", 50 | "es2040": "^1.2.5", 51 | "express-enforces-ssl": "^1.1.0", 52 | "feathers": "^2.2.0", 53 | "feathers-configuration": "^0.4.2", 54 | "feathers-errors": "^2.8.0", 55 | "feathers-hooks": "^2.0.1", 56 | "feathers-reactive": "^0.4.1", 57 | "feathers-rest": "^1.7.3", 58 | "feathers-socketio": "^2.0.0", 59 | "fela": "^6.1.7", 60 | "fela-beautifier": "^5.0.11", 61 | "fela-monolithic": "^5.0.11", 62 | "fela-plugin-fallback-value": "^5.0.12", 63 | "fela-plugin-lvha": "^5.0.0", 64 | "fela-plugin-prefixer": "^5.0.12", 65 | "fela-plugin-validator": "^5.0.11", 66 | "helmet": "^3.6.0", 67 | "history": "^4.6.1", 68 | "knex": "^0.13.0", 69 | "longjohn": "^0.2.12", 70 | "pino": "^4.5.0", 71 | "pino-colada": "^1.4.0", 72 | "pino-http": "^2.6.1", 73 | "pump": "^1.0.2", 74 | "ramda": "^0.24.0", 75 | "react": "^16.4.0", 76 | "react-dom": "^16.4.0", 77 | "react-fela": "^7.2.0", 78 | "react-hyperscript": "^3.0.0", 79 | "react-intl": "^2.4.0", 80 | "react-redux": "^5.0.5", 81 | "react-router-redux": "5.0.0-alpha.6", 82 | "recompose": "^0.27.1", 83 | "redux": "^3.6.0", 84 | "redux-devtools-extension": "^2.13.2", 85 | "redux-form": "^7.0.4", 86 | "redux-fp": "^0.2.0", 87 | "redux-logger": "^3.0.6", 88 | "redux-observable": "^0.14.1", 89 | "rxjs": "^5.4.0", 90 | "serve-favicon": "^2.4.3", 91 | "socket.io-client": "^2.0.1", 92 | "static-module": "^1.3.2", 93 | "string-to-stream": "^1.1.0", 94 | "uws": "^0.14.5", 95 | "yargs": "^7.1.0" 96 | }, 97 | "devDependencies": { 98 | "ava": "^0.25.0", 99 | "dependency-check": "^2.7.0", 100 | "npm-run-all": "^4.0.1", 101 | "pg": "^6.2.2", 102 | "sqlite3": "^3.1.8" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /createStyle.js: -------------------------------------------------------------------------------- 1 | const getDefaultExport = require('./lib/getDefaultExport') 2 | 3 | const PropTypes = require('prop-types') 4 | const { createRenderer } = require('fela') 5 | const { Provider: FelaProvider, ThemeProvider: FelaThemeProvider } = require('react-fela') 6 | const h = require('react-hyperscript') 7 | const MuiThemeProvider = getDefaultExport(require('@material-ui/core/styles/MuiThemeProvider')) 8 | const createMuiTheme = getDefaultExport(require('@material-ui/core/styles/createMuiTheme')) 9 | 10 | // TODO publish preset `fela-preset-dogstack` 11 | // plugins and enhancers from https://github.com/cloudflare/cf-ui/blob/master/packages/cf-style-provider/src/index.js#L40 12 | 13 | const fallbackValue = getDefaultExport(require('fela-plugin-fallback-value')) 14 | const lvha = getDefaultExport(require('fela-plugin-lvha')) 15 | const validator = getDefaultExport(require('fela-plugin-validator')) 16 | const beautifier = getDefaultExport(require('fela-beautifier')) 17 | const monolithic = getDefaultExport(require('fela-monolithic')) 18 | const prefixer = getDefaultExport(require('fela-plugin-prefixer')) 19 | 20 | module.exports = { 21 | createStyleRenderer, 22 | StyleProvider 23 | } 24 | 25 | function createStyleRenderer (options) { 26 | const { 27 | plugins: userPlugins = [], 28 | enhancers: userEnhancers = [], 29 | setup = noop, 30 | prod = process.env.NODE_ENV === 'production', 31 | dev = !prod, 32 | selectorPrefix 33 | } = options 34 | 35 | // plugin order matters! 36 | // https://github.com/rofrischmann/fela/blob/master/docs/advanced/Plugins.md#order-matters 37 | 38 | var defaultPlugins = [] 39 | var defaultEnhancers = [] 40 | 41 | if (dev) { 42 | defaultPlugins = [lvha(), fallbackValue(), validator()] 43 | defaultEnhancers = [beautifier(), monolithic()] 44 | } 45 | 46 | if (prod) { 47 | defaultPlugins = [lvha(), prefixer(), fallbackValue()] 48 | } 49 | 50 | const plugins = [...defaultPlugins, ...userPlugins] 51 | const enhancers = [...defaultEnhancers, ...userEnhancers] 52 | 53 | const renderer = createRenderer({ 54 | plugins, 55 | enhancers, 56 | selectorPrefix 57 | }) 58 | 59 | setup(renderer) 60 | 61 | return renderer 62 | } 63 | 64 | function StyleProvider (options) { 65 | const { 66 | renderer, 67 | theme, 68 | children 69 | } = options 70 | 71 | const muiTheme = themeToMuiTheme(theme) 72 | 73 | return ( 74 | h(FelaProvider, { 75 | renderer 76 | }, [ 77 | h(FelaThemeProvider, { 78 | theme 79 | }, [ 80 | h(MuiThemeProvider, { 81 | theme: muiTheme 82 | }, children) 83 | ]) 84 | ]) 85 | ) 86 | } 87 | 88 | StyleProvider.defaultProps = { 89 | theme: {} 90 | } 91 | StyleProvider.propTypes = { 92 | renderer: PropTypes.object, 93 | theme: PropTypes.object, 94 | children: PropTypes.node.isRequired 95 | } 96 | 97 | function noop () {} 98 | 99 | function themeToMuiTheme (theme) { 100 | return createMuiTheme({ 101 | fontFamily: theme.fontFamily, 102 | palette: { 103 | primary1Color: theme.colors.primary1, 104 | primary2Color: theme.colors.primary2, 105 | primary3Color: theme.colors.primary3, 106 | accent1Color: theme.colors.accent1, 107 | accent2Color: theme.colors.accent2, 108 | accent3Color: theme.colors.accent3, 109 | textColor: theme.colors.text, 110 | alternateTextColor: theme.colors.alternateText, 111 | canvasColor: theme.colors.canvas, 112 | borderColor: theme.colors.border, 113 | shadowColor: theme.colors.shadow 114 | } 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | dogstack on a post-it note 7 |
8 | dogstack 9 |

10 | 11 |

12 | :dog: :dog: :dog: a popular-choice grab-bag framework for teams working on production web apps 13 |

14 | 15 |
16 | :cat: see also catstack, dogstack's smarter, slimmer, more cunning partner in crime 17 |
18 | 19 | ## features 20 | 21 | - abstracts away the app plumbing that you don't want to write again, and let's you focus on features 22 | - prescribes enough opinion to reduce friction for your team 23 | - is [omakase](https://www.youtube.com/watch?v=E99FnoYqoII), modules are hand-picked by expert chefs to deliver a consistent taste throughout 24 | - gives prescriptive opinions for how to structure production-scale apps 25 | 26 | ## examples 27 | 28 | - [dogstack.netlify.com](https://dogstack.netlify.com/): [dogstack-example](https://github.com/root-systems/dogstack-example) deployed to netlify / heroku 29 | 30 | ## documentation 31 | 32 | [dogstack.js.org](https://dogstack.gitbooks.io/handbook/content/) 33 | 34 | ## cli usage 35 | 36 | - [api](#api) 37 | - [asset](#asset) 38 | - [db](#db) 39 | 40 | ### api server 41 | 42 | starts api server 43 | 44 | ```shell 45 | dog api 46 | ``` 47 | 48 | ### asset server 49 | 50 | starts asset server 51 | 52 | ```shell 53 | dog asset 54 | ``` 55 | 56 | ### db 57 | 58 | Runs [`knex`](http://knexjs.org/#Migrations-CLI) command, with any arguments. 59 | 60 | ```shell 61 | dog db 62 | ``` 63 | 64 | ## api usage 65 | 66 | ### `server.js` 67 | 68 | 69 | export configuration for the [`feathers`](http://feathersjs.com) server 70 | 71 | - `services`: an array of functions that will be run with [`server.configure(service)`](https://docs.feathersjs.com/api/application.html#configurecallback) 72 | 73 | example: 74 | 75 | ```js 76 | // server.js 77 | export default { 78 | services: [ 79 | require('./agents/service') 80 | require('./accounts/service'), 81 | require('./authentication/service'), 82 | require('./profiles/service'), 83 | require('./relationships/service') 84 | ] 85 | } 86 | ``` 87 | 88 | ```js 89 | // agents/service.js 90 | import feathersKnex from 'feathers-knex' 91 | 92 | export default function () { 93 | const app = this 94 | const db = app.get('db') 95 | 96 | const name = 'dogs' 97 | const options = { Model: db, name } 98 | 99 | app.use(name, feathersKnex(options)) 100 | app.service(name).hooks(hooks) 101 | } 102 | 103 | const hooks = { 104 | before: {}, 105 | after: {}, 106 | error: {} 107 | } 108 | ``` 109 | 110 | ### `browser.js` 111 | 112 | dogstack exports a function `createBrowserEntry` out of `browser.js` with which to generate your dogstack client app. a dogstack app should have a file which calls this function with the required arguments, and which has it's name passed to `entry` as part of the `asset` [config](#config). 113 | 114 | example: 115 | ```js 116 | const createBrowserEntry = require('dogstack/browser') 117 | const Config = require('dogstack/config') 118 | const config = Config()() 119 | window.config = config 120 | 121 | // other imports of files needed for browser entry argument, as outlined in sections below 122 | 123 | createBrowserEntry({ 124 | config, 125 | store, 126 | style, 127 | client, 128 | root, 129 | intl, 130 | routes, 131 | Layout 132 | }) 133 | ``` 134 | 135 | explanations and examples of the parts that must be passed to `createBrowserEntry`: 136 | 137 | #### `config` 138 | a [feathers-configuration](https://github.com/feathersjs/configuration) compatible config object. Dogstack provides [`dogstack/config`](#configjs) as a wrapper around feathers-configuration to make this easy 139 | 140 | example: 141 | ```js 142 | // config/default.js 143 | module.exports = { 144 | favicon: 'app/favicon.ico', 145 | app: { 146 | name: 'Dogstack Example' 147 | }, 148 | api: { 149 | port: 3001, 150 | url: 'http://localhost:3001/', 151 | }, 152 | asset: { 153 | port: 3000, 154 | entry: 'browser.js', 155 | root: 'app/assets', 156 | url: 'http://localhost:3000/' 157 | } 158 | ``` 159 | 160 | #### `store` 161 | an object with `updater` and `epic` properties: 162 | - [`updater`](https://github.com/rvikmanis/redux-fp#updaters-vs-reducers): a function of shape `action => state => nextState`, combined from each topic using [`redux-fp.concat`](https://github.com/rvikmanis/redux-fp/blob/master/docs/API.md#concat) 163 | - [`epic`](https://redux-observable.js.org/): a function of shape `(action$, store, { feathers }) => nextAction$`, combined from each topic using [`combineEpics`](https://redux-observable.js.org/docs/api/combineEpics.html) 164 | 165 | example: 166 | ```js 167 | // store.js 168 | import updater from './updater' 169 | import epic from './epic' 170 | 171 | export default { 172 | updater, 173 | epic 174 | } 175 | ``` 176 | 177 | #### `style` 178 | an object with `theme` and `setup` properties: 179 | - `theme`: object passsed to `` 180 | - `setup`: function of shape `(renderer) => {}` 181 | 182 | example: 183 | ```js 184 | // style.js 185 | export default { 186 | theme: { 187 | colorPrimary: 'green', 188 | colorSecondary: 'blue' 189 | }, 190 | setup: (renderer) => { 191 | renderer.renderStatic( 192 | { fontFamily: 'Lato' }, 193 | 'html,body,#app' 194 | ) 195 | renderer.renderFont('Lato', [ 196 | 'https://fonts.gstatic.com/s/lato/v11/qIIYRU-oROkIk8vfvxw6QvesZW2xOQ-xsNqO47m55DA.woff' 197 | ]) 198 | } 199 | } 200 | ``` 201 | 202 | #### `client` 203 | configuration for [`feathers` client](https://docs.feathersjs.com/api/client.html), as an object with `services` and `config` properties: 204 | - `services`: an array of functions that will be run with [`client.configure(plugin)`](https://docs.feathersjs.com/api/application.html#configurecallback) 205 | - `apiUrl`: the url of the api server for the client to connect to (normally this can be extracted from your `config`) 206 | 207 | example: 208 | ```js 209 | // client.js 210 | export default { 211 | services: [ 212 | authentication 213 | ], 214 | config 215 | } 216 | ``` 217 | 218 | #### `root` 219 | a configuration object for the root React component with `appNode` and `styleNode` properties: 220 | - `appNode`: query selector string or dom node to render app content 221 | - `styleNode`: query selector string or dom node to render app styles 222 | 223 | example: 224 | ```js 225 | // root.js 226 | export default { 227 | appNode: '#app', 228 | styleNode: '#app-styles', 229 | } 230 | ``` 231 | 232 | #### `routes` 233 | an array of [React routes](https://github.com/ReactTraining/react-router) to be rendered as props into your top-level `Layout` component 234 | 235 | example: 236 | ```js 237 | // routes.js 238 | export default [ 239 | { 240 | name: 'home', 241 | path: '/', 242 | exact: true, 243 | Component: Home, 244 | selector: getIsNotAuthenticated, 245 | navigation: { 246 | title: 'app.home', 247 | icon: 'fa fa-home' 248 | } 249 | }, 250 | { 251 | name: 'dogs', 252 | path: '/', 253 | exact: true, 254 | Component: UserIsAuthenticated(DogsContainer), 255 | selector: getIsAuthenticated, 256 | navigation: { 257 | title: 'dogs.dogs', 258 | selector: getIsAuthenticated, 259 | icon: 'fa fa-paw' 260 | } 261 | }, 262 | { 263 | name: 'dog', 264 | path: '/d/:dogId', 265 | Component: UserIsAuthenticated(DogContainer) 266 | } 267 | ] 268 | ``` 269 | 270 | #### `Layout` 271 | your top-level rendered React component, which accepts `routes` as props 272 | 273 | example: 274 | - see the [dogstack-example Layout component](https://github.com/root-systems/dogstack-example/blob/master/app/components/Layout.js) 275 | 276 | ### `transform.js` 277 | 278 | exported browserify transform to be plugged in to your app's `package.json` 279 | - can be configured to whitelist particular config key / values to be available to the browser 280 | 281 | example: 282 | ```js 283 | // package.json 284 | ... 285 | "browserify": { 286 | "transform": [ 287 | // other transforms 288 | [ 289 | "dogstack/transform", 290 | { 291 | "config": { 292 | "keys": [ 293 | "api", 294 | "asset", 295 | "authentication" 296 | ] 297 | } 298 | } 299 | ] 300 | ] 301 | } 302 | ... 303 | ``` 304 | --------------------------------------------------------------------------------