diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e7bacab --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "react", + "es2015" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c74335e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# Spaces in js, jsx, and json +[*.{coffee,js,jsx,json}] +indent_style = space +indent_size = 2 + +# Tabs in styles +[*.{less,css,scss}] +indent_style = space +indent_size = 2 + +# Spaces in HTML +[*.html] +indent_style = space +indent_size = 2 + +[.eslintrc] +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..c94e1ad --- /dev/null +++ b/.eslintrc @@ -0,0 +1,181 @@ +{ + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true, + "jasmine": true + }, + "plugins": [ + "react" + ], + "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "forOf": true, + "generators": false, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": false, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true, + "jsx": true + }, + "rules": { + "strict": [2, "never"], + "no-var": 2, + + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-unused-vars": [2, { + "vars": "local", + "args": "after-used" + }], + "no-use-before-define": 2, + + "comma-dangle": [2, "never"], + "no-cond-assign": [2, "always"], + "no-console": 1, + "no-debugger": 1, + "no-alert": 1, + "no-constant-condition": 1, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 0, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-obj-calls": 2, + "quote-props": [2, 'consistent-as-needed'], + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + /*"block-scoped-var": 2,*/ + + "consistent-return": 2, + "curly": [2, "multi-line"], + "default-case": 2, + "dot-notation": [2, { + "allowKeywords": true + }], + "eqeqeq": 2, + "guard-for-in": 2, + "no-caller": 2, + "no-else-return": 2, + "no-eq-null": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-throw-literal": 2, + "no-with": 2, + "radix": 2, + "vars-on-top": 2, + "wrap-iife": [2, "any"], + "yoda": 2, + + "indent": [2, 2], + "brace-style": [2, + "1tbs", { + "allowSingleLine": true + }], + "quotes": [ + 2, "single", "avoid-escape" + ], + "camelcase": [2, { + "properties": "never" + }], + "comma-spacing": [2, { + "before": false, + "after": true + }], + "comma-style": [2, "last"], + "eol-last": 2, + "func-names": 0, + "key-spacing": [2, { + "beforeColon": false, + "afterColon": true + }], + "new-cap": [2, { + "newIsCap": true, + "capIsNewExceptions": [ + "List", + "Map", + "OrderedMap", + "Set", + "OrderedSet", + "Stack", + "Record", + "Seq", + "KeyedSeq", + "IndexedSeq", + "SetSeq", + "Iterable", + "KeyedIterable", + "IndexedIterable", + "SetIterable", + "Collection", + "KeyedCollection", + "IndexedCollection", + "SetCollection" + ] + }], + "no-multiple-empty-lines": [2, { + "max": 2 + }], + "no-nested-ternary": 1, + "no-new-object": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-extra-parens": 0, + "no-underscore-dangle": 0, + "one-var": [2, "never"], + "padded-blocks": [2, "never"], + "semi": [2, "always"], + "semi-spacing": [2, { + "before": false, + "after": true + }], + "space-after-keywords": 2, + "space-before-blocks": 2, + "space-before-function-paren": [2, "never"], + "space-infix-ops": 2, + "space-return-throw-case": 2, + "spaced-comment": 2, + "react/jsx-uses-react": 1, + "react/no-danger": 1, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/react-in-jsx-scope": 1, + "react/require-extension": 1, + "react/self-closing-comp": 1, + "react/sort-comp": 1 + } +} diff --git a/.gitignore b/.gitignore index 615a102..7e95031 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -node_modules/ -build/ -npm-debug.log +node_modules +build +dist +tmp +npm-debug.log* .DS_Store diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..31876ac --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,52 @@ +/*eslint-disable */ +require('babel-register'); +var path = require('path'); +var webpack = require('webpack'); +var webpackConfig = require(path.join(__dirname, 'webpack.config.babel')).default; +/*eslint-enable */ + +module.exports = function(config) { + config.set({ + browsers: ['PhantomJS'], + singleRun: false, + logLevel: config.LOG_ERROR, + plugins: [ + 'karma-phantomjs-launcher', + 'karma-phantomjs-shim', + 'karma-mocha', + 'karma-chai', + 'karma-webpack', + 'karma-sourcemap-loader', + 'karma-mocha-reporter' + ], + frameworks: [ + 'mocha', + 'chai', + 'phantomjs-shim' + ], + files: [ + path.join(__dirname, 'tests.bundle.js') + ], + preprocessors: { + 'tests.bundle.js': [ + 'webpack', + 'sourcemap' + ] + }, + webpack: { + devtool: 'eval', + resolve: webpackConfig.resolve, + module: webpackConfig.module, + plugins: [ + new webpack.IgnorePlugin(/ReactContext/) + ] + }, + webpackMiddleware: { + noInfo: true, + watchOptions: { + poll: true + } + }, + reporters: [ 'mocha' ] + }); +}; diff --git a/package.json b/package.json index 594ad36..feaea1a 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,14 @@ "description": "Starter sederhana untuk ReactJS, Webpack, dan Babel ES2015", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "webserver": "node src/server.js", - "build": "node_modules/.bin/webpack", - "dev": "node_modules/.bin/webpack-dev-server", - "deploy": "NODE_ENV=production webpack -p" + "prestart": "rimraf tmp", + "start": "webpack-dev-server --watch-poll", + "prebuild": "rimraf dist", + "build": "npm run prestart && webpack --config webpack.config.prod.babel.js -p", + "test": "npm run prestart && karma start karma.conf.js --single-run", + "tdd": "karma start karma.conf.js", + "precommit": "npm test", + "cleanup": "npm run prestart && npm run prebuild" }, "repository": { "type": "git", @@ -26,16 +29,38 @@ }, "homepage": "https://github.com/reactjs-id/react-webpack-simple-starter#readme", "dependencies": { - "babel-core": "^6.3.17", - "babel-loader": "^6.2.0", - "html-webpack-plugin": "^1.7.0", "react": "^0.14.3", - "react-dom": "^0.14.3", - "webpack": "^1.12.9" + "react-dom": "^0.14.3" }, "devDependencies": { + "babel-core": "^6.3.17", + "babel-loader": "^6.2.0", "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", + "babel-register": "^6.3.13", + "chai": "^3.4.1", + "css-loader": "^0.23.0", + "extract-text-webpack-plugin": "^0.9.1", + "file-loader": "^0.8.5", + "html-webpack-plugin": "^1.7.0", + "husky": "^0.10.2", + "karma": "^0.13.15", + "karma-chai": "^0.1.0", + "karma-mocha": "^0.2.1", + "karma-mocha-reporter": "^1.1.3", + "karma-phantomjs-launcher": "^0.2.1", + "karma-phantomjs-shim": "^1.1.2", + "karma-sourcemap-loader": "^0.3.6", + "karma-webpack": "^1.7.0", + "mocha": "^2.3.4", + "phantomjs": "^1.9.19", + "react-addons-create-fragment": "^0.14.3", + "react-addons-test-utils": "^0.14.3", + "react-hot-loader": "^1.3.0", + "rimraf": "^2.4.4", + "skin-deep": "^0.13.0", + "style-loader": "^0.13.0", + "webpack": "^1.12.9", "webpack-dev-server": "^1.14.0" } } diff --git a/readme.md b/readme.md index 809f391..4e2c2f1 100644 --- a/readme.md +++ b/readme.md @@ -14,24 +14,60 @@ Starter ini sangat sederhana dan hanya mencakup library berikut : Clone repository ini. lalu jalankan - npm install +```sh +npm install +``` -Setelah itu silakan mulai modifikasi file `src/index.js`. +untuk membuat komponen baru, buat folder di `src/components` sesuai dengan nama component yang ingin dibuat. lihat contoh component `hello`. + +untuk bisa melihat hasilnya, modifikasi file `src/index.js` dengan meng-import komponen yang baru dibuat. Untuk memulai aplikasi dengan mode development (webpack-dev-server) jalankan perintah berikut - npm run dev +```sh +npm start +``` lalu buka `http://localhost:8080/` -Untuk build aplikasi yang menghasilkan static file jalankan perintah berikut +# Testing + +untuk melakukan testing, silakan buat file dengan ekstensi `.spec.js` didalam folder komponen yang baru dibuat. silakan lihat contoh yang ada pada komponen `Hello`. + +untuk melakukan **continuous testing**, jalankan perintah berikut + +```sh +npm run tdd +``` + +sedangkan untuk melakukan testing pada sistem CI (Continuous Integration) seperti wercker/travisCI, jalankan perintah berikut + +```sh +npm test +``` + +# Deployment + +Untuk build aplikasi untuk deployment, jalankan perintah berikut + +```sh +npm run build +``` + +hasil kompilasi aplikasi akan ada di folder `dist`. + +# Lain lain - npm run build +## Testing on Commit +setiap anda melakukan `git commit` pada projek ini, secara otomatis komputer akan menjalankan command `npm test`. untuk melewati langkah ini, cukup gunakan perintah `git commit --no-verify`. -Untuk deploy production jalankan perintah berikut +## Windows Development +apabila anda melakukan development menggunakan OS Windows dan virtualbox (seperti vagrant/laravel homestead), maka tambahkan `env` HOST=0.0.0.0 sebelum menjalankan command `npm start`. - npm run deploy +```sh +$ HOST=0.0.0.0 npm start +``` # Lisensi -MIT \ No newline at end of file +MIT diff --git a/src/components/hello/index.js b/src/components/hello/index.js new file mode 100644 index 0000000..55c0bf8 --- /dev/null +++ b/src/components/hello/index.js @@ -0,0 +1,14 @@ +import React, { Component } from 'react'; + +class Hello extends Component { + render() { + return ( +
+

Halo Dunia!

+

Selamat belajar ReactJS

+
+ ); + } +} + +export default Hello; diff --git a/src/components/hello/index.spec.js b/src/components/hello/index.spec.js new file mode 100644 index 0000000..f054862 --- /dev/null +++ b/src/components/hello/index.spec.js @@ -0,0 +1,32 @@ +import { expect } from 'chai'; +import React from 'react'; +import skinDeep from 'skin-deep'; + +import Hello from './index'; + +const $ = React.createElement; + +describe('Hello Component', () => { + const tree = skinDeep.shallowRender($(Hello)); + const vdom = tree.getRenderOutput(); + + it('should render a React Component', () => { + expect(vdom).to.have.property('type', 'div'); + }); + + it('should have two children', () => { + expect(vdom.props.children.length).to.equal(2); + }); + + it('should render `Halo Dunia` on h1', () => { + let target = vdom.props.children[0]; + expect(target).to.have.property('type', 'h1'); + expect(target.props.children).to.equal('Halo Dunia!'); + }); + + it('should render `Selamat belajar ReactJS` on p', () => { + let target = vdom.props.children[1]; + expect(target).to.have.property('type', 'p'); + expect(target.props.children).to.equal('Selamat belajar ReactJS'); + }); +}); diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..bb67105 --- /dev/null +++ b/src/index.html @@ -0,0 +1,11 @@ + + + + + + React Webpack Simple Starter + + +
+ + diff --git a/src/index.js b/src/index.js index 9801755..b42163c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,12 @@ -import React, { Component } from 'react'; +import React from 'react'; import { render } from 'react-dom'; -class App extends Component { - render() { - return ( -
-

Halo Dunia!

-

Selamat belajar ReactJS

-
- ); - } -}; +import Hello from 'components/hello'; -const app = document.createElement('div'); -document.body.appendChild(app); -render(, app); \ No newline at end of file +const appContainer = document.getElementById('app'); + +render(, appContainer); + +if (DEBUG) { + window.React = React; +} diff --git a/tests.bundle.js b/tests.bundle.js new file mode 100644 index 0000000..0500c76 --- /dev/null +++ b/tests.bundle.js @@ -0,0 +1,4 @@ +/*eslint-disable */ +var context = require.context('./src', true, /.+\.spec\.js?$/); +/*eslint-enable */ +context.keys().forEach(context); diff --git a/webpack.config.babel.js b/webpack.config.babel.js new file mode 100644 index 0000000..ea8abe5 --- /dev/null +++ b/webpack.config.babel.js @@ -0,0 +1,48 @@ +import webpack from 'webpack'; +import path from 'path'; +import config from './webpack.main'; + +const host = process.env.HOST || 'localhost'; +const port = process.env.PORT || '8080'; + +config.cache = true; +config.devtool = 'source-map'; +config.debug = true; + +config.module.loaders = [ + { + test: /\.js$/, + exclude: /node_modules/, + loaders: [ + 'react-hot', + 'babel?cacheDirectory' + ] + }, + { + test: /\.css$/, + loaders: [ + 'style', + 'css?&modules&importLoaders=1&localIdentName=[path][name]---[local]---[hash:base64:5]&sourceMap' + ] + }, + ...config.module.loaders +]; + +config.plugins = [ + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin(), + ...config.plugins +]; + +config.output.publicPath = 'http://localhost:8080/'; + +config.devServer = { + contentBase: path.join(__dirname, 'tmp'), + noInfo: true, + hot: true, + inline: true, + host, + port +}; + +export default config; diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index aa9ec25..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,36 +0,0 @@ -var webpack = require('webpack'); -var HtmlwebpackPlugin = require('html-webpack-plugin'); - -module.exports = { - devtool: 'source-map', - entry: [ - './src/index.js' - ], - module: { - loaders: [{ - test: /\.js?$/, - exclude: /node_modules/, - loader: 'babel', - query: { - presets: ['react', 'es2015'] - } - }] - }, - resolve: { - extensions: ['', '.js', '.jsx'] - }, - output: { - path: __dirname + '/dist/', - publicPath: '/', - filename: 'bundle.js', - }, - devServer: { - contentBase: __dirname + '/dist/' - }, - plugins: [ - new webpack.HotModuleReplacementPlugin(), - new HtmlwebpackPlugin({ - title: 'React Webpack Starter' - }) - ] -}; \ No newline at end of file diff --git a/webpack.config.prod.babel.js b/webpack.config.prod.babel.js new file mode 100644 index 0000000..11b837d --- /dev/null +++ b/webpack.config.prod.babel.js @@ -0,0 +1,44 @@ +import webpack from 'webpack'; +import path from 'path'; +import ExtractTextPlugin from 'extract-text-webpack-plugin'; +import config from './webpack.main'; + +const banner = `Generated on ${ Date.now() } - v${ require('./package.json').version }`; + +process.env.NODE_ENV = 'production'; + +config.devtool = 'source-map'; +config.debug = false; + +config.module.loaders = [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel' + }, + { + test: /\.css$/, + loader: ExtractTextPlugin.extract( + 'style', + 'css?&modules&importLoaders=1&localIdentName=[path][name]---[local]---[hash:base64:5]&sourceMap' + ) + }, + ...config.module.loaders +]; + +config.plugins = [ + new webpack.optimize.DedupePlugin(), + new ExtractTextPlugin('[hash].css'), + new webpack.optimize.OccurenceOrderPlugin(), + new webpack.optimize.AggressiveMergingPlugin(), + new webpack.optimize.UglifyJsPlugin(), + new webpack.BannerPlugin(banner, { + raw: false, + entryOnly: false + }), + ...config.plugins +]; + +config.output.path = path.join(__dirname, 'dist'); + +export default config; diff --git a/webpack.main.js b/webpack.main.js new file mode 100644 index 0000000..4cfa186 --- /dev/null +++ b/webpack.main.js @@ -0,0 +1,44 @@ +import webpack from 'webpack'; +import path from 'path'; + +import HtmlPlugin from 'html-webpack-plugin'; + +const srcPath = path.join(__dirname, 'src'); + +let config = { + devtool: 'source-map', + entry: './src/index.js', + module: { + loaders: [ + { + test: /\.(jpe?g|png|gif)$/i, + loader: 'file?hash=sha1&digest=hex&name=[hash].[ext]' + }, + { + test: /\.html$/, + loaders: ['file?name=[name].[ext]'] + } + ] + }, + resolve: { + root: srcPath, + extensions: ['', '.js', '.jsx'] + }, + plugins: [ + new HtmlPlugin({ + inject: true, + template: path.join(srcPath, 'index.html') + }), + new webpack.DefinePlugin({ + DEBUG: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'true')) + }) + ], + output: { + filename: '[hash].js', + pathInfo: true, + path: path.join(__dirname, 'tmp'), + publicPath: '/' + } +}; + +export default config;