├── 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 | [](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 |
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 |
--------------------------------------------------------------------------------