├── app ├── vendor.js ├── helpers │ └── map-errors.js ├── index.html ├── components │ └── labeled-slider │ │ └── index.js └── index.js ├── .eslintignore ├── .travis.yml ├── .gitignore ├── .editorconfig ├── changelog.md ├── webpack-dev.config.babel.js ├── webpack-prod.config.babel.js ├── package.json ├── Readme.md ├── webpack-common.config.js └── .eslintrc /app/vendor.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | "@cycle/core", 3 | "@cycle/dom", 4 | ]; 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | public/** 3 | ignore/** 4 | vendors/** 5 | build/** 6 | dist/** 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | sudo: false 5 | script: 6 | - npm test 7 | - npm run compile 8 | notifications: 9 | email: 10 | on_success: never 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific stuff 2 | build/ 3 | dist/ 4 | tmp/ 5 | webpack-stats.json 6 | 7 | # npm stuff 8 | node_modules/ 9 | npm-debug.log 10 | 11 | # Editor Stuff 12 | *.sublime-project 13 | *.sublime-workspace 14 | *.bak 15 | *~ 16 | 17 | # System stuff 18 | .DS_Store 19 | .Spotlight-V100 20 | .Trashes 21 | Thumbs.db 22 | ehthumbs.db 23 | Desktop.ini 24 | $RECYCLE.BIN/ 25 | .directory 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.js] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.jsx] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [*.less] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | [*.md] 28 | trim_trailing_whitespace = false 29 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 2 | ## master (2015-07-29) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * **example** Parse Slider Value to Integer ((33e9c099), closes (#2)) 8 | 9 | #### Features 10 | 11 | * **build** 12 | * Minify HTML Assets ((1a7dfaff)) 13 | * Clean Build Folder Before Compile ((1ffaa16e)) 14 | * Use HtmlWebpackPlugin ((169e43e0)) 15 | * Webpack Setup With Cycle Example ((66f3b1a8)) 16 | * **example** 17 | * Handle Errors More Elegantly ((81e79ca7)) 18 | * Handle and Show Errors ((b7aee2cb)) 19 | * Add BMI Cycle Example Code ((d4274e1c)) 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /webpack-dev.config.babel.js: -------------------------------------------------------------------------------- 1 | import merge from 'webpack-merge'; 2 | 3 | import commonConfig from './webpack-common.config.js'; 4 | 5 | export default merge(commonConfig, { 6 | debug: true, 7 | devtool: "cheap-module-inline-source-map", 8 | profile: false, 9 | 10 | watch: true, 11 | watchOptions: { 12 | aggregateTimeout: 300, 13 | poll: 1000, 14 | }, 15 | 16 | devServer: { 17 | contentBase: "./public", 18 | port: 3000, 19 | 20 | hot: false, 21 | inline: true, 22 | historyApiFallback: true, 23 | 24 | colors: true, 25 | stats: 'normal', 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /app/helpers/map-errors.js: -------------------------------------------------------------------------------- 1 | import {Rx} from '@cycle/core'; 2 | 3 | /** 4 | * Helpers to catch errors 5 | */ 6 | 7 | export function returnAsObservable(error) { 8 | return Rx.Observable.just({error}); 9 | } 10 | 11 | /** 12 | * Helpers to easily map over errors 13 | */ 14 | 15 | function isError(data) { 16 | return !!(data && data.error); 17 | } 18 | 19 | function identity(x) { 20 | return x; 21 | } 22 | 23 | export function ifOk(mapper) { 24 | return (data) => isError(data) ? identity(data) : mapper(data); 25 | } 26 | 27 | export function ifError(mapper) { 28 | return (data) => isError(data) ? mapper(data) : identity(data); 29 | } 30 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {%=o.htmlWebpackPlugin.options.title || 'Webpack App'%} 6 | {% if (o.htmlWebpackPlugin.files.favicon) { %} 7 | {% } %} 8 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 9 | {% } %} 10 | 11 | 12 |
13 | 14 | {% if (o.webpackConfig.devServer && o.webpackConfig.watch) { %} 15 | {% } %} 16 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 17 | {% } %} 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/components/labeled-slider/index.js: -------------------------------------------------------------------------------- 1 | import {Rx} from '@cycle/core'; 2 | import {hJSX} from '@cycle/dom'; 3 | 4 | export default function labeledSlider(responses) { 5 | const initialValue$ = responses.props.get('initial').first(); 6 | const newValue$ = responses.DOM.get('.slider', 'input') 7 | .map((ev) => parseInt(ev.target.value), 10); 8 | 9 | const value$ = initialValue$.concat(newValue$); 10 | const props$ = responses.props.get('*'); 11 | 12 | const vtree$ = Rx.Observable 13 | .combineLatest(props$, value$, (props, value) => 14 |
15 |
16 | {props.children.concat(value + props.unit)} 17 |
18 | 22 |
23 | ); 24 | 25 | return { 26 | DOM: vtree$, 27 | events: { 28 | newValue: newValue$, 29 | }, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /webpack-prod.config.babel.js: -------------------------------------------------------------------------------- 1 | import merge from 'webpack-merge'; 2 | 3 | import webpack from 'webpack'; 4 | import CompressionPlugin from 'compression-webpack-plugin'; 5 | import CleanPlugin from 'clean-webpack-plugin'; 6 | 7 | import commonConfig from './webpack-common.config.js'; 8 | 9 | export default merge(commonConfig, { 10 | debug: false, 11 | devtool: "source-map", 12 | profile: true, 13 | watch: false, 14 | 15 | plugins: [ 16 | new CleanPlugin(["build"]), 17 | new webpack.NoErrorsPlugin(), 18 | new webpack.DefinePlugin({ 19 | "process.env": { 20 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 21 | BROWSER: JSON.stringify(true), 22 | }, 23 | }), 24 | new webpack.optimize.DedupePlugin(), 25 | new webpack.optimize.OccurenceOrderPlugin(true), 26 | new webpack.optimize.UglifyJsPlugin({ 27 | compress: { 28 | warnings: false, 29 | }, 30 | comments: /\@license|\@preserv/gi, 31 | }), 32 | new CompressionPlugin({ 33 | asset: "{file}.gz", 34 | algorithm: "gzip", 35 | regExp: new RegExp("\.(js|html|css|svg)$"), 36 | threshold: 10240, 37 | minRatio: 0.8, 38 | }), 39 | function writeWebpackStats() { 40 | this.plugin("done", function writeStats(stats) { 41 | require("fs").writeFileSync( 42 | require('path').join(__dirname, "build", "webpack-stats.json"), 43 | JSON.stringify(stats.toJson()) 44 | ); 45 | }); 46 | }, 47 | ], 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycle-webpack-starter", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "env NODE_ENV=production webpack --config webpack-prod.config.babel.js --progress", 8 | "lint": "eslint .", 9 | "start": "webpack-dev-server --config webpack-dev.config.babel.js --progress", 10 | "test": "npm run lint" 11 | }, 12 | "author": "Pascal Hertleif (http://pascalhertleif.de/)", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/killercup/cycle-webpack-starter.git" 16 | }, 17 | "license": "MIT", 18 | "devDependencies": { 19 | "babel": "^5.8.23", 20 | "babel-core": "^5.8.25", 21 | "babel-eslint": "^4.1.3", 22 | "babel-loader": "^5.3.2", 23 | "babel-plugin-closure-elimination": "0.0.2", 24 | "babel-plugin-typecheck": "^1.3.0", 25 | "babel-runtime": "^5.8.25", 26 | "clean-webpack-plugin": "^0.1.3", 27 | "compression-webpack-plugin": "^0.2.0", 28 | "eslint": "^1.6.0", 29 | "eslint-loader": "^1.0.0", 30 | "eslint-plugin-babel": "^2.1.1", 31 | "eslint-plugin-no-class": "^0.1.0", 32 | "eslint-plugin-react": "^3.5.1", 33 | "html-webpack-plugin": "^1.6.1", 34 | "node-libs-browser": "^0.5.3", 35 | "webpack": "^1.12.2", 36 | "webpack-dev-server": "^1.12.0", 37 | "webpack-merge": "^0.2.0" 38 | }, 39 | "dependencies": { 40 | "@cycle/core": "^3.1.0", 41 | "@cycle/dom": "^5.3.0", 42 | "rx": "^4.0.0", 43 | "virtual-dom": "^2.1.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Starter Pack using Cycle.js, JSX/ES6 (Babel), and Webpack 2 | 3 | Boilerplate for building ES6 web apps using [Cycle](http://cycle.js.org/). 4 | 5 | [![Build Status](https://travis-ci.org/killercup/cycle-webpack-starter.svg?branch=master)](https://travis-ci.org/killercup/cycle-webpack-starter) 6 | 7 | ## Getting Started 8 | 9 | 1. `npm install` 10 | 2. `npm start` 11 | 3. Code your app 12 | 4. ??? 13 | 5. PROFIT! 14 | 15 | ### NPM Tasks 16 | 17 | - `npm start` runs Webpack's development server (watches for file changes and reloads your browser) 18 | - `npm run compile` compiles your app for production (minify and optimizes the hell out of your code) 19 | - `npm test` runs lints and tests (currently, there are no tests) 20 | - `npm run lint` lints your code using [ESLint](eslint.org) (OBEY ESLINT!) 21 | 22 | ### Vendor Module Packaging 23 | 24 | Webpack's [CommonsChunkPlugin](http://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin) is used to split your code and vendor dependencies into separate bundles. Ideally, that allows you to cache dependency code longer than application code and allows you to easily blackbox dependency code when debugging. 25 | 26 | To add dependencies to your _vendor_ bundle, just edit `src/vendor.js`. 27 | 28 | ## Technology 29 | 30 | - Cycle.js 1.0 with RxJS, Cycle DOM 3.0 with virtual-dom 31 | - Webpack to package JS code and assets 32 | - Babel.js to compile modern JS to ES5 33 | - Supports writing JSX with virtual-dom using Cycle DOM's helper (make sure to `import {hJSX} from '@cycle/dom';`) 34 | - In development mode, JSX/Flow/TypeScript-like type hints are converted to asserts using [typecheck](https://github.com/codemix/babel-plugin-typecheck) 35 | 36 | ## TODO 37 | 38 | - [x] Create HTML files using the [HTML Webpack plugin](https://github.com/ampedandwired/html-webpack-plugin) 39 | - [x] Use hashes in filenames 40 | - [ ] Add Webpack plugins for CSS and asset files 41 | - [ ] Try to integrate Polymer elements 42 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import Cycle, {Rx} from '@cycle/core'; 2 | import {hJSX, makeDOMDriver} from '@cycle/dom'; 3 | import {ifOk, ifError, returnAsObservable} from './helpers/map-errors'; 4 | 5 | function calculateBMI(weight: number, height: number) { 6 | if (height < 160) { 7 | throw new Error("This is just a test, I don't mean any disrepect!"); 8 | } 9 | const heightMeters = height * 0.01; 10 | return Math.round(weight / (heightMeters * heightMeters)); 11 | } 12 | 13 | function intent(DOM) { 14 | return { 15 | changeWeight: DOM.get('#weight', 'newValue').map((ev) => ev.detail), 16 | changeHeight: DOM.get('#height', 'newValue').map((ev) => ev.detail), 17 | }; 18 | } 19 | 20 | function model(actions) { 21 | return Rx.Observable 22 | .combineLatest( 23 | actions.changeWeight.startWith(70), 24 | actions.changeHeight.startWith(170), 25 | (weight, height) => ({ 26 | weight, 27 | height, 28 | bmi: calculateBMI(weight, height), 29 | }) 30 | ) 31 | .catch(returnAsObservable); 32 | } 33 | 34 | function view(state) { 35 | return state 36 | .map(ifOk(({weight, height, bmi}) => 37 |
38 | 40 |

Weight

41 |
42 | 44 |

Height

45 |
46 |

{`BMI is ${bmi}`}

47 |
48 | )) 49 | .map(ifError(({error}) => 50 |
51 |

{`${error}`}

52 | Shit's on fire 53 |
54 | )) 55 | .doOnError(console.error.bind(console)); 56 | } 57 | 58 | function main({DOM}) { 59 | return { 60 | DOM: view(model(intent(DOM))), 61 | }; 62 | } 63 | 64 | Cycle.run(main, { 65 | DOM: makeDOMDriver('#app', { 66 | 'labeled-slider': require('./components/labeled-slider'), 67 | }), 68 | }); 69 | -------------------------------------------------------------------------------- /webpack-common.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | 4 | let name = 'Cycle!'; 5 | 6 | let vendorModules = /(node_modules|bower_components)/; 7 | 8 | export default { 9 | target: "web", 10 | entry: { 11 | app: "./app/index.js", 12 | vendor: require("./app/vendor.js"), 13 | }, 14 | 15 | output: { 16 | path: "./build", 17 | filename: "[name]-[chunkhash].js", 18 | pathinfo: true, 19 | publicPath: "", 20 | }, 21 | 22 | module: { 23 | preLoaders: [ 24 | {test: /\.jsx?$/, loader: "eslint-loader", exclude: vendorModules}, 25 | ], 26 | loaders: [ 27 | { 28 | test: /\.jsx?$/, 29 | exclude: vendorModules, 30 | loader: "babel", 31 | query: { 32 | optional: [ 33 | "runtime", 34 | "validation.undeclaredVariableCheck", 35 | "optimisation.react.constantElements", 36 | ], 37 | env: { 38 | development: { 39 | plugins: [ 40 | "typecheck", 41 | "closure-elimination", 42 | ], 43 | }, 44 | }, 45 | jsxPragma: "hJSX", 46 | stage: 0, 47 | }, 48 | }, 49 | ], 50 | }, 51 | 52 | plugins: [ 53 | new webpack.optimize.CommonsChunkPlugin( 54 | 'vendor', 'vendor-[chunkhash].js', Infinity 55 | ), 56 | new HtmlWebpackPlugin({ 57 | title: name, 58 | minify: process.env.NODE_ENV === 'production' ? { 59 | removeComments: true, 60 | removeCommentsFromCDATA: true, 61 | collapseWhitespace: true, 62 | conservativeCollapse: false, 63 | collapseBooleanAttributes: true, 64 | removeAttributeQuotes: true, 65 | removeRedundantAttributes: true, 66 | preventAttributesEscaping: true, 67 | useShortDoctype: true, 68 | removeEmptyAttributes: true, 69 | removeScriptTypeAttributes: true, 70 | removeStyleLinkTypeAttributes: true, 71 | } : false, 72 | template: './app/index.html', 73 | }), 74 | ], 75 | }; 76 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "babel", 9 | "react", 10 | "no-class", 11 | ], 12 | "ecmaFeatures": { 13 | "arrowFunctions": true, 14 | "binaryLiterals": true, 15 | "regexUFlag": true, 16 | "regexYFlag": true, 17 | "unicodeCodePointEscapes": true, 18 | "restParams": true, 19 | "destructuring": true, 20 | "modules": true, 21 | "defaultParams": true, 22 | "classes": true, 23 | "spread": true, 24 | "superInFunctions": true, 25 | "templateStrings": true, 26 | "objectLiteralShorthandMethods": true, 27 | "objectLiteralDuplicateProperties": true, 28 | "objectLiteralComputedProperties": true, 29 | "objectLiteralShorthandProperties": true, 30 | "generators": true, 31 | "forOf": true, 32 | "blockBindings": true, 33 | "jsx": true 34 | }, 35 | "rules": { 36 | "no-cond-assign": [2, "always"], 37 | "no-ex-assign": 2, 38 | "curly": 2, 39 | "max-len": [1, 80, 4], 40 | "max-depth": [2, 5], 41 | "complexity": [1, 8], 42 | "indent": [2, 2], 43 | "no-trailing-spaces": [2, {"skipBlankLines": false}], 44 | "one-var": [2, "never"], 45 | "func-names": 2, 46 | "arrow-parens": 2, 47 | "arrow-spacing": 2, 48 | "prefer-spread": 1, 49 | "callback-return": [2, ["callback", "cb", "next", "done"]], 50 | "no-useless-call": 2, 51 | "no-const-assign": 2, 52 | "no-class-assign": 2, 53 | "require-yield": 1, 54 | "key-spacing": [2, { 55 | "beforeColon": false, 56 | "afterColon": true 57 | }], 58 | "max-nested-callbacks": [2, 3], 59 | "new-cap": [2, {"newIsCap": true}], 60 | "new-parens": 2, 61 | "no-mixed-spaces-and-tabs": 2, 62 | "no-multiple-empty-lines": [1, {"max": 1}], 63 | "no-nested-ternary": 2, 64 | "no-new-object": 2, 65 | "no-spaced-func": 2, 66 | "operator-assignment": [2, "always"], 67 | "operator-linebreak": [2, "after"], 68 | "padded-blocks": [2, "never"], 69 | "quote-props": [2, "as-needed"], 70 | "space-after-keywords": [2, "always"], 71 | "space-before-blocks": [2, "always"], 72 | "space-before-function-paren": [2, "never"], 73 | "object-curly-spacing": [2, "never"], 74 | "array-bracket-spacing": [2, "never"], 75 | "space-in-parens": [2, "never"], 76 | "space-infix-ops": [2, {"int32Hint": true}], 77 | "space-return-throw-case": 2, 78 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 79 | "no-delete-var": 2, 80 | "no-underscore-dangle": 0, 81 | "no-shadow": 2, 82 | "no-shadow-restricted-names": 2, 83 | "no-undef-init": 2, 84 | "no-undef": 2, 85 | "no-undefined": 2, 86 | "no-unused-vars": [1, {"vars": "all", "args": "after-used"}], 87 | "no-use-before-define": 2, 88 | "yoda": [2, "never"], 89 | "consistent-return": 2, 90 | "spaced-line-comment": 0, 91 | "eqeqeq": 2, 92 | "guard-for-in": 2, 93 | "no-alert": 2, 94 | "no-caller": 2, 95 | "no-labels": 2, 96 | "no-eval": 2, 97 | "no-fallthrough": 2, 98 | "default-case": 2, 99 | "no-iterator": 2, 100 | "no-loop-func": 2, 101 | "no-multi-spaces": 2, 102 | "no-multi-str": 2, 103 | "no-new": 2, 104 | "no-param-reassign": 2, 105 | "no-proto": 2, 106 | "no-redeclare": 2, 107 | "no-return-assign": 2, 108 | "no-self-compare": 2, 109 | "no-sequences": 2, 110 | "no-throw-literal": 2, 111 | "no-unused-expressions": 2, 112 | "no-with": 2, 113 | "vars-on-top": 0, 114 | "wrap-iife": [2, "outside"], 115 | "valid-typeof": 2, 116 | "max-statements": [1, 30], 117 | "max-params": [1, 3], 118 | "no-var": 2, 119 | "semi": 2, 120 | "dot-location": [2, "property"], 121 | 122 | "quotes": 0, # symbols in single quotes 123 | "global-strict": 0, # "use strict" should be inserted automatically 124 | "comma-dangle": [1, "always-multiline"], # add last commas to improve diffs 125 | 126 | "react/jsx-boolean-value": 1, 127 | "react/jsx-no-undef": 2, 128 | "react/jsx-quotes": 1, 129 | "react/jsx-uses-react": [2, {"pragma": "hJSX"}], 130 | "react/jsx-uses-vars": 2, 131 | "react/self-closing-comp": 1, 132 | "react/wrap-multilines": 1, 133 | "react/jsx-no-duplicate-props": 2, 134 | "react/jsx-indent-props": [2, 2], 135 | "react/jsx-closing-bracket-location": [1, {location: 'after-props'}], 136 | 137 | "babel/generator-star-spacing": 1, 138 | "babel/new-cap": 1, 139 | "babel/object-curly-spacing": 1, 140 | "babel/object-shorthand": 1, 141 | "babel/arrow-parens": 1, 142 | 143 | "no-class/no-class": 2, 144 | } 145 | } 146 | --------------------------------------------------------------------------------