diff --git a/README.md b/README.md index 3945022..d4630e8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,29 @@ arch-cli serve your application is now running on . +## Configuration + +Arch is configurable by environment variables or a `arch.config.js` / `arch.config.ls` file in your project root. + +Hardcoded config (passed to arch.server) takes precedence over environment variables. + +##### List of configuration options + +| option | env variable | description | default | +|-------------|----------------------------|---------------------------------------------------------|-----------------------------------| +| appPath | arch_app_path | absolute path to app directory ** | your app's package.json directory | +| archPath | arch_arch_path | absolute path to arch directory ** | arch's package.json directory | +| bundle | arch_bundle | handle bundling of js in arch | true in development | +| debug | arch_debug | show debug output | false | +| environment | arch_environment, NODE_ENV | environment for arch to target | development | +| minify | arch_minify | minify client output | true in production | +| public | arch_public | asset path (relative to app path) | 'dist' | +| port | arch_port, ARCH_PORT* | port to listen on | 3000 | +| watch | arch_watch | watch for fs changes and reload server + rebuild client | true in development | + + * Will be deprecated + ** You probably never need to touch this + ## Documentation Arch doesn't have a website yet, but you can [read the diff --git a/lib/application.js b/lib/application.js index 0872ce1..c48988a 100644 --- a/lib/application.js +++ b/lib/application.js @@ -4,7 +4,7 @@ cursor = require('./cursor'); dom = require('./dom'); routes = require('./routes'); - serverRendering = require('./server-rendering'); + serverRendering = require('./server/server-rendering'); unescape = require('lodash/string/unescape'); domUtils = require('./virtual-dom-utils'); span = dom.span; diff --git a/lib/bundler.js b/lib/bundler.js index 10e17c5..fb7cfb8 100644 --- a/lib/bundler.js +++ b/lib/bundler.js @@ -1,59 +1,25 @@ (function(){ - var webpack, path, webpackDevServer, ref$, Obj, keys; + var webpack, path, webpackDevServer, fs, deepExtend, archWebpackConfig, ref$, Obj, keys; webpack = require('webpack'); path = require('path'); webpackDevServer = require('webpack-dev-server'); + fs = require('fs'); + deepExtend = require('deep-extend'); + archWebpackConfig = require('./webpack.config'); ref$ = require('prelude-ls'), Obj = ref$.Obj, keys = ref$.keys; - exports.bundle = function(paths, watch, changed){ - var entry, browserEnv, config, bundler, lastBuild, server; - entry = require.resolve(paths.app.abs); - browserEnv = clone$(process.env); - browserEnv.ARCH_ENV = 'browser'; - browserEnv = Obj.map(JSON.stringify)( - browserEnv); - config = { - entry: ['./' + path.basename(entry)], - context: path.dirname(entry), - output: { - libraryTarget: 'var', - library: 'Application', - path: path.join(paths.app.abs, paths['public']), - filename: 'app.js' - }, - resolve: { - root: path.join(paths.app.abs, 'node_modules'), - fallback: path.join(paths.arch.abs, 'node_modules'), - extensions: ['', '.ls', '.js', '.jsx'] - }, - resolveLoader: { - root: path.join(paths.arch.abs, 'node_modules'), - fallback: path.join(paths.app.abs, 'node_modules') - }, - plugins: [new webpack.DefinePlugin({ - 'process.env': browserEnv - })], - module: { - preLoaders: [ - { - test: /\.ls$/, - loader: 'livescript-loader', - exclude: /node_modules/ - }, { - test: /\.(?:js|jsx)$/, - loader: 'babel-loader', - exclude: /node_modules/ - } - ], - loaders: [], - postLoaders: [] - }, - devtool: 'source-map' - }; - if (process.env.NODE_ENV === 'production') { + exports.bundle = function(options, changed){ + var baseConf, userConf, config, bundler, lastBuild, server; + baseConf = archWebpackConfig(options); + userConf = {}; + try { + userConf = require(path.join(options.appPath, 'webpack.config.js')); + } catch (e$) {} + config = deepExtend(baseConf, userConf); + if (options.minify) { config.plugins.push(new webpack.optimize.DedupePlugin()); config.plugins.push(new webpack.optimize.UglifyJsPlugin()); } - if (watch) { + if (options.watch) { config.entry.unshift('webpack/hot/dev-server'); config.entry.unshift('webpack-dev-server/client?http://localhost:3001'); config.output.publicPath = 'http://localhost:3001/'; @@ -66,7 +32,7 @@ config.plugins.push(new webpack.NoErrorsPlugin()); } bundler = webpack(config); - if (watch) { + if (options.watch) { lastBuild = null; bundler.plugin('done', function(stats){ var diff; @@ -83,7 +49,7 @@ }); server = new webpackDevServer(bundler, { filename: 'app.js', - contentBase: path.join(paths.app.abs, paths['public']), + contentBase: path.join(options.appPath, options['public']), hot: true, quiet: true, noInfo: false, @@ -93,14 +59,12 @@ } }); return server.listen(3001, 'localhost'); - } else { + } else if (options.bundle) { return bundler.run(function(err, stats){ return console.log('Bundled app.js'); }); + } else { + return console.warn("Built-in watch and bundle disabled. Compile your own client bundle!"); } }; - function clone$(it){ - function fun(){} fun.prototype = it; - return new fun; - } }).call(this); diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..0559cf8 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,6 @@ +(function(){ + var rc, defaultConfig; + rc = require('rc'); + defaultConfig = require('./default-config'); + module.exports = rc('arch', defaultConfig); +}).call(this); diff --git a/lib/default-config.js b/lib/default-config.js new file mode 100644 index 0000000..b5d6f05 --- /dev/null +++ b/lib/default-config.js @@ -0,0 +1,149 @@ +(function(){ + var path, fs, ref$, filter, map, first, join, keys, parsers, parser, fpathRegex, filterConfigs, merge, initialConf, config, defaultConfig; + path = require('path'); + fs = require('fs'); + ref$ = require('prelude-ls'), filter = ref$.filter, map = ref$.map, first = ref$.first, join = ref$.join, keys = ref$.keys; + /* Map of parsers which take a file path and parse functions*/ + parsers = { + js: function(it){ + return require(it); + }, + ls: function(it){ + return require(it); + } + }; + parser = function(fname){ + return parsers[path.extname(fname).slice(1)](fname); + }; + fpathRegex = new RegExp("arch.config.(?:" + join('|')( + keys( + parsers)) + ")$"); + filterConfigs = function(it){ + return fpathRegex.test(it); + }; + merge = function(x, xs){ + return import$(x, xs); + }; + initialConf = { + appPath: process.env.arch_app_path || path.resolve('.'), + archPath: process.env.arch_arch_path || path.dirname(require.resolve('../package.json')), + bundle: process.env.arch_bundle || true, + debug: process.env.arch_debug || false, + environment: process.env.arch_environment || process.env.NODE_ENV || 'development', + minify: process.env.arch_minify || process.env.NODE_ENV === 'production', + 'public': process.env.arch_public || 'dist', + port: process.env.ARCH_PORT || process.env.arch_port || 3000, + watch: process.env.arch_watch || process.env.NODE_ENV !== 'production' + }; + config = null; + defaultConfig = function(){ + var files, confFiles, config; + if (config) { + return config; + } + files = fs.readdirSync(path.dirname('.')); + confFiles = filter(filterConfigs, map(function(it){ + return path.resolve('.', it); + }, files)); + if (confFiles.length > 1) { + console.error('Multiple configs found. Please have one arch.config.ls or arch.config.js'); + config = initialConf; + } else if (deepEq$(confFiles.length, 1, '===')) { + config = merge(initialConf, parser(first(confFiles))); + } else { + config = initialConf; + } + return config; + }; + defaultConfig.parsers = parsers; + module.exports = defaultConfig; + function import$(obj, src){ + var own = {}.hasOwnProperty; + for (var key in src) if (own.call(src, key)) obj[key] = src[key]; + return obj; + } + function deepEq$(x, y, type){ + var toString = {}.toString, hasOwnProperty = {}.hasOwnProperty, + has = function (obj, key) { return hasOwnProperty.call(obj, key); }; + var first = true; + return eq(x, y, []); + function eq(a, b, stack) { + var className, length, size, result, alength, blength, r, key, ref, sizeB; + if (a == null || b == null) { return a === b; } + if (a.__placeholder__ || b.__placeholder__) { return true; } + if (a === b) { return a !== 0 || 1 / a == 1 / b; } + className = toString.call(a); + if (toString.call(b) != className) { return false; } + switch (className) { + case '[object String]': return a == String(b); + case '[object Number]': + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + return +a == +b; + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') { return false; } + length = stack.length; + while (length--) { if (stack[length] == a) { return true; } } + stack.push(a); + size = 0; + result = true; + if (className == '[object Array]') { + alength = a.length; + blength = b.length; + if (first) { + switch (type) { + case '===': result = alength === blength; break; + case '<==': result = alength <= blength; break; + case '<<=': result = alength < blength; break; + } + size = alength; + first = false; + } else { + result = alength === blength; + size = alength; + } + if (result) { + while (size--) { + if (!(result = size in a == size in b && eq(a[size], b[size], stack))){ break; } + } + } + } else { + if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) { + return false; + } + for (key in a) { + if (has(a, key)) { + size++; + if (!(result = has(b, key) && eq(a[key], b[key], stack))) { break; } + } + } + if (result) { + sizeB = 0; + for (key in b) { + if (has(b, key)) { ++sizeB; } + } + if (first) { + if (type === '<<=') { + result = size < sizeB; + } else if (type === '<==') { + result = size <= sizeB + } else { + result = size === sizeB; + } + } else { + first = false; + result = size === sizeB; + } + } + } + stack.pop(); + return result; + } + } +}).call(this); diff --git a/lib/get-config.js b/lib/get-config.js new file mode 100644 index 0000000..96f5c76 --- /dev/null +++ b/lib/get-config.js @@ -0,0 +1,10 @@ +(function(){ + var rc, defaultConfig, deepExtend; + rc = require('rc'); + defaultConfig = require('./default-config'); + deepExtend = require('deep-extend'); + module.exports = function(opts){ + opts == null && (opts = {}); + return rc('arch', deepExtend(defaultConfig, opts)); + }; +}).call(this); diff --git a/lib/index.js b/lib/index.js index 29ca3dc..78cfaeb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,7 +3,7 @@ global.React = require('react/addons'); path = require('path'); dom = require('./dom'); - serverRendering = require('./server-rendering'); + serverRendering = require('./server/server-rendering'); createComponent = function(spec){ return dom(React.createClass(spec)); }; diff --git a/lib/paths.js b/lib/paths.js new file mode 100644 index 0000000..6f30af0 --- /dev/null +++ b/lib/paths.js @@ -0,0 +1,15 @@ +(function(){ + var path, paths; + path = require('path'); + paths = { + app: { + abs: path.resolve('.'), + rel: path.relative(__dirname, path.resolve('.')) + }, + arch: { + abs: path.dirname(require.resolve("../package.json")), + rel: path.relative(path.resolve('.'), path.dirname(require.resolve("../package.json"))) + } + }; + module.exports = paths; +}).call(this); diff --git a/lib/server/render.js b/lib/server/render.js index 68ebb66..85ef576 100644 --- a/lib/server/render.js +++ b/lib/server/render.js @@ -24,9 +24,9 @@ stringifyState = exports.stringifyState; bundlePath = options.environment === 'development' ? "http://localhost:3001/app.js" - : "/" + options.paths['public'] + "/app.js"; + : "/" + options['public'] + "/app.js"; archBody = __template({ - 'public': options.paths['public'], + 'public': options['public'], bundle: bundlePath, body: body, state: stringifyState(appState) diff --git a/lib/server/server-rendering.js b/lib/server/server-rendering.js new file mode 100644 index 0000000..34239bf --- /dev/null +++ b/lib/server/server-rendering.js @@ -0,0 +1,88 @@ +(function(){ + var domUtils, ref$, difference, filter, first, keys, Obj, ReactServerRenderingTransaction, ReactDefaultBatchingStrategy, instantiateReactComponent, ReactUpdates, redirectLocation, configureReact, renderTree, fakeEvent, changeInputs, submitForm, processForm, routeMetadata, resetRedirect, redirect; + domUtils = require('../virtual-dom-utils'); + ref$ = require('prelude-ls'), difference = ref$.difference, filter = ref$.filter, first = ref$.first, keys = ref$.keys, Obj = ref$.Obj; + ReactServerRenderingTransaction = require('react/lib/ReactServerRenderingTransaction'); + ReactDefaultBatchingStrategy = require('react/lib/ReactDefaultBatchingStrategy'); + instantiateReactComponent = require('react/lib/instantiateReactComponent'); + ReactUpdates = require('react/lib/ReactUpdates'); + redirectLocation = null; + configureReact = function(){ + ReactDefaultBatchingStrategy.isBatchingUpdates = true; + ReactUpdates.injection.injectReconcileTransaction(ReactServerRenderingTransaction); + return ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy); + }; + renderTree = function(element){ + var transaction, instance; + transaction = ReactServerRenderingTransaction.getPooled(true); + instance = instantiateReactComponent(element, null); + try { + transaction.perform(function(){ + return instance.mountComponent("canBeAynthingWhee", transaction, {}); + }); + } finally { + ReactServerRenderingTransaction.release(transaction); + } + return instance._instance; + }; + fakeEvent = function(element, opts){ + var target, ref$; + opts == null && (opts = {}); + target = (ref$ = element.props.type) === 'checkbox' || ref$ === 'radio' + ? { + checked: !!opts.value + } + : { + value: opts.value + }; + return { + stopPropagation: function(){}, + preventDefault: function(){}, + target: target + }; + }; + changeInputs = function(inputs, postData){ + return each(function(it){ + it.props.onChange(fakeEvent(it, { + value: postData[it.props.name] + })); + return ReactUpdates.flushBatchedUpdates(); + })( + inputs); + }; + submitForm = function(form){ + form.props.onSubmit(fakeEvent(form)); + return ReactUpdates.flushBatchedUpdates(); + }; + processForm = function(rootElement, initialState, postData, path){ + var instance, inputNames, ref$, form, inputs, that; + configureReact(); + resetRedirect(); + instance = renderTree(rootElement); + inputNames = keys(postData); + ref$ = domUtils.formElements(instance, path, inputNames), form = ref$[0], inputs = ref$[1]; + changeInputs(inputs, postData); + submitForm(form); + if (that = redirectLocation) { + return that; + } + return null; + }; + routeMetadata = function(rootElement, initialState){ + var instance; + configureReact(); + instance = renderTree(rootElement); + return domUtils.routeMetadata(instance); + }; + resetRedirect = function(){ + return redirectLocation = null; + }; + redirect = function(path){ + return redirectLocation = path; + }; + module.exports = { + routeMetadata: routeMetadata, + processForm: processForm, + redirect: redirect + }; +}).call(this); diff --git a/lib/server/server.js b/lib/server/server.js index e1dc763..116fc5c 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -1,5 +1,5 @@ (function(){ - var express, path, bluebird, bodyParser, bundler, livescript, register, renderBody, ref$, each, values, filter, find, flatten, map, first, defaults, archGet, archPost; + var express, path, bluebird, bodyParser, bundler, livescript, register, defaultConfig, deepExtend, renderBody, ref$, each, values, filter, find, flatten, map, first, archGet, archPost; express = require('express'); path = require('path'); bluebird = require('bluebird'); @@ -7,27 +7,15 @@ bundler = require('../bundler'); livescript = require('livescript'); register = require('babel/register'); + defaultConfig = require('../default-config'); + deepExtend = require('deep-extend'); renderBody = require('./render').renderBody; ref$ = require('prelude-ls'), each = ref$.each, values = ref$.values, filter = ref$.filter, find = ref$.find, flatten = ref$.flatten, map = ref$.map, first = ref$.first; - defaults = { - environment: process.env.NODE_ENV || 'development', - port: process.env.ARCH_PORT || 3000, - paths: { - app: { - abs: path.resolve('.'), - rel: path.relative(__dirname, path.resolve('.')) - }, - arch: { - abs: path.dirname(require.resolve("../../package.json")), - rel: path.relative(path.resolve('.'), path.dirname(require.resolve("../../package.json"))) - }, - 'public': 'dist' - } - }; - module.exports = function(options){ - var app, get, post; - options = import$(clone$(defaults), options); - app = options.app || require(options.paths.app.rel); + module.exports = function(serverOptions){ + var defaultOptions, options, app, get, post; + defaultOptions = defaultConfig(); + options = deepExtend(defaultOptions, serverOptions); + app = require(options.appPath); get = function(req, res){ console.log("GET", req.originalUrl); return archGet(app, req.originalUrl, options).spread(function(status, headers, body){ @@ -47,10 +35,10 @@ return { start: function(cb){ var server, listener; - server = express().use("/" + options.paths['public'], express['static'](path.join(options.paths.app.abs, options.paths['public']))).use(bodyParser.urlencoded({ + server = express().use("/" + options['public'], express['static'](path.join(options.appPath, options['public']))).use(bodyParser.urlencoded({ extended: false })).get('*', get).post('*', post); - bundler.bundle(options.paths, options.environment === 'development', function(ids){ + bundler.bundle(options, function(ids){ var done, id, parents, e; done = []; while (id = first(ids)) { @@ -70,7 +58,7 @@ })( done); try { - return app = require(options.paths.app.rel); + return app = require(options.appPath); } catch (e$) { e = e$; return console.error('Error in changed files when restarting server'); @@ -132,15 +120,6 @@ return [200, {}, html]; }); }; - function import$(obj, src){ - var own = {}.hasOwnProperty; - for (var key in src) if (own.call(src, key)) obj[key] = src[key]; - return obj; - } - function clone$(it){ - function fun(){} fun.prototype = it; - return new fun; - } function in$(x, xs){ var i = -1, l = xs.length >>> 0; while (++i < l) if (x === xs[i]) return true; diff --git a/lib/webpack.config.js b/lib/webpack.config.js new file mode 100644 index 0000000..07ce20b --- /dev/null +++ b/lib/webpack.config.js @@ -0,0 +1,52 @@ +(function(){ + var deepExtend, path, defaultConfig, webpack, config, entryPoint, appModules, archModules; + deepExtend = require('deep-extend'); + path = require('path'); + defaultConfig = require('./default-config'); + webpack = require('webpack'); + config = defaultConfig(); + entryPoint = require.resolve(config.appPath); + appModules = path.join(config.appPath, 'node_modules'); + archModules = path.join(config.archPath, 'node_modules'); + module.exports = function(severOptions){ + return { + context: path.dirname(entryPoint), + entry: ['./' + path.basename(entryPoint)], + output: { + libraryTarget: 'var', + library: 'Application', + path: path.join(config.appPath, config['public']), + filename: 'app.js' + }, + module: { + loaders: [], + postLoaders: [], + preLoaders: [ + { + test: /\.ls$/, + loader: 'livescript-loader', + exclude: /node_modules/ + }, { + test: /\.(?:js|jsx)$/, + loader: 'babel-loader', + exclude: /node_modules/ + } + ] + }, + plugins: [new webpack.DefinePlugin({ + 'process.env': JSON.stringify(deepExtend(process.env, { + ARCH_ENV: 'browser' + })) + })], + resolve: { + root: appModules, + fallback: archModules, + extensions: ['', '.ls', '.js', '.jsx'] + }, + resolveLoader: { + root: archModules, + fallback: appModules + } + }; + }; +}).call(this); diff --git a/package.json b/package.json index 9c8426e..797e7dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arch", - "version": "0.0.4", + "version": "0.0.5", "description": "Web application framework for React", "homepage": "https://github.com/redbadger/arch", "repository": { @@ -40,6 +40,7 @@ "babel-loader": "^5.1.4", "bluebird": "^2.9.27", "body-parser": "^1.12.4", + "deep-extend": "^0.4.0", "express": "^4.12.4", "immutable": "^3.7.3", "jade": "^1.10.0", @@ -60,6 +61,7 @@ "gulp-livescript": "^2.4.0", "gulp-load-plugins": "^1.0.0-rc.1", "gulp-strip-code": "^0.1.2", - "jasmine": "^2.3.1" + "jasmine": "^2.3.1", + "sinon": "^1.16.1" } } diff --git a/spec/config_spec.ls b/spec/config_spec.ls new file mode 100644 index 0000000..9832d36 --- /dev/null +++ b/spec/config_spec.ls @@ -0,0 +1,36 @@ +require! <[ ../src/default-config bluebird path fs sinon ]> + +describe "config" (_) -> + describe "loading" (_) -> + sandbox = sinon.sandbox.create! + + after-each -> sandbox.restore! + + it "shows an error when multiple configs are found" -> + sandbox + .stub fs, 'readdirSync' + .returns [ + "arch.config.js", + "arch.config.ls" + ] + + sandbox.stub console, 'error' + + default-config! + + expect console.error.called-with "Multiple configs found. Please have one arch.config.ls or arch.config.js" .to-be true + + it "merges the config with the initial config when one is found" -> + sandbox + .stub fs, 'readdirSync' + .returns [ + "arch.config.js" + ] + + sandbox + .stub default-config.parsers, "js" + .returns port: 12345 + + conf = default-config! + + expect conf.port .to-be 12345 diff --git a/src/application.ls b/src/application.ls index ac4b1c8..0158a4a 100644 --- a/src/application.ls +++ b/src/application.ls @@ -1,4 +1,4 @@ -require! <[ bluebird ./cursor ./dom ./routes ./server-rendering lodash/string/unescape ]> +require! <[ bluebird ./cursor ./dom ./routes ./server/server-rendering lodash/string/unescape ]> require! './virtual-dom-utils': 'dom-utils' {span} = dom @@ -121,4 +121,3 @@ module.exports = body = unless location then React.render-to-string root-element else null [meta, app-state.deref!, body, location] - diff --git a/src/bundler.ls b/src/bundler.ls index a088efb..32a7ee7 100644 --- a/src/bundler.ls +++ b/src/bundler.ls @@ -1,58 +1,24 @@ -require! <[ webpack path webpack-dev-server ]> +require! <[ webpack path webpack-dev-server fs deep-extend ]> +arch-webpack-config = require './webpack.config' {Obj, keys} = require 'prelude-ls' -exports.bundle = (paths, watch, changed) -> - entry = require.resolve paths.app.abs +exports.bundle = (options, changed) -> + base-conf = arch-webpack-config options + user-conf = {} - browser-env = ^^process.env - browser-env.ARCH_ENV = 'browser' - browser-env = browser-env |> Obj.map JSON.stringify + try + user-conf = require path.join(options.app-path, 'webpack.config.js') - # Basic configuration - config = - entry: [ './' + path.basename entry ] - - context: path.dirname entry - - output: - library-target: 'var' - library: 'Application' - path: path.join paths.app.abs, paths.public - filename: 'app.js' - - resolve: - root: path.join paths.app.abs, 'node_modules' - fallback: path.join paths.arch.abs, 'node_modules' - extensions: [ '', '.ls', '.js', '.jsx' ] - - resolve-loader: - root: path.join paths.arch.abs, 'node_modules' - fallback: path.join paths.app.abs, 'node_modules' - - plugins: [ new webpack.DefinePlugin 'process.env': browser-env ] - - module: - pre-loaders: [ - * test: /\.ls$/ - loader: 'livescript-loader' - exclude: /node_modules/ - * test: /\.(?:js|jsx)$/ - loader: 'babel-loader' - exclude: /node_modules/ - ] - loaders: [] - post-loaders: [] - - devtool: \source-map + config = deep-extend base-conf, user-conf # Optimise for production. - if process.env.NODE_ENV is 'production' + if options.minify config.plugins.push new webpack.optimize.DedupePlugin! config.plugins.push new webpack.optimize.UglifyJsPlugin! # Enable HMR if watching. - if watch + if options.watch config.entry.unshift 'webpack/hot/dev-server' config.entry.unshift 'webpack-dev-server/client?http://localhost:3001' config.output.public-path = 'http://localhost:3001/' @@ -67,7 +33,7 @@ exports.bundle = (paths, watch, changed) -> bundler = webpack config # Just bundle or watch + serve via webpack-dev-server - if watch + if options.watch # Add a callback to server, passing changed files, to reload app code server-side. last-build = null @@ -82,7 +48,7 @@ exports.bundle = (paths, watch, changed) -> # Start the webpack dev server server = new webpack-dev-server bundler, do filename: 'app.js' - content-base: path.join paths.app.abs, paths.public + content-base: path.join options.app-path, options.public hot: true # Enable hot loading quiet: true no-info: false @@ -92,7 +58,9 @@ exports.bundle = (paths, watch, changed) -> server.listen 3001, 'localhost' - else + else if options.bundle # Run once if watch is false bundler.run (err, stats) -> console.log 'Bundled app.js' + else + console.warn "Built-in watch and bundle disabled. Compile your own client bundle!" \ No newline at end of file diff --git a/src/default-config.ls b/src/default-config.ls new file mode 100644 index 0000000..e6f3fc0 --- /dev/null +++ b/src/default-config.ls @@ -0,0 +1,53 @@ +require! <[ path fs ]> + +{ filter, map, first, join, keys } = require 'prelude-ls' + +# RC automatically overwrites these with env variables. +# For example to edit environment set arch_environment + +/* Map of parsers which take a file path and parse functions*/ + +parsers = + js: -> require it, + ls: -> require it + +parser = (fname) -> parsers[(path.extname fname).slice(1)](fname) + +fpath-regex = new RegExp "arch\.config\.(?:#{parsers |> keys |> join '|'})$" + +filter-configs = -> fpath-regex.test it + +merge = (x, xs) -> x import xs + +initial-conf = + app-path: process.env.arch_app_path or path.resolve '.' + arch-path: process.env.arch_arch_path or path.dirname require.resolve '../package.json' + bundle: process.env.arch_bundle or true + debug: process.env.arch_debug or false + environment: process.env.arch_environment or process.env.NODE_ENV or 'development' + minify: process.env.arch_minify or process.env.NODE_ENV is 'production' + public: process.env.arch_public or 'dist' + port: process.env.ARCH_PORT or process.env.arch_port or 3000 # TODO: Deprecate ARCH_PORT (caps) in future (use another convention) + watch: process.env.arch_watch or process.env.NODE_ENV isnt 'production' + +config = null + +default-config = -> + return config if config + + files = fs.readdir-sync (path.dirname '.') + conf-files = (filter filter-configs, map((-> path.resolve '.', it), files)) + + if conf-files.length > 1 + console.error 'Multiple configs found. Please have one arch.config.ls or arch.config.js' + config = initial-conf + else if conf-files.length === 1 + config = merge initial-conf, parser(first conf-files) + else + config = initial-conf + + return config + +default-config.parsers = parsers + +module.exports = default-config diff --git a/src/index.ls b/src/index.ls index 5db567d..8699583 100644 --- a/src/index.ls +++ b/src/index.ls @@ -2,7 +2,7 @@ global.React = require 'react/addons' # FIXME require server-rendering only on the server -require! <[ path ./dom ./server-rendering ]> +require! <[ path ./dom ./server/server-rendering ]> create-component = (spec) -> dom React.create-class spec diff --git a/src/server/render.ls b/src/server/render.ls index 1d25fa4..0726428 100644 --- a/src/server/render.ls +++ b/src/server/render.ls @@ -13,8 +13,8 @@ exports.stringify-state = -> JSON.stringify it, escape-filter exports.render-body = (meta, body, app-state, options) -> stringify-state = exports.stringify-state - bundle-path = if options.environment is 'development' then "http://localhost:3001/app.js" else "/#{options.paths.public}/app.js" - arch-body = __template public: options.paths.public, bundle: bundle-path, body: body, state: stringify-state app-state + bundle-path = if options.environment is 'development' then "http://localhost:3001/app.js" else "/#{options.public}/app.js" + arch-body = __template public: options.public, bundle: bundle-path, body: body, state: stringify-state app-state {layout, title} = meta layout body: arch-body, title: title diff --git a/src/server-rendering.ls b/src/server/server-rendering.ls similarity index 98% rename from src/server-rendering.ls rename to src/server/server-rendering.ls index b9f48c6..c2d2d4b 100644 --- a/src/server-rendering.ls +++ b/src/server/server-rendering.ls @@ -1,4 +1,4 @@ -dom-utils = require './virtual-dom-utils' +dom-utils = require '../virtual-dom-utils' {difference, filter, first, keys, Obj} = require 'prelude-ls' ReactServerRenderingTransaction = require 'react/lib/ReactServerRenderingTransaction' diff --git a/src/server/server.ls b/src/server/server.ls index 4dbaf88..a9f37bb 100644 --- a/src/server/server.ls +++ b/src/server/server.ls @@ -2,27 +2,18 @@ require! <[ express path bluebird body-parser ../bundler livescript babel/register + ../default-config deep-extend ]> { render-body } = require './render' -{each, values, filter, find, flatten, map, first} = require 'prelude-ls' +{ each, values, filter, find, flatten, map, first } = require 'prelude-ls' -defaults = - environment: process.env.NODE_ENV or 'development' - port: process.env.ARCH_PORT or 3000 - paths: - app: - abs: path.resolve '.' - rel: path.relative __dirname, path.resolve '.' - arch: - abs: path.dirname require.resolve "../../package.json" - rel: path.relative (path.resolve '.'), (path.dirname require.resolve "../../package.json") - public: 'dist' +module.exports = (server-options) -> + default-options = default-config! # These defaults already have env overwrites applied to them. See default-config. + options = deep-extend default-options, server-options -module.exports = (options) -> - options = ^^defaults import options - app = options.app or require options.paths.app.rel + app = require options.app-path get = (req, res) -> console.log "GET", req.original-url @@ -46,7 +37,7 @@ module.exports = (options) -> start: (cb) -> server = express! - .use "/#{options.paths.public}", express.static path.join(options.paths.app.abs, options.paths.public) + .use "/#{options.public}", express.static path.join(options.app-path, options.public) .use body-parser.urlencoded extended: false .get '*', get .post '*', post @@ -55,7 +46,7 @@ module.exports = (options) -> # .bundle takes a boolean of whether to watch and can take a callback which # allows you to hook into any watch changes. - bundler.bundle options.paths, options.environment is 'development', (ids) -> + bundler.bundle options, (ids) -> done = [] while id = first ids parents = require.cache |> values |> filter (-> !(it.id in done) and it.children |> find (.id is id)) |> flatten |> map (.id) @@ -66,7 +57,7 @@ module.exports = (options) -> done |> each -> delete require.cache[it] try - app := require options.paths.app.rel + app := require options.app-path catch console.error 'Error in changed files when restarting server' diff --git a/src/webpack.config.ls b/src/webpack.config.ls new file mode 100644 index 0000000..c47066f --- /dev/null +++ b/src/webpack.config.ls @@ -0,0 +1,39 @@ +require! { + 'deep-extend' + path + './default-config' + webpack +} + +config = default-config! + +entry-point = require.resolve config.app-path +app-modules = path.join config.app-path, 'node_modules' +arch-modules = path.join config.arch-path, 'node_modules' + +module.exports = (sever-options) -> + context: path.dirname entry-point + entry: [ './' + path.basename entry-point ] + output: + library-target: 'var' + library: 'Application' + path: path.join config.app-path, config.public + filename: 'app.js' + module: + loaders: [] + post-loaders: [] + pre-loaders: + * test: /\.ls$/ + loader: 'livescript-loader' + exclude: /node_modules/ + * test: /\.(?:js|jsx)$/ + loader: 'babel-loader' + exclude: /node_modules/ + plugins: [ new webpack.DefinePlugin('process.env': JSON.stringify(deep-extend process.env, ARCH_ENV: 'browser')) ] + resolve: + root: app-modules + fallback: arch-modules + extensions: [ '', '.ls', '.js', '.jsx' ] + resolveLoader: + root: arch-modules + fallback: app-modules