├── .github └── ISSUE_TEMPLATE ├── .gitignore ├── .travis.yml ├── README.md ├── how-to-translate.md ├── img ├── chai.png ├── eslint.png ├── flow.png ├── gulp.png ├── js.png ├── mocha.png ├── npm.png ├── react.png ├── redux.png ├── webpack.png └── yarn.png ├── mdlint.js ├── package.json ├── tutorial ├── 1-node-npm-yarn-package-json │ ├── README.md │ ├── index.js │ └── package.json ├── 10-immutable-redux-improvements │ ├── .gitignore │ ├── README.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── actions │ │ │ │ └── dog-actions.js │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ │ ├── button.jsx │ │ │ │ └── message.jsx │ │ │ ├── containers │ │ │ │ ├── bark-button.js │ │ │ │ └── bark-message.js │ │ │ └── reducers │ │ │ │ └── dog-reducer.js │ │ ├── server │ │ │ └── index.js │ │ └── shared │ │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 11-testing-mocha-chai-sinon │ ├── .gitignore │ ├── README.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── actions │ │ │ │ └── dog-actions.js │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ │ ├── button.jsx │ │ │ │ └── message.jsx │ │ │ ├── containers │ │ │ │ ├── bark-button.js │ │ │ │ └── bark-message.js │ │ │ └── reducers │ │ │ │ └── dog-reducer.js │ │ ├── server │ │ │ └── index.js │ │ ├── shared │ │ │ └── dog.js │ │ └── test │ │ │ ├── client │ │ │ └── state-test.js │ │ │ └── shared │ │ │ └── dog-test.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 12-flow │ ├── .flowconfig │ ├── .gitignore │ ├── README.md │ ├── dist │ │ ├── client-bundle.js.map │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── actions │ │ │ │ └── dog-actions.js │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ │ ├── button.jsx │ │ │ │ └── message.jsx │ │ │ ├── containers │ │ │ │ ├── bark-button.js │ │ │ │ └── bark-message.js │ │ │ └── reducers │ │ │ │ └── dog-reducer.js │ │ ├── server │ │ │ └── index.js │ │ ├── shared │ │ │ └── dog.js │ │ └── test │ │ │ ├── client │ │ │ └── state-test.js │ │ │ └── shared │ │ │ └── dog-test.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 2-packages │ ├── .gitignore │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── 3-es6-babel-gulp │ ├── .gitignore │ ├── README.md │ ├── gulpfile.js │ ├── package.json │ ├── src │ │ └── index.js │ └── yarn.lock ├── 4-es6-syntax-class │ ├── .gitignore │ ├── README.md │ ├── gulpfile.js │ ├── package.json │ ├── src │ │ ├── dog.js │ │ └── index.js │ └── yarn.lock ├── 5-es6-modules-syntax │ ├── .gitignore │ ├── README.md │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── dog.js │ │ └── index.js │ └── yarn.lock ├── 6-eslint │ ├── .gitignore │ ├── README.md │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── dog.js │ │ └── index.js │ └── yarn.lock ├── 7-client-webpack │ ├── .gitignore │ ├── README.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ └── app.js │ │ ├── server │ │ │ └── index.js │ │ └── shared │ │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── 8-react │ ├── .gitignore │ ├── README.md │ ├── dist │ │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ │ ├── client │ │ │ └── app.jsx │ │ ├── server │ │ │ └── index.js │ │ └── shared │ │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock └── 9-redux │ ├── .gitignore │ ├── README.md │ ├── dist │ └── index.html │ ├── gulpfile.babel.js │ ├── package.json │ ├── src │ ├── client │ │ ├── actions │ │ │ └── dog-actions.js │ │ ├── app.jsx │ │ ├── components │ │ │ ├── button.jsx │ │ │ └── message.jsx │ │ ├── containers │ │ │ ├── bark-button.js │ │ │ └── bark-message.js │ │ └── reducers │ │ │ └── dog-reducer.js │ ├── server │ │ └── index.js │ └── shared │ │ └── dog.js │ ├── webpack.config.babel.js │ └── yarn.lock └── yarn.lock /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ### Type of issue: (feature suggestion, bug, translation?) 2 | 3 | ### Chapter: 4 | 5 | ### If it's a bug: 6 | 7 | Please try using the code provided in this repository instead of your own to see if that solves the issue. If it does, compare the content of relevant files to see if anything is missing in your version. Every chapter is automatically tested, so issues are likely coming from missing an instruction in the tutorial, a typo, or something like this. Feel free to open an issue if there is a problem with instructions though. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | "6" 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Stack from Scratch [ฉบับแปลไทย] 2 | 3 | [![Build Status](https://travis-ci.org/verekia/js-stack-from-scratch.svg?branch=master)](https://travis-ci.org/verekia/js-stack-from-scratch) [![Join the chat at https://gitter.im/js-stack-from-scratch/Lobby](https://badges.gitter.im/js-stack-from-scratch/Lobby.svg)](https://gitter.im/js-stack-from-scratch/Lobby) 4 | 5 | [![Yarn](/img/yarn.png)](https://yarnpkg.com/) 6 | [![React](/img/react.png)](https://facebook.github.io/react/) 7 | [![Gulp](/img/gulp.png)](http://gulpjs.com/) 8 | [![Redux](/img/redux.png)](http://redux.js.org/) 9 | [![ESLint](/img/eslint.png)](http://eslint.org/) 10 | [![Webpack](/img/webpack.png)](https://webpack.github.io/) 11 | [![Mocha](/img/mocha.png)](https://mochajs.org/) 12 | [![Chai](/img/chai.png)](http://chaijs.com/) 13 | [![Flow](/img/flow.png)](https://flowtype.org/) 14 | 15 | **หมายเหตุผู้แปล**: Tutorial นี้แปลมาจากต้นฉบับภาษาอังกฤษ [JavaScript Stack from Scratch](https://github.com/verekia/js-stack-from-scratch) โดยส่วนมากจะแปลและแปลงสำนวนจากคำอธิบายดั้งเดิมตรงๆ และอาจมีบางส่วนที่อธิบายเพิ่มเติมขึ้นมาเพื่อให้เห็นภาพชัดเจนมากขึ้น สำหรับใครที่เห็นว่าคำแปลบางส่วนดูแปลกๆ หรืออธิบายเข้าใจยาก สามารถ Pull Request ที่แก้มาได้เลยครับ และน้อมรับคำติชมทุกประการครับผม :) 16 | 17 | ยินดีต้อนรับสู่ Tutorial สอนการสร้าง และใช้งานเครื่องมือต่างๆ ของภาษา JavaScript: **JavaScript Stack from Scratch** 18 | 19 | Tutorial นี้เป็น tutorial ที่จะสอนการใช้งาน JavaScript tool ต่างๆ ด้วยกัน โดยจะสอนแบบตรงไปตรงมา อธิบายทีละจุดทีละเรื่อง ซึ่งผู้อ่านควรจะมีพื้นฐานทางด้านการเขียนโปรแกรมมาบ้าง รวมถึงรู้เบสิคของ JavaScript มาบ้าง **Tutorial นี้จะเน้นไปที่การรวมเอา tools ต่างๆ หลายๆ ตัวมาใช้งานด้วยกัน** และให้**ตัวอย่างโค้ดที่เรียบง่ายที่สุด** สำหรับในแต่ละ tool เพื่อให้นำไปใช้งานต่อได้ง่ายและเข้าใจมากขึ้น ซึ่งคุณเองก็สามารถที่จะอ่าน tutorial นี้ไปแต่ละบท *พร้อมกับสร้าง boilerplate ไว้ใช้งานเองได้ตามที่ต้องการ* 20 | 21 | ในความเป็นจริง คุณไม่จำเป็นต้องใช้ stack แบบที่เรานำเสนอในการพัฒนา Web Page เล็กๆ ที่ไม่จำเป็นต้องมี interaction อะไรมากมาย (ถ้าโปรเจคมีขนาดเล็ก ใช้แค่ Browserify/Webpack + Babel + jQuery ก็เพียงพอแล้วในการที่เราจะเขียนโค้ดตามมาตรฐาน ES6 แต่ก็ใช้ Library เดิมๆ อย่าง jQuery ได้) แต่ถ้าคุณต้องทำ Web Apps ขนาดใหญ่ รองรับต่อการ scale ขนาดของ Web Apps รวมถึงต้องการความช่วยเหลือในการ setup tool ต่่างๆ, Tutorial นี้จะช่วยเหลือคุณได้มาก 22 | 23 | เนื่องจากเป้าหมายของ Tutorial นี้คือจะสอนการใช้ tool หลายๆ ตัวร่วมกัน ซึ่งผมเองก็จะไม่ลงรายละเอียดว่า tools แต่ละตัวนั้นทำงานยังไง ถ้าอยากก็รู้สามารถอ่าน documentation หรือ tutorial แบบ in-depth ตัวอื่นๆ ได้ ถ้าต้องการเข้าใจการทำงานของ tools แต่ละอันมากขึ้น 24 | 25 | ซึ่ง stack ที่จะใช้ในบทนี้นั้นจะใช้ React เป็นหลัก ซึ่งถ้าคุณอยากเรียนรู้ React โปรเจค [create-react-app](https://github.com/facebookincubator/create-react-app) นั้นก็ช่วยให้คุณ set up React environment ได้อย่างง่ายได้ โดยที่คุณไม่ต้องไป config อะไรเองเลย ซึ่งโดยส่วนตัวผมเอง(ผู้เขียน) ก็แนะนำให้ใช้ `create-react-app` สำหรับผู้มาใหม่ที่ต้องใช้ React และต้องเข้าใจเรื่องต่างๆ ให้รวดเร็วหน่อย แต่ใน Tutorial นี้ คุณไม่จำเป็นต้องใช้ config ที่ถูกตั้งค่ามาแล้วเหล่านั้น เพราะเราต้องการให้คุณเข้าใจทุกอย่างตั้งแต่แรกเริ่มไปจนถึงได้ Web Apps ออกมา 26 | 27 | นอกจากนี้เรายังมีตัวอย่างโค้ดให้ด้วยสำหรับแต่ละบท และทุกบทนั้นสามารถ run ได้ด้วยคำสั่งง่ายๆ อย่าง `yarn && yarn start` หรือ `npm install && npm start` ผมเองก็ขอแนะนำให้ทุกท่านเริ่มเขียนทุกอย่างเองตั้งแต่ไม่มีอะไรเลย โดย**ทำตามขั้นตอนแต่ละขั้น** ซึ่งจะอ้างอิงตาม tutorial ในแต่ละบท 28 | 29 | **ทุกบทนั้นจะมีการอ้างถึงและใช้งานโค้ดจากบทก่อนหน้า** ดังนั้นถ้าคุณกำลังมองหา boilerplate ที่มีครบทุกอย่าง ก็ทำได้ง่ายๆ โดยการ clone โค้ดจากบทสุดท้ายมาก็พอแล้ว 30 | 31 | หมายเหตุ: ลำดับการเรียงของบทนั้นไม่ค่อยเหมาะสมกับวิธีการเรียนแบบปกติ เช่น การทำ testing หรือ type checking ควรทำก่อนที่จะเขียน React ด้วยซ้ำ ซึ่งจะเป็นการยากหากเราจะทำการสลับบทเพื่อเรียงลำดับใหม่ เพราะยังมีการปรับแก้แต่ละบทอยู่เรื่อยๆ ซึ่งผม(ผู้เขียน) ขอให้ทุกอย่างเสร็จสิ้นก่อน แล้วเราจะพิจารณาเรื่องนี้อีกทีหนึ่ง 32 | 33 | Code ที่อยู่ใน Tutorial นี้สามารถทำงานได้ทั้งบน Linux, macOS และ Windows 34 | 35 | ## สารบัญ 36 | 37 | [บทที่ 1 - Node, NPM, Yarn และ package.json](/tutorial/1-node-npm-yarn-package-json) 38 | 39 | [บทที่ 2 - ติดตั้งและใช้งาน package](/tutorial/2-packages) 40 | 41 | [บทที่ 3 - ตั้งค่าเพื่อใช้งาน ES6 โดยใช้ Babel และ Gulp](/tutorial/3-es6-babel-gulp) 42 | 43 | [บทที่ 4 - การใช้ ES6 syntax ในการเขียน Class](/tutorial/4-es6-syntax-class) 44 | 45 | [บทที่ 5 - การใช้ ES6 syntax ในการสร้าง modules](/tutorial/5-es6-modules-syntax) 46 | 47 | [บทที่ 6 - การใช้ ESLint](/tutorial/6-eslint) 48 | 49 | [บทที่ 7 - พัฒนาแอพฝั่ง Client โดยใช้ Webpack](/tutorial/7-client-webpack) 50 | 51 | [บทที่ 8 - React](/tutorial/8-react) 52 | 53 | [บทที่ 9 - Redux](/tutorial/9-redux) 54 | 55 | [บทที่ 10 - Immutable JS และ Redux Improvements](/tutorial/10-immutable-redux-improvements) 56 | 57 | [บทที่ 11 - การทำ Testing โดยใช้ Mocha, Chai และ Sinon](/tutorial/11-testing-mocha-chai-sinon) 58 | 59 | [บทที่ 12 - เช็ค Data Type ด้วย Flow](/tutorial/12-flow) 60 | 61 | ## เร็วๆ นี้ 62 | 63 | Production / development environments, Express, React Router, Server-Side Rendering, Styling, Enzyme, Git Hooks. 64 | 65 | ## ภาษาอื่นๆ 66 | 67 | - [中文](https://github.com/pd4d10/js-stack-from-scratch) by [@pd4d10](http://github.com/pd4d10) 68 | - [Italiano](https://github.com/fbertone/js-stack-from-scratch) by [Fabrizio Bertone](https://github.com/fbertone) 69 | - [日本語](https://github.com/takahashim/js-stack-from-scratch) by [@takahashim](https://github.com/takahashim) 70 | - [Русский](https://github.com/UsulPro/js-stack-from-scratch) by [React Theming](https://github.com/sm-react/react-theming) 71 | - [ไทย](https://github.com/MicroBenz/js-stack-from-scratch) by [MicroBenz](https://github.com/MicroBenz) 72 | 73 | If you want to add your translation, please read the [translation recommendations](/how-to-translate.md) to get started! 74 | 75 | ## Credits 76 | 77 | เขียนโดย [@verekia](https://twitter.com/verekia) – [verekia.com](http://verekia.com/). 78 | 79 | ลิขสิทธิ์: MIT 80 | -------------------------------------------------------------------------------- /how-to-translate.md: -------------------------------------------------------------------------------- 1 | # How to translate this tutorial 2 | 3 | Thank you for your interest in translating my tutorial! Here are a few recommendations to get started. 4 | 5 | This tutorial is in constant evolution to provide the best learning experience to readers. Both the code and `README.md` files will change over time. It is great if you do a one-shot translation that won't evolve, but it would be even better if you could try to keep up with the original English version as it changes! 6 | 7 | Here is what I think is a good workflow: 8 | 9 | - Check if there is already a translation issue open for your language. If that's the case, get in touch with the folks who opened it and consider collaborating. All maintainers will be mentioned on the English repo, so team work is encouraged! You can open issues on their translation fork project to offer your help on certain chapters for instance. 10 | 11 | - Join the [Translations Gitter room](https://gitter.im/js-stack-from-scratch/Translations) if you're feeling chatty. 12 | 13 | - Fork the main [English repository](https://github.com/verekia/js-stack-from-scratch). 14 | 15 | - Open an issue on the English repo to show you're currently working on a translation. 16 | 17 | - Translate the `README.md` files. 18 | 19 | - Add a note somewhere explaining on the main `README.md` that this is a translation, with a link to the English repository. If you don't plan to make the translation evolve over time, you can maybe add a little note saying to refer to the English one for an up-to-date version of the tutorial. I'll leave that up to your preference. 20 | 21 | - Submit a Pull Request to the English repo to add a link to your forked repository under the Translations section of the main `README.md`. It could look like this: 22 | 23 | ```md 24 | ## Translations 25 | 26 | - [Language](http://github.com/yourprofile/your-fork) by [You](http://yourwebsite.com) 27 | or 28 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://twitter.com/yourprofile) 29 | or 30 | - [Language](http://github.com/yourprofile/your-fork) by [@You](http://github.com/yourprofile) 31 | ``` 32 | 33 | Since I want to reward you for your good work as much as possible, you can put any link you like on your name (to your personal website, Twitter profile, or Github profile for instance). 34 | 35 | - After your original one-shot translation, if you want to update your repo with the latest change from the main English repo, [sync your fork](https://help.github.com/articles/syncing-a-fork/) with my repo. To make it easy to see what changed since your initial translation, you can use Github's feature to [compare commits](https://help.github.com/articles/comparing-commits-across-time/#comparing-commits). Set the **base** to the last commit from the English repo you used to translate, and compare it to **master**, like so: 36 | 37 | 38 | https://github.com/verekia/js-stack-from-scratch/compare/dfab78b581a3da800daeb3686b900dd9ea972da0...master 39 | 40 | 41 | That should give you a easy-to-read diff to see exactly what changed in `README.md` files since your translation! 42 | -------------------------------------------------------------------------------- /img/chai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/chai.png -------------------------------------------------------------------------------- /img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/eslint.png -------------------------------------------------------------------------------- /img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/flow.png -------------------------------------------------------------------------------- /img/gulp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/gulp.png -------------------------------------------------------------------------------- /img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/js.png -------------------------------------------------------------------------------- /img/mocha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/mocha.png -------------------------------------------------------------------------------- /img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/npm.png -------------------------------------------------------------------------------- /img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/react.png -------------------------------------------------------------------------------- /img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/redux.png -------------------------------------------------------------------------------- /img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/webpack.png -------------------------------------------------------------------------------- /img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicroBenz/js-stack-from-scratch/2b01a4890d67c164b8a7afba508860a63a893b1d/img/yarn.png -------------------------------------------------------------------------------- /mdlint.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const markdownlint = require('markdownlint'); 3 | 4 | const config = { 5 | 'default': true, 6 | 'line_length': false, 7 | 'no-emphasis-as-header': false 8 | } 9 | 10 | const files = glob.sync('**/*.md', { ignore: '**/node_modules/**' }); 11 | 12 | markdownlint({ files, config }, (err, result) => { 13 | if (!err) { 14 | const resultString = result.toString(); 15 | console.log('== Linting Markdown Files...'); 16 | if (resultString) { 17 | console.log(resultString); 18 | process.exit(1); 19 | } else { 20 | console.log('== OK!'); 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "test": "yarn run mdlint && cd tutorial/1-node-npm-yarn-package-json && yarn && yarn run tutorial-test && cd ../2-packages && yarn && yarn run tutorial-test && cd ../3-es6-babel-gulp && yarn && yarn run tutorial-test && cd ../4-es6-syntax-class && yarn && yarn run tutorial-test && cd ../5-es6-modules-syntax && yarn && yarn run tutorial-test && cd ../6-eslint && yarn && yarn run tutorial-test && cd ../7-client-webpack && yarn && yarn run tutorial-test && cd ../8-react && yarn && yarn run tutorial-test && cd ../9-redux && yarn && yarn run tutorial-test && cd ../10-immutable-redux-improvements && yarn && yarn run tutorial-test && cd ../11-testing-mocha-chai-sinon && yarn && yarn run tutorial-test && cd ../12-flow && yarn && yarn run tutorial-test", 7 | "mdlint": "node mdlint.js" 8 | }, 9 | "devDependencies": { 10 | "glob": "^7.1.1", 11 | "markdownlint": "^0.3.0", 12 | "yarn": "^0.16.1" 13 | }, 14 | "repository": "verekia/js-stack-from-scratch", 15 | "author": "Jonathan Verrecchia - @verekia", 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /tutorial/1-node-npm-yarn-package-json/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 1 - Node, NPM, Yarn และ package.json 2 | 3 | ในบทนี้เราจะพูดถึงการ set up Node, NPM, Yarn, และการใช้งาน `package.json` ในขั้นต้น 4 | 5 | แรกสุด เราต้องติดตั้ง Node ก่อน ซึ่งเราจะไม่ได้ใช้ Node สำหรับการทำ Back-End ด้วย JavaScript เท่านั้น แต่เครื่องมือที่เราใช้สำหรับ Front-End ก็ใช้ Node ด้วย 6 | 7 | ไปที่หน้า[ดาวน์โหลด](https://nodejs.org/en/download/current/) สำหรับ macOS หรือ Windows แบบ binaries หรือใช้ [package manager](https://nodejs.org/en/download/package-manager/) สำหรับ Linux 8 | 9 | สำหรับ **Ubuntu / Debian** คุณสามารถใช้คำสั่งนี้เพื่อติดตั้ง Node ได้ 10 | 11 | ```bash 12 | curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 13 | sudo apt-get install -y nodejs 14 | ``` 15 | 16 | ติดตั้งเวอร์ชันอะไรก็ได้ที่มากกว่า 6.5.0 17 | 18 | `npm` เป็น package manager สำหรับ Node ซึ่งติดตั้งโดยอัตโนมัติอยู่แล้วเมื่อติดตั้ง Node ดังนั้นจึงไม่ต้องติดตั้งอะไรเพิ่ม 19 | 20 | **Note**: ถ้าเคยติดตั้ง Node มาแล้ว ให้ติดตั้ง `nvm` ([Node Version Manager](https://github.com/creationix/nvm)) แล้วใช้ `nvm` ในการติดตั้งเวอร์ชันล่าสุดที่คุณต้องการ 21 | 22 | [Yarn](https://yarnpkg.com/) ก็เป็น package manager เหมือนกับ NPM แต่ว่าเร็วกว่า NPM, ใช้งานแบบ Offline ได้ รวมถึงสามารถค้นหา dependencies ต่างๆ [แบบคาดเดาได้มากขึ้น](https://yarnpkg.com/en/docs/yarn-lock) ตั้งแต่ที่ Yarn [release](https://code.facebook.com/posts/1840075619545360) ออกมาเมื่อตุลาคม 2016 นั้น ก็ได้รับการตอบรับอย่างดี รวมถึงมีการ fix bug ได้รวดเร็วมาก จนกลายเป็น package manager ตัวใหม่ที่เป็นทางเลือกนอกเหนือจากการใช้ NPM ซึ่งใน Tutorial ของเรานั้นจะใช้ Yarn ทั้งหมด แต่ถ้าคุณอยากใช้ NPM เดิมๆ ก็เพียงแค่ใช้ `npm install --save` กับ `npm install --dev` แทน `yarn add` กับ `yarn add --dev` ที่อยู่ใน Tutorial นี้ทั้งหมด 23 | 24 | - ติดตั้ง Yarn โดยทำตาม [instructions](https://yarnpkg.com/en/docs/install) ตามนี้ หรือจะสั่ง `npm install -g yarn` หรือ `sudo npm install -g yarn` ก็ได้ (ใช่แล้ว เราใช้ NPM เพื่อติดตั้ง Yarn มันก็คล้ายๆ กับใช้ Internet Explorer หรือ Safari เพื่อติดตั้ง Chrome นั้นแหละ!) 25 | 26 | - สร้าง folder ใหม่ขึ้นมา (ชื่ออะไรก็ได้) และ `cd` เข้าไป 27 | - สั่ง `yarn init` และตอบคำถามตามที่แสดงมา (หรือใช้ `yarn init -y` เพื่อข้ามทุกคำถามเลยก็ได้) เพื่อให้ Yarn ทำการสร้างไฟล์ `package.json` ขึ้นมาเอง 28 | - สร้าง `index.js` แล้วเขียน `console.log('Hello world')` ในไฟล์นั้นลงไป 29 | - สั่ง `node .` ในโฟลเดอร์ปัจจุบัน (`index.js` คือ default file ที่ Node จะมองหาในโฟลเดอร์ปัจจุบัน) เมื่อสั่งรันแล้วควรจะเห็นคำว่า "Hello world" ขึ้นมา 30 | 31 | ซึ่งการสั่ง `node .` นั้นดูจะ low-level ไปนิด เราจะใช้ NPM/Yarn script เพื่อให้สั่งรันคำสั่งที่ว่านั้นแทน แต่จะได้ความ abstraction และเข้าใจง่ายขึ้นมาด้วย เพราะเราจะสั่งคำสั่งด้วย `yarn start` เฉยๆ เลย แม้ว่าในอนาคตตัวคำสั่งที่เราจะสั่งมันจะซับซ้อนขึ้นไปกว่านี้อีก เราก็สั่ง `yarn start` ก็พอ ซึ่งวิธีการก็ทำตามนี้ 32 | 33 | - ในไฟล์ `package.json` เพิ่ม `scripts` object เข้าไปใน root object แบบนี้ 34 | 35 | ```json 36 | "scripts": { 37 | "start": "node ." 38 | } 39 | ``` 40 | 41 | ซึ่ง `package.json` ต้องเป็นไฟล์ JSON จริงๆ (ห้ามมี trailing commas) ดังนี้ระวังให้ดีเมื่อต้องแก้ไข `package.json` ด้วยมือ 42 | 43 | - สั่ง `yarn start` ทีนี้ก็จะเห็นคำว่า `Hello world` แล้ว 44 | 45 | - สร้าง `.gitignore` ขึ้นมา และเพิ่มข้อมูลเหล่านี้ลงไป 46 | 47 | ```gitignore 48 | npm-debug.log 49 | yarn-error.log 50 | ``` 51 | 52 | **Note**: ถ้าดูในไฟล์ `package.json` ที่เรามีให้ในโปรเจคนี้ จะเห็น script `tutorial-test` ในทุกๆ บทเลย ซึ่ง script นี้จะช่วยให้ผม(ผู้เขียน) เทสว่าโค้ดในบทนี้ใช้งานได้ เมื่อทำการรัน `yarn && yarn start` ดังนั้น คุณสามารถลบ script นี้ทิ้งได้เลยในโปรเจคของคุณเอง 53 | 54 | บทถัดไป [บทที่ 2 - ติดตั้งและใช้งาน package](/tutorial/2-packages) 55 | 56 | กลับไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 57 | -------------------------------------------------------------------------------- /tutorial/1-node-npm-yarn-package-json/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello world'); 2 | -------------------------------------------------------------------------------- /tutorial/1-node-npm-yarn-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "node .", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "yarn start" 9 | }, 10 | "repository": "verekia/js-stack-from-scratch", 11 | "author": "Jonathan Verrecchia - @verekia", 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 10 - Immutable JS และ Redux Improvements 2 | 3 | ## Immutable JS 4 | 5 | แตกต่างกับบทที่แล้วโดยสิ้นเชิง เนื้อหาในบทนี้จะง่ายขึ้นมาก ซึ่งเกี่ยวข้องกับการใช้งาน Redux ให้มีประสิทธิภาพมากขึ้น 6 | 7 | แรกสุด เราจะทำการเพิ่ม **Immutable JS** เข้าไปในโค้ดเดิมของเรา Immutable นั้นเป็น library ที่ช่วยให้เราจัดการ, แก้ไข object ได้โดยไม่แก้ไข object ตรงๆ เช่น แทนที่จะทำแบบนี้ 8 | 9 | ```javascript 10 | const obj = { a: 1 }; 11 | obj.a = 2; // เปลี่ยนแปลง `obj` ตัวเดิม 12 | ``` 13 | 14 | เราจะทำแบบนี้แทน 15 | 16 | ```javascript 17 | const obj = Immutable.Map({ a: 1 }); 18 | obj.set('a', 2); // ได้ object ก้อนใหม่มา โดยที่ไม่ได้แก้ไขค่าใน `obj` ตัวเดิมตรงๆ 19 | ``` 20 | 21 | วิธีการแบบนี้ทำให้เราสามารถเขียนโปรแกรมในลักษณะของ **functional programming** ได้ ซึ่งจะเป็นประโยชน์มากเมื่อใช้หลักการเขียนโปรแกรมแบบนี้คู่กับ Redux เพราะถ้าดูดีๆ reducer function ของเรานั้น*ต้อง*เป็น pure function ที่ไม่ได้แก้ไข state ที่ถูกส่งมาเป็น parameter ตรงๆ แต่แปลงไปเป็น state object ก้อนใหม่แทน ซึ่งการใช้ Immutable จะช่วยงานเราในส่วนนี้ได้ 22 | 23 | - สั่ง `yarn add immutable` 24 | 25 | เราจะมีการใช้ `Map` ในโปรเจคของเรา แต่ใน ESLint และ Airbnb นั้นจะบ่นเราเรื่องของการใช้ตัวแปรชื่อพิมพ์ใหญ่ทั้งๆ ที่มันไม่ใช่ class ให้เราทำการเพิ่มค่าส่วนนี้เข้าไปใน `package.json` ในส่วนของ `eslintConfig` 26 | 27 | ```json 28 | "rules": { 29 | "new-cap": [ 30 | 2, 31 | { 32 | "capIsNewExceptions": [ 33 | "Map", 34 | "List" 35 | ] 36 | } 37 | ] 38 | } 39 | ``` 40 | 41 | นั่นจะทำให้เราใช้ `Map` กับ `List` (Immutable objects สองชนิดที่เรามักจะใช้แทบจะทุกครั้ง) ได้แล้ว โดยที่ ESLint มองข้ามกฎการตั้งชื่อเริ่มต้นด้วยตัวพิมพ์ใหญ่ไป 42 | 43 | กลับมาที่เรื่องของ Immutable กัน ใน `dog-reducer.js` ให้แก้ไขโค้ดให้มีหน้าตาดังนี้ 44 | 45 | ```javascript 46 | import Immutable from 'immutable'; 47 | import { MAKE_BARK } from '../actions/dog-actions'; 48 | 49 | const initialState = Immutable.Map({ 50 | hasBarked: false, 51 | }); 52 | 53 | const dogReducer = (state = initialState, action) => { 54 | switch (action.type) { 55 | case MAKE_BARK: 56 | return state.set('hasBarked', action.payload); 57 | default: 58 | return state; 59 | } 60 | }; 61 | 62 | export default dogReducer; 63 | ``` 64 | 65 | ตอนนี้จะเห็นว่า state เริ่มต้นจะถูกสร้างมา โดยใช้ Immutable Map แล้ว และ state ใหม่จะถูกสร้างมาโดยใช้ function `set()` เผื่อหลีกเลี่ยงการเปลี่ยนแปลงค่าใน state เก่าโดยตรง 66 | 67 | ในโค้ด `containers/bark-message.js` แก้ function `mapStateToProps` ให้ใช้ `.get('hasBarked')` แทนที่จะใช้ `.hasBarked` 68 | 69 | ```javascript 70 | const mapStateToProps = state => ({ 71 | message: state.dog.get('hasBarked') ? 'The dog barked' : 'The dog did not bark', 72 | }); 73 | ``` 74 | 75 | แอพจะทำงานได้เหมือนเดิม ตามที่เราเคยทำในบทที่แล้ว 76 | 77 | **หมายเหตุ**: ถ้า Babel แจ้งเรื่องเกี่ยวกับ Immutable มีขนาดเกิน 100KB ให้เพิ่ม `"compact": false` ในไฟล์ `package.json` ภายใต้ field `babel` 78 | 79 | ดังที่เห็นใน code ด้านบน state object ของเรายังคงเก็บ เป็น plain object ที่มี `dog` เป็น attribute เหมือนเดิม ซึ่งไม่ใช่ immutable object ซึ่งเป็นเรื่องที่รับได้โดยปกติ แต่ถ้าหากเราต้องการให้ทุกอย่างถูกจัดการด้วยความเป็น immutable objects เท่านั้น คุณต้องใช้ package `redux-immutable` เพื่อแทนที่ function `combineReducers` ของ Redux ไปด้วย 80 | 81 | **ส่วนนี้จะทำหรือไม่ทำก็ได้ แต่หากอยากใช้ `redux-immutable` ก็ให้ทำตามนี้** 82 | 83 | - สั่ง `yarn add redux-immutable` 84 | - แทน function `combineReducers` ในไฟล์ `app.jsx` โดยใช้ package ที่ import มาจาก `redux-immutable` แทน 85 | - ในไฟล์ `bark-message.js` แทน `state.dog.get('hasBarked')` ด้วย `state.getIn(['dog', 'hasBarked'])` 86 | 87 | ## Redux Actions 88 | 89 | เมื่อคุณเริ่มเพิ่ม actions เข้าไปในแอพมากขึ้น เราจะค้นพบว่าเรามักจะทำอะไรซ้ำซากคล้ายๆ เดิมเยอะเหลือเกิน package `redux-actions` ช่วยให้เราลดความซ้ำซากเหล่านั้นได้ ด้วยความช่วยเหลือของ `redux-actions` เราสามารถเขียนโค้ด `dog-actions.js` ให้บางลงได้เยอะ แบบนี้ 90 | 91 | ```javascript 92 | import { createAction } from 'redux-actions'; 93 | 94 | export const MAKE_BARK = 'MAKE_BARK'; 95 | export const makeBark = createAction(MAKE_BARK, () => true); 96 | ``` 97 | 98 | `redux-actions` เป็นหนึ่งในการ implement ของ [Flux Standard Action](https://github.com/acdlite/flux-standard-action) model เหมือนๆ กับ action ที่เราเคยเขียนก่อนหน้านั้น ดังนั้นการใช้ `redux-actions` เหมือนให้เราเขียนโค้ดสั้นลง แต่ได้ผลเหมือนเดิม 99 | 100 | - อย่าลืมที่จะสั่ง `yarn add redux-actions` เข้าไปด้วยก่อนแก้โค้ดนี้ 101 | 102 | บทถัดไป [บทที่ 11 - การทำ Testing โดยใช้ Mocha, Chai และ Sinon](/tutorial/11-testing-mocha-chai-sinon) 103 | 104 | กลับไปยัง[บทที่แล้ว](/tutorial/9-redux) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 105 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.jsx', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | }, 18 | "rules": { 19 | "new-cap": [ 20 | 2, 21 | { 22 | "capIsNewExceptions": [ 23 | "Map", 24 | "List" 25 | ] 26 | } 27 | ] 28 | } 29 | }, 30 | "babel": { 31 | "presets": [ 32 | "latest", 33 | "react" 34 | ], 35 | "compact": false 36 | }, 37 | "dependencies": { 38 | "babel-polyfill": "^6.16.0", 39 | "immutable": "^3.8.1", 40 | "react": "^15.3.2", 41 | "react-dom": "^15.3.2", 42 | "react-redux": "^4.4.5", 43 | "redux": "^3.6.0", 44 | "redux-actions": "^0.12.0", 45 | "redux-immutable": "^3.0.8" 46 | }, 47 | "devDependencies": { 48 | "babel-loader": "^6.2.5", 49 | "babel-preset-latest": "^6.16.0", 50 | "babel-preset-react": "^6.16.0", 51 | "del": "^2.2.2", 52 | "eslint": "^3.8.1", 53 | "eslint-config-airbnb": "^12.0.0", 54 | "eslint-plugin-import": "^2.0.1", 55 | "eslint-plugin-jsx-a11y": "^2.2.3", 56 | "eslint-plugin-react": "^6.4.1", 57 | "gulp": "^3.9.1", 58 | "gulp-babel": "^6.1.2", 59 | "gulp-eslint": "^3.0.1", 60 | "webpack-stream": "^3.2.0" 61 | }, 62 | "repository": "verekia/js-stack-from-scratch", 63 | "author": "Jonathan Verrecchia - @verekia", 64 | "license": "MIT" 65 | } 66 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const MAKE_BARK = 'MAKE_BARK'; 4 | export const makeBark = createAction(MAKE_BARK, () => true); 5 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createStore } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import { combineReducers } from 'redux-immutable'; 8 | import dogReducer from './reducers/dog-reducer'; 9 | import BarkMessage from './containers/bark-message'; 10 | import BarkButton from './containers/bark-button'; 11 | 12 | const store = createStore(combineReducers({ 13 | dog: dogReducer, 14 | })); 15 | 16 | ReactDOM.render( 17 | 18 |
19 | 20 | 21 |
22 |
23 | , document.querySelector('.app') 24 | ); 25 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Button = ({ action, actionLabel }) => ; 4 | 5 | Button.propTypes = { 6 | action: PropTypes.func.isRequired, 7 | actionLabel: PropTypes.string.isRequired, 8 | }; 9 | 10 | export default Button; 11 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Message = ({ message }) =>
{message}
; 4 | 5 | Message.propTypes = { 6 | message: PropTypes.string.isRequired, 7 | }; 8 | 9 | export default Message; 10 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Button from '../components/button'; 3 | import { makeBark } from '../actions/dog-actions'; 4 | 5 | const mapDispatchToProps = dispatch => ({ 6 | action: () => { dispatch(makeBark()); }, 7 | actionLabel: 'Bark', 8 | }); 9 | 10 | export default connect(null, mapDispatchToProps)(Button); 11 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Message from '../components/message'; 3 | 4 | const mapStateToProps = state => ({ 5 | message: state.getIn(['dog', 'hasBarked']) ? 'The dog barked' : 'The dog did not bark', 6 | }); 7 | 8 | export default connect(mapStateToProps)(Message); 9 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { MAKE_BARK } from '../actions/dog-actions'; 3 | 4 | const initialState = Immutable.Map({ 5 | hasBarked: false, 6 | }); 7 | 8 | const dogReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case MAKE_BARK: 11 | return state.set('hasBarked', action.payload); 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default dogReducer; 18 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/10-immutable-redux-improvements/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 11 - การทำ Testing โดยใช้ Mocha, Chai และ Sinon 2 | 3 | ## Mocha และ Chai 4 | 5 | - สร้างโฟลเดอร์ `src/test` ขึ้นมา โฟลเดอร์นี้จะเป็นเหมือนกระจกของโฟลเดอร์ที่มีแอพของเรา ดังนั้นให้สร้างโฟลเดอร์ `src/test/client` ขึ้นมาด้วย (ซึ่งคุณสามารถสร้างโฟลเดอร์ `server` และ `shared` ขึ้นมาด้วยก็ได้ แต่เราจะยังไม่เขียน test ให้กับไฟล์เหล่านั้นในตอนนี้) 6 | 7 | - ใน `src/test/client` สร้างไฟล์ `state-test.js` ขึ้นมา โดยเราจะใช้ไฟล์นี้ในการ test Redux life cycle 8 | 9 | เราจะใช้ [Mocha](http://mochajs.org/) สำหรับเป็น testing framework ตัวหลักของเรา Mocha นั้นใช้งานง่ายมาก พร้อมทั้งมีฟีเจอร์หลากหลาย แถมยังเป็น [JavaScript testing framework ที่ได้รับความนิยมสูงสุด](http://stateofjs.com/2016/testing/)อีกด้วย รวมถึงยังมีความยืดหยุ่นสูง Mocha สามารถให้เราใช้ assertion library ที่เราอยากใช้ได้ โดยเราจะใช้ [Chai](http://chaijs.com/) เป็น assertion library ที่มี [plugins](http://chaijs.com/plugins/) ให้ใช้เยอะมาก ตามสไตล์ของการเขียน assertion ที่เราชอบ 10 | 11 | - มาทำการติดตั้ง Mocha และ Chai โดยสั่ง `yarn add --dev mocha chai` 12 | 13 | ในไฟล์ `state-test.js` เขียนโค้ดต่อไปนี้ 14 | 15 | ```javascript 16 | /* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */ 17 | 18 | import { createStore } from 'redux'; 19 | import { combineReducers } from 'redux-immutable'; 20 | import { should } from 'chai'; 21 | import { describe, it, beforeEach } from 'mocha'; 22 | import dogReducer from '../../client/reducers/dog-reducer'; 23 | import { makeBark } from '../../client/actions/dog-actions'; 24 | 25 | should(); 26 | let store; 27 | 28 | describe('App State', () => { 29 | describe('Dog', () => { 30 | beforeEach(() => { 31 | store = createStore(combineReducers({ 32 | dog: dogReducer, 33 | })); 34 | }); 35 | describe('makeBark', () => { 36 | it('should make hasBarked go from false to true', () => { 37 | store.getState().getIn(['dog', 'hasBarked']).should.be.false; 38 | store.dispatch(makeBark()); 39 | store.getState().getIn(['dog', 'hasBarked']).should.be.true; 40 | }); 41 | }); 42 | }); 43 | }); 44 | ``` 45 | 46 | เอาล่ะ เรามาดูกันทีละส่วนของทั้งหมดนี้ 47 | 48 | แรกสุด ให้ดูว่าเราทำการ import `should` assertion style มาจาก `chai` ซึ่งทำให้เราสามารถตรวจสอบค่าได้โดยใช้ syntax แบบ `mynumber.should.equal(3)` ซึ่งอ่าน-เขียนเข้าใจได้ง่าย เพื่อที่จะเรียกใช้ `should` กับ object อะไรก็ได้ เราจึงต้องเรียก function `should()` ก่อนที่จะทำทุกๆ อย่าง ซึ่งการทำ assertion บางอันนั้นจะเขียนเป็น *expressions* เช่น `mybook.should.be.true` ได้เหมือนกัน ทำให้บางที ESLint อาจจะเข้าใจผิดว่าเราเขียนผิดกฎ จึงต้องมีการ comment ที่ด้านบนสุดของโค้ดเพื่อปิดการใช้กฎ `no-unused-expressions` สำหรับไฟล์นี้ 49 | 50 | การทำเทสด้วย Mocha จะทำงานคล้ายๆ กับ tree อย่างในเคสนี้ เราจะเขียนเทส function `makeBark` ที่ควรจะมีผลต่อ attribute `dog` ใน application state ของเรา ดังนั้น จึงมีความ make sense ที่เราจะเขียน test โดยเช็คตามนี้ `App State > Dog > makeBark` โดยเราจะประกาศการเทส โดยใช้คำสั่ง `descrive()` และ `it()` ซึ่งจะเป็นส่วนที่จะเกิดการเทสจริงๆ ส่วนฟังก์ชัน `beforeEach()` นั้น จะเป็นฟังก์ชันที่ถูกเรียกก่อนที่จะเทสผ่านฟังก์ชัน `it()` ในกรณีนี้เราต้องการ store อันใหม่เอี่ยมก่อนจะทำการรันเทสในแต่ละอัน เราจึงประกาศตัวแปร `store` ขึ้นมาอยู่ด้านบนสุด (หลัง `should()`) เพราะเราต้องใช้ store ในทุกการเทสของไฟล์นี้ 51 | 52 | การเทสฟังก์ชัน `makeBark` ของเราจะชัดเจนที่สุด และเราสามารถอธิบายได้ว่าเทสเคสนี้ต้องการทดสอบอะไรโดยใส่เป็น string ภายในฟังก์ชัน `it()` เพื่อบอกว่าเทสเคสนี้ ตัวแปร `hasBarked` จะต้องเปลี่ยนจาก `false` เป็น `true` หลังจากมีการเรียก `makeBark` 53 | 54 | เอาหล่ะ เมื่อเราเขียนเทสเสร็จแล้ว เรามารันเทสกันเถอะ 55 | 56 | - ใน `gulpfile.babel.js` สร้าง task `test` ดังต่อไปนี้ โดยมีการใช้ plugin `gulp-mocha` ด้วย 57 | 58 | ```javascript 59 | import mocha from 'gulp-mocha'; 60 | 61 | const paths = { 62 | // [...] 63 | allLibTests: 'lib/test/**/*.js', 64 | }; 65 | 66 | // [...] 67 | 68 | gulp.task('test', ['build'], () => 69 | gulp.src(paths.allLibTests) 70 | .pipe(mocha()) 71 | ); 72 | ``` 73 | 74 | - สั่ง `yarn add --dev gulp-mocha` 75 | 76 | โดยทั้งนี้ไฟล์ test ที่เราจะนำมารันนั่นจะต้องถูก compile เสร็จแล้วอยู่ในโฟลเดอร์ `lib` นี่คือเหตุผลว่าทำไม task `build` จึงต้องถูกทำก่อนเมื่อสั่งงาน task `test` โดย task `build` เองก็ต้องสั่งงาน task `lint` ก่อนเช่นกัน และท้ายที่สุด เราจะให้ task `test` ถูกทำงานก่อน task `main` ด้วย ซึ่งทำให้ภาพรวมสุดท้ายสำหรับ `default` task จะเป็นดังนี้ 77 | 78 | `lint` > `build` > `test` > `main` 79 | 80 | นั่นคือก่อนที่จะสั่งรันแอพเราได้ จะต้องผ่านการ linting ก่อน แล้วทำการ compile โค้ด ES6 เป็น ES5 จากนั้นก็จะทำการรัน test เมื่อผ่านแล้วเราจะสามารถสั่งรันแอพได้ 81 | 82 | - เปลี่ยน prerequisite task (task ที่จะถูกทำก่อน) ของ `main` ให้เป็น `test` ดังนี้ 83 | 84 | ```javascript 85 | gulp.task('main', ['test'], () => /* ... */ ); 86 | ``` 87 | 88 | - ในไฟล์ `package.json` แทน script `"test"` เดิมด้วย `"test": "gulp test"` เมื่อทำอย่างนี้แล้ว เราสามารถสั่ง `yarn test` เพื่อทำการรัน test ที่เราเขียนไว้ได้ นอกจากนี้ `test` ยังเป็น standard script ที่จะถูกรันโดยอัตโนมัติ เมื่อใช้เครื่องมืออย่างเช่น CI (Continuous Integration) ดังนั้นทำให้เราควรผูก script การ test ไว้กับ script `test` ตลอด เพราะเวลาเราสั่ง `yarn start` เองก็จะมีการรัน test ก่อนที่จะ build โดยใช้ Webpack เหมือนกัน ดังนั้นมันจะถูก build ก็ต่อเมื่อ test ผ่านแล้วเท่านั้น 89 | 90 | - สั่ง `yarn test` หรือ `yarn start` จะเห็นผลของการ test (คาดหวังว่าจะเจอแต่สีเขียวคือผ่านทั้งหมดนะ) 91 | 92 | ## Sinon 93 | 94 | ในบางกรณี เราต้องการที่จะ *fake* บางสิ่งอย่างใน unit test ตัวอย่างเช่น สมมติเรามีฟังก์ชัน `deleteEverything` ที่จะเรียกใช้ฟังก์ชัน `deleteDatabases()` ซึ่งการสั่ง `deleteDatabases()` จะก่อให้เกิด side-effect จำนวนมาก (เช่น เผลอไปลบ Database จริงๆ ที่มี) ซึ่งเป็นสิ่งเราไม่ต้องการให้เกิดขึ้นตอนที่เราทำเทสแน่นอน 95 | 96 | [Sinon](http://sinonjs.org/) เป็น testing library ที่มีสิ่งที่เรียกว่า **Stubs** (และอื่นๆ อีกมากมาย) ที่จะช่วยให้ฟังก์ชัน `deleteDatabases` นั้นถูก "neutralize" (ลบล้างการทำงาน) ไปก่อน และ monitor ขึ้นมาดูการทำงานของมันแทน ทำให้เราสามารถเทสสิ่งที่จะถูกเรียกมา รวมถึงพารามิเตอร์ที่ใช้ได้ด้วย ทำให้ Sinon นั้นมีประโยชน์กับการ fake หรือ หลีกเลี่ยงการทำ AJAX calls ตรงๆ ซึ่งอาจก่อให้เกิด side-effects กับ back-end ได้ 97 | 98 | เพื่อให้เห็นภาพมากขึ้น ในแอพของเรา เราจะเพิ่มฟังก์ชัน `barkInConsole` เข้าไปในคลาส `Dog` ของเราที่อยู่ใน `src/shared/dog.js` โดยมีโค้ดตามนี้ 99 | 100 | ```javascript 101 | class Dog { 102 | constructor(name) { 103 | this.name = name; 104 | } 105 | 106 | bark() { 107 | return `Wah wah, I am ${this.name}`; 108 | } 109 | 110 | barkInConsole() { 111 | /* eslint-disable no-console */ 112 | console.log(this.bark()); 113 | /* eslint-enable no-console */ 114 | } 115 | } 116 | 117 | export default Dog; 118 | ``` 119 | 120 | เมื่อเรารัน `barkInConsole` ใน unit test `console.log()` จะทำการแสดงผลข้อความขึ้นมาใน terminal ซึ่งเราจะถือว่าการทำแบบนี้จะก่อให้เกิด side-effect ที่เราไม่ต้องการในแง่มุมของการทำ unit test เราสนใจว่า `console.log()` *จะถูกเรียกใช้จริงๆ แน่นอน* หลังจากสั่ง `barkInConsole` และเรายัง้องการเทสว่าพารามิเตอร์ใดจะเป็นคนที่ *โดนใส่มาตอนที่มีการเรียกใช้ฟังก์ชันดังกล่าว* 121 | 122 | ทำให้ในเคสนี้ เราต้อง expect ว่า (1) `console.log()` จะถูกเรียก และ (2) พารามิเตอร์ที่อยู่ใน `console.log()` จะต้องตรงกับที่ `this.bark()` return ออกมาให้ ซึ่งทำได้ดังนี้ 123 | 124 | - สร้างไฟล์ใหม่ `src/test/shared/dog-test.js` ขึ้นมา และเขียนโค้ดตามนี้ 125 | 126 | ```javascript 127 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 128 | 129 | import chai from 'chai'; 130 | import { stub } from 'sinon'; 131 | import sinonChai from 'sinon-chai'; 132 | import { describe, it } from 'mocha'; 133 | import Dog from '../../shared/dog'; 134 | 135 | chai.should(); 136 | chai.use(sinonChai); 137 | 138 | describe('Shared', () => { 139 | describe('Dog', () => { 140 | describe('barkInConsole', () => { 141 | it('should print a bark string with its name', () => { 142 | stub(console, 'log'); 143 | new Dog('Test Toby').barkInConsole(); 144 | console.log.should.have.been.calledWith('Wah wah, I am Test Toby'); 145 | console.log.restore(); 146 | }); 147 | }); 148 | }); 149 | }); 150 | ``` 151 | 152 | ในจุดนี้เรามีการใช้ *stubs* ของ Sinon และใช้ plugin ของ Chai ในการทำ assertion กับ Sinon stubs และอื่นๆ ด้วย 153 | 154 | - สั่งรัน `yarn add --dev sinon sinon-chai` เพื่อทำการติดตั้ง package ดังกล่าว 155 | 156 | โอเค ทีนี้มาดูก่อนว่ามีอะไรแปลกใหม่บ้าง แรกสุดเลย เรามีการใช้คำสั่ง `chai.use(sinonChai)` เพื่อเรียกใช้งาน plugin ของ Chai ขึ้นมา ถัดจากนั้น ความวิเศษของ Sinon ก็เริ่มโผล่มาใน `it()` เช่น `stub(console, 'log')` นั้นจะทำการ "neutralize" (ลบล้างการทำงานจริงๆ) `console.log` ไปก่อน และทำการ monitor มัน เมื่อคำสั่ง `new Dog('Test Toby').barkInConsole()` ถูกเรียก คำสั่ง `console.log` นั้นจะถูกรันขึ้นมาเป็นเรื่องปกติ เราเทสการเรียกใช้งาน `console.log` ด้วย `console.log.should.have.been.calledWith()` (ถูกเรียกจริงหรือไม่) และท้ายสุด เราทำการ `restore` การถูก neutralized ของ `console.log` ออกไป และให้มันกลับมาทำงานเหมือนเดิมตามปกติ 157 | 158 | **สำคัญต้องอ่าน**: การทำ Stub กับ `console.log` ไม่เป็นที่แนะนำ เพราะหาก test fail ขึ้นมา `console.log.restore()` จะไม่ถูกเรียกเลย ทำให้ `console.log` จะพังไปตลอด จนเราจะไม่เห็น error message รวมถึงการแสดงผลธรรมดาใดๆ ได้อีกเลย ดังนั้นผม(ผู้เขียน) จึงขอให้เป็นการแนะนำขั้นต้นไว้ก่อนว่าอาจจะเกิดเหตุการณ์นี้ขึ้นได้ ซึ่งท่านอาจจะงงๆ ว่าทำไมถึงยกตัวอย่างนี้มา แต่ตัวอย่างนี้เป็นตัวอย่างที่ดีที่สุดแล้วสำหรับแอพที่เรียบง่ายที่เราใช้ 159 | 160 | ถ้าทุกอย่างเป็นไปตามที่ปกติในบทนี้ คุณจะได้ 2 passing test ในขั้นสุดท้าย 161 | 162 | บทถัดไป [บทที่ 12 - เช็ค Data Type ด้วย Flow](/tutorial/12-flow) 163 | 164 | กลับไปยัง[บทที่แล้ว](/tutorial/10-immutable-redux-improvements) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 165 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import mocha from 'gulp-mocha'; 7 | import del from 'del'; 8 | import webpack from 'webpack-stream'; 9 | import webpackConfig from './webpack.config.babel'; 10 | 11 | const paths = { 12 | allSrcJs: 'src/**/*.js?(x)', 13 | serverSrcJs: 'src/server/**/*.js?(x)', 14 | sharedSrcJs: 'src/shared/**/*.js?(x)', 15 | allLibTests: 'lib/test/**/*.js', 16 | clientEntryPoint: 'src/client/app.jsx', 17 | clientBundle: 'dist/client-bundle.js?(.map)', 18 | gulpFile: 'gulpfile.babel.js', 19 | webpackFile: 'webpack.config.babel.js', 20 | libDir: 'lib', 21 | distDir: 'dist', 22 | }; 23 | 24 | gulp.task('lint', () => 25 | gulp.src([ 26 | paths.allSrcJs, 27 | paths.gulpFile, 28 | paths.webpackFile, 29 | ]) 30 | .pipe(eslint()) 31 | .pipe(eslint.format()) 32 | .pipe(eslint.failAfterError()) 33 | ); 34 | 35 | gulp.task('clean', () => del([ 36 | paths.libDir, 37 | paths.clientBundle, 38 | ])); 39 | 40 | gulp.task('build', ['lint', 'clean'], () => 41 | gulp.src(paths.allSrcJs) 42 | .pipe(babel()) 43 | .pipe(gulp.dest(paths.libDir)) 44 | ); 45 | 46 | gulp.task('test', ['build'], () => 47 | gulp.src(paths.allLibTests) 48 | .pipe(mocha()) 49 | ); 50 | 51 | gulp.task('main', ['test'], () => 52 | gulp.src(paths.clientEntryPoint) 53 | .pipe(webpack(webpackConfig)) 54 | .pipe(gulp.dest(paths.distDir)) 55 | ); 56 | 57 | gulp.task('watch', () => { 58 | gulp.watch(paths.allSrcJs, ['main']); 59 | }); 60 | 61 | gulp.task('default', ['watch', 'main']); 62 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "gulp test", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | }, 18 | "rules": { 19 | "new-cap": [ 20 | 2, 21 | { 22 | "capIsNewExceptions": [ 23 | "Map", 24 | "List" 25 | ] 26 | } 27 | ] 28 | } 29 | }, 30 | "babel": { 31 | "presets": [ 32 | "latest", 33 | "react" 34 | ], 35 | "compact": false 36 | }, 37 | "dependencies": { 38 | "babel-polyfill": "^6.16.0", 39 | "immutable": "^3.8.1", 40 | "react": "^15.3.2", 41 | "react-dom": "^15.3.2", 42 | "react-redux": "^4.4.5", 43 | "redux": "^3.6.0", 44 | "redux-actions": "^0.12.0", 45 | "redux-immutable": "^3.0.8" 46 | }, 47 | "devDependencies": { 48 | "babel-loader": "^6.2.5", 49 | "babel-preset-latest": "^6.16.0", 50 | "babel-preset-react": "^6.16.0", 51 | "chai": "^3.5.0", 52 | "del": "^2.2.2", 53 | "eslint": "^3.8.1", 54 | "eslint-config-airbnb": "^12.0.0", 55 | "eslint-plugin-import": "^2.0.1", 56 | "eslint-plugin-jsx-a11y": "^2.2.3", 57 | "eslint-plugin-react": "^6.4.1", 58 | "gulp": "^3.9.1", 59 | "gulp-babel": "^6.1.2", 60 | "gulp-eslint": "^3.0.1", 61 | "gulp-mocha": "^3.0.1", 62 | "mocha": "^3.1.2", 63 | "sinon": "^1.17.6", 64 | "sinon-chai": "^2.8.0", 65 | "webpack-stream": "^3.2.0" 66 | }, 67 | "repository": "verekia/js-stack-from-scratch", 68 | "author": "Jonathan Verrecchia - @verekia", 69 | "license": "MIT" 70 | } 71 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const MAKE_BARK = 'MAKE_BARK'; 4 | export const makeBark = createAction(MAKE_BARK, () => true); 5 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createStore } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import { combineReducers } from 'redux-immutable'; 8 | import dogReducer from './reducers/dog-reducer'; 9 | import BarkMessage from './containers/bark-message'; 10 | import BarkButton from './containers/bark-button'; 11 | 12 | const store = createStore(combineReducers({ 13 | dog: dogReducer, 14 | })); 15 | 16 | ReactDOM.render( 17 | 18 |
19 | 20 | 21 |
22 |
23 | , document.querySelector('.app') 24 | ); 25 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Button = ({ action, actionLabel }) => ; 4 | 5 | Button.propTypes = { 6 | action: PropTypes.func.isRequired, 7 | actionLabel: PropTypes.string.isRequired, 8 | }; 9 | 10 | export default Button; 11 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Message = ({ message }) =>
{message}
; 4 | 5 | Message.propTypes = { 6 | message: PropTypes.string.isRequired, 7 | }; 8 | 9 | export default Message; 10 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Button from '../components/button'; 3 | import { makeBark } from '../actions/dog-actions'; 4 | 5 | const mapDispatchToProps = dispatch => ({ 6 | action: () => { dispatch(makeBark()); }, 7 | actionLabel: 'Bark', 8 | }); 9 | 10 | export default connect(null, mapDispatchToProps)(Button); 11 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Message from '../components/message'; 3 | 4 | const mapStateToProps = state => ({ 5 | message: state.getIn(['dog', 'hasBarked']) ? 'The dog barked' : 'The dog did not bark', 6 | }); 7 | 8 | export default connect(mapStateToProps)(Message); 9 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { MAKE_BARK } from '../actions/dog-actions'; 3 | 4 | const initialState = Immutable.Map({ 5 | hasBarked: false, 6 | }); 7 | 8 | const dogReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case MAKE_BARK: 11 | return state.set('hasBarked', action.payload); 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default dogReducer; 18 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | 11 | barkInConsole() { 12 | /* eslint-disable no-console */ 13 | console.log(this.bark()); 14 | /* eslint-enable no-console */ 15 | } 16 | } 17 | 18 | export default Dog; 19 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/test/client/state-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */ 2 | 3 | import { createStore } from 'redux'; 4 | import { combineReducers } from 'redux-immutable'; 5 | import { should } from 'chai'; 6 | import { describe, it, beforeEach } from 'mocha'; 7 | import dogReducer from '../../client/reducers/dog-reducer'; 8 | import { makeBark } from '../../client/actions/dog-actions'; 9 | 10 | should(); 11 | let store; 12 | 13 | describe('App State', () => { 14 | describe('Dog', () => { 15 | beforeEach(() => { 16 | store = createStore(combineReducers({ 17 | dog: dogReducer, 18 | })); 19 | }); 20 | describe('makeBark', () => { 21 | it('should make hasBarked go from false to true', () => { 22 | store.getState().getIn(['dog', 'hasBarked']).should.be.false; 23 | store.dispatch(makeBark()); 24 | store.getState().getIn(['dog', 'hasBarked']).should.be.true; 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/src/test/shared/dog-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 2 | 3 | import chai from 'chai'; 4 | import { stub } from 'sinon'; 5 | import sinonChai from 'sinon-chai'; 6 | import { describe, it } from 'mocha'; 7 | import Dog from '../../shared/dog'; 8 | 9 | chai.should(); 10 | chai.use(sinonChai); 11 | 12 | describe('Shared', () => { 13 | describe('Dog', () => { 14 | describe('barkInConsole', () => { 15 | it('should print a bark string with its name', () => { 16 | stub(console, 'log'); 17 | new Dog('Test Toby').barkInConsole(); 18 | console.log.should.have.been.calledWith('Wah wah, I am Test Toby'); 19 | console.log.restore(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tutorial/11-testing-mocha-chai-sinon/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/12-flow/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | .*/node_modules/gulp-flowtype/.* 4 | -------------------------------------------------------------------------------- /tutorial/12-flow/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/12-flow/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 12 - เช็ค Data Type ด้วย Flow 2 | 3 | [Flow](https://flowtype.org/) เป็นตัวทำ static type checker ที่จะทำการตรวจสอบหาประเภทของ data types ที่ไม่สอดคล้องกันในโค้ดได้ ซึ่งทำให้คุณสามารถเพิ่มการประกาศประเภทของข้อมูลเพิ่มเติมได้ด้วย ผ่าน annotations พิเศษของ Flow 4 | 5 | - ในการที่จะให้ Babel เข้าใจและลบ annotation ของ Flow ทิ้งตอนที่ทำการ compile นั้น ให้ทำการติดตั้ง Flow present ของ Babel โดยสั่ง `yarn add --dev babel-preset-flow` หลังจากนั้นให้ทำการเพิ่ม `"flow"` ลงไปภายใน `babel.presets` ในไฟล์ `package.json` 6 | 7 | - สร้างไฟล์เปล่า `.flowconfig` ใน root ของโปรเจค 8 | 9 | - สั่ง `yarn add --dev gulp-flowtype` เพื่อติดตั้ง Gulp plugin สำหรับใช้งาน Flow และเพิ่ม `flow()` เข้าไปใน task `lint` 10 | 11 | ```javascript 12 | import flow from 'gulp-flowtype'; 13 | 14 | // [...] 15 | 16 | gulp.task('lint', () => 17 | gulp.src([ 18 | paths.allSrcJs, 19 | paths.gulpFile, 20 | paths.webpackFile, 21 | ]) 22 | .pipe(eslint()) 23 | .pipe(eslint.format()) 24 | .pipe(eslint.failAfterError()) 25 | .pipe(flow({ abort: true })) // Add Flow here 26 | ); 27 | ``` 28 | 29 | option `abort` มีไว้เพื่อบอกว่าให้หยุดการทำงานของ Gulp task เมื่อ Flow ตรวจเจอ issue ขึ้นมา 30 | 31 | เมื่อทำทั้งหมดข้างต้นเสร็จแล้ว ณ ตอนนี้ก็ควรสามารถใช้งาน Flow ได้แล้ว 32 | 33 | - เพิ่ม Flow annotation ใน `src/shared/dog.js` ตามโค้ดด้านล่างนี้ 34 | 35 | ```javascript 36 | // @flow 37 | 38 | class Dog { 39 | name: string; 40 | 41 | constructor(name: string) { 42 | this.name = name; 43 | } 44 | 45 | bark(): string { 46 | return `Wah wah, I am ${this.name}`; 47 | } 48 | 49 | barkInConsole() { 50 | /* eslint-disable no-console */ 51 | console.log(this.bark()); 52 | /* eslint-enable no-console */ 53 | } 54 | 55 | } 56 | 57 | export default Dog; 58 | ``` 59 | 60 | comment `// @flow` ใช้เพื่อบอก Flow ว่าเราต้องการให้ไฟล์นี้ทั้งไฟล์ถูกตรวจสอบ data type โดย Flow annotation จะมีลักษณะเป็น colon (:) ตามหลังพารามิเตอร์ของฟังก์ชัน หรือด้านหลังชื่อฟังก์ชัน ซึ่งคุณสามารถดู documentation ของ Flow ได้สำหรับรายละเอียดเพิ่มเติม 61 | 62 | ดังนั้น เมื่อคุณรัน `yarn start` ไป Flow ก็จะทำงานได้ตามปกติ แต่ ESLint จะบ่นเราเรื่องที่มี syntax ที่ผิดมาตรฐานถูกใช้อยู่ แต่บังเอิญเป็นเรื่องดีที่ว่า plugin `babel-preset-flow` นั้นช่วยให้ตัว parser ของ Babel เข้าใจว่าจุดที่แปลกๆ นั้นคือ Flow content นั่นเอง ซึ่งจะเป็นการดีมาก หาก ESLint นั้นเลือกที่จะเชื่อมั่นในตัว parser ของ Babel แทนที่จะมานั่งทำความเข้าใจ Flow annotation เอง ดังนั้นเราจึงมีการใช้ package `babel-eslint` เพื่อให้ ESLint เชื่อมั่นในตัว parser ของ Babel แทนว่าโค้ดที่เราเขียนนั้นไม่มีปัญหาเรื่อง syntax แปลกประหลาดแต่อย่างใด 63 | 64 | - สั่งรัน `yarn add --dev babel-eslint` 65 | 66 | - ในไฟล์ `package.json` ในส่วนของ `eslintConfig` เพิ่ม property `"parser": "babel-eslint"` ลงไป 67 | 68 | เมื่อสั่ง `yarn start` ณ ตอนนี้จะพบว่าทั้งการทำ linting และ typecheck ในโค้ดของคุณนั้นทำงานได้อย่างปกติสุขไม่มีปัญหาอะไร 69 | 70 | ตอนนี้ ESLint และ Babel นั้นสามารถ share parser ด้วยกันได้แล้ว เราสามารถใช้ ESLint เพื่อทำการ lint Flow annotation ได้แล้ว ผ่าน plugin `eslint-plugin-flowtype` 71 | 72 | - สั่ง `yarn add --dev eslint-plugin-flowtype` และเพิ่ม `"flowtype"` ลงไปใน `eslintConfig.plugins` ในไฟล์ `package.json` 73 | 74 | - ทำการเพิ่ม `"plugin:flowtype/recommended"` ลงไปใน `eslintConfig.extends` ใน array ช่องถัดจาก `"airbnb"` ที่อยู่ในไฟล์ `package.json` ด้วย 75 | 76 | ตอนนี้ ถ้าคุณพิมพ์ `name:string` เป็น annotation ESLint จะทำการบ่นคุณว่าลืมเติมช่องว่างหลัง colon ให้ไปเติมด้วย 77 | 78 | **หมายเหตุ**: property `"parser": "babel-eslint"` ที่ให้เพิ่มใน `package.json` นั้นจริงๆ แล้วถูกรวมอยู่ใน config ของ `"plugin:flowtype/recommended"` อยู่แล้ว ดังนั้นคุณสามารถลบมันทิ้งได้ เพื่อลดขนาดของ `package.json` ลงได้ แต่คุณสามารถปล่อยให้เป็นแบบนั้นไว้ได้ เพื่อให้เกิดความชัดเจนเวลามีคนอื่นสงสัยว่า parser หายไปไหน ทำไมไม่มี ดังนั้น เรื่องนี้จึงเป็นเรื่องส่วนบุคคล ว่าอยากจะเก็บไว้หรือไม่ แต่เนื่องจาก tutorial นี้จะเน้นให้ minimal มากที่สุด ดังนั้นผม(ผู้เขียน) จึงขอลบออก 79 | 80 | - ตอนนี้คุณสามารถเพิ่ม `// @flow` ในทุกไฟล์ที่มีนามสกุล `.js` และ `.jsx` ที่อยู่ภายในโฟลเดอร์ `src` ได้แล้ว โดยสามารถสั่ง `yarn test` หรือ `yarn start` และเพิ่ม type annotation ให้กับทุกที่ที่ Flow บอกให้คุณเพิ่ม 81 | 82 | ต่อมา เรามีหนึ่งตัวอย่างที่น่าสนใจมานำเสนอ (และอาจจะดูขัดใจแปลกๆ) สำหรับไฟล์ `src/client/components/message.jsx' 83 | 84 | ```javascript 85 | const Message = ({ message }: { message: string }) =>
{message}
; 86 | ``` 87 | 88 | เราจะเห็นว่า เมื่อเราทำการ destructuring พารามิเตอร์ในฟังก์ชันแล้ว เราจะต้องทำ type annotate กับ properties ที่จะถูกแกะออกมาด้วย โดยใช้หลักการทำ object literal notation (การประกาศ Object โดยใช้ { }) ซึ่งในทีนี้เราให้ตัวแปร `message` มี type เป็น string 89 | 90 | อีกกรณีนึงที่คุณอาจจะเจอ คือในไฟล์ `src/client/reducers/dog-reducer.js` Flow จะบ่นเรื่องของ Immutable ไม่มี default export ซึ่ง issue นี้ก็ยังเป็นที่ถกเถียงกันใน [Issue#863 on Immutable](https://github.com/facebook/immutable-js/issues/863) ที่มี workarounds อยู่สองแบบ 91 | 92 | ```javascript 93 | import { Map as ImmutableMap } from 'immutable'; 94 | // หรือ 95 | import * as Immutable from 'immutable'; 96 | ``` 97 | 98 | จนกว่าเจ้าของ Immutable จะทำการประกาศแจ้งเรื่อง issue นี้อีกทีนึงในอนาคต ให้เลือกใช้อันที่คุณคิดว่าดีกว่าในการ import Immutable components มาใช้ ซึ่งส่วนตัวผม(ผู้เขียน) เลือกใช้ `import * as Immutable from 'immutable'` เพราะโค้ดสั้นกว่า และไม่ต้องทำ refactoring โค้ดเดิมเลย 99 | 100 | **หมายเหตุ**: ถ้า Flow ตรวจเจอ type errors ในโฟลเดอร์ `node_modules` ให้เพิ่ม `[ignore]` section ในไฟล์ `.flowconfig` เพื่อให้ ignore เฉพาะ package ที่ก่อให้เกิด issue ดังกล่าวเป็นกรณีพิเศษไป (อย่า ignore ทั้งโฟลเดอร์ `node_modules` นะ) โดยหน้าตาจะประมาณนี้ 101 | 102 | ```flowconfig 103 | [ignore] 104 | 105 | .*/node_modules/gulp-flowtype/.* 106 | ``` 107 | 108 | ในกรณีที่ผม(ผู้เขียน) เจอมา plugin `linter-flow` ของ Atom ถูก detect ว่ามี type errors ในโฟลเดอร์ `node_modules/gulp-flowtype` ซึ่งมีการระบุว่าใช้ `// @flow` ด้วย จึง ignore เฉพาะโฟลเดอร์ดังกล่าวไป ตามตัวอย่างที่แสดงด้านบน 109 | 110 | ณ ตอนนี้ คุณก็จะต้องเขียนโค้ดที่ผ่านการ lint, typecheck และถูกรัน test ครบถ้วนแล้วก่อนที่จะ build เยี่ยมยอดมาก! 111 | 112 | กลับไปยัง[บทที่แล้ว](/tutorial/11-testing-mocha-chai-sinon) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 113 | -------------------------------------------------------------------------------- /tutorial/12-flow/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/12-flow/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import flow from 'gulp-flowtype'; 7 | import mocha from 'gulp-mocha'; 8 | import del from 'del'; 9 | import webpack from 'webpack-stream'; 10 | import webpackConfig from './webpack.config.babel'; 11 | 12 | const paths = { 13 | allSrcJs: 'src/**/*.js?(x)', 14 | serverSrcJs: 'src/server/**/*.js?(x)', 15 | sharedSrcJs: 'src/shared/**/*.js?(x)', 16 | allLibTests: 'lib/test/**/*.js', 17 | clientEntryPoint: 'src/client/app.jsx', 18 | clientBundle: 'dist/client-bundle.js?(.map)', 19 | gulpFile: 'gulpfile.babel.js', 20 | webpackFile: 'webpack.config.babel.js', 21 | libDir: 'lib', 22 | distDir: 'dist', 23 | }; 24 | 25 | gulp.task('lint', () => 26 | gulp.src([ 27 | paths.allSrcJs, 28 | paths.gulpFile, 29 | paths.webpackFile, 30 | ]) 31 | .pipe(eslint()) 32 | .pipe(eslint.format()) 33 | .pipe(eslint.failAfterError()) 34 | .pipe(flow({ abort: true })) 35 | ); 36 | 37 | gulp.task('clean', () => del([ 38 | paths.libDir, 39 | paths.clientBundle, 40 | ])); 41 | 42 | gulp.task('build', ['lint', 'clean'], () => 43 | gulp.src(paths.allSrcJs) 44 | .pipe(babel()) 45 | .pipe(gulp.dest(paths.libDir)) 46 | ); 47 | 48 | gulp.task('test', ['build'], () => 49 | gulp.src(paths.allLibTests) 50 | .pipe(mocha()) 51 | ); 52 | 53 | gulp.task('main', ['test'], () => 54 | gulp.src(paths.clientEntryPoint) 55 | .pipe(webpack(webpackConfig)) 56 | .pipe(gulp.dest(paths.distDir)) 57 | ); 58 | 59 | gulp.task('watch', () => { 60 | gulp.watch(paths.allSrcJs, ['main']); 61 | }); 62 | 63 | gulp.task('default', ['watch', 'main']); 64 | -------------------------------------------------------------------------------- /tutorial/12-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "gulp test", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": [ 12 | "airbnb", 13 | "plugin:flowtype/recommended" 14 | ], 15 | "parser": "babel-eslint", 16 | "plugins": [ 17 | "import", 18 | "flowtype" 19 | ], 20 | "env": { 21 | "browser": true 22 | }, 23 | "rules": { 24 | "new-cap": [ 25 | 2, 26 | { 27 | "capIsNewExceptions": [ 28 | "Map", 29 | "List" 30 | ] 31 | } 32 | ] 33 | } 34 | }, 35 | "babel": { 36 | "presets": [ 37 | "latest", 38 | "react", 39 | "flow" 40 | ], 41 | "compact": false 42 | }, 43 | "dependencies": { 44 | "babel-polyfill": "^6.16.0", 45 | "immutable": "^3.8.1", 46 | "react": "^15.3.2", 47 | "react-dom": "^15.3.2", 48 | "react-redux": "^4.4.5", 49 | "redux": "^3.6.0", 50 | "redux-actions": "^0.12.0", 51 | "redux-immutable": "^3.0.8" 52 | }, 53 | "devDependencies": { 54 | "babel-eslint": "^7.0.0", 55 | "babel-loader": "^6.2.5", 56 | "babel-preset-flow": "^1.0.0", 57 | "babel-preset-latest": "^6.16.0", 58 | "babel-preset-react": "^6.16.0", 59 | "chai": "^3.5.0", 60 | "del": "^2.2.2", 61 | "eslint": "^3.8.1", 62 | "eslint-config-airbnb": "^12.0.0", 63 | "eslint-plugin-flowtype": "^2.21.0", 64 | "eslint-plugin-import": "^2.0.1", 65 | "eslint-plugin-jsx-a11y": "^2.2.3", 66 | "eslint-plugin-react": "^6.4.1", 67 | "gulp": "^3.9.1", 68 | "gulp-babel": "^6.1.2", 69 | "gulp-eslint": "^3.0.1", 70 | "gulp-flowtype": "^1.0.0", 71 | "gulp-mocha": "^3.0.1", 72 | "mocha": "^3.1.2", 73 | "sinon": "^1.17.6", 74 | "sinon-chai": "^2.8.0", 75 | "webpack-stream": "^3.2.0" 76 | }, 77 | "repository": "verekia/js-stack-from-scratch", 78 | "author": "Jonathan Verrecchia - @verekia", 79 | "license": "MIT" 80 | } 81 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createAction } from 'redux-actions'; 4 | 5 | export const MAKE_BARK = 'MAKE_BARK'; 6 | export const makeBark = createAction(MAKE_BARK, () => true); 7 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import 'babel-polyfill'; 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { createStore } from 'redux'; 8 | import { Provider } from 'react-redux'; 9 | import { combineReducers } from 'redux-immutable'; 10 | import dogReducer from './reducers/dog-reducer'; 11 | import BarkMessage from './containers/bark-message'; 12 | import BarkButton from './containers/bark-button'; 13 | 14 | const store = createStore(combineReducers({ 15 | dog: dogReducer, 16 | })); 17 | 18 | ReactDOM.render( 19 | 20 |
21 | 22 | 23 |
24 |
25 | , document.querySelector('.app') 26 | ); 27 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { PropTypes } from 'react'; 4 | 5 | const Button = ({ action, actionLabel }: { action: Function, actionLabel: string}) => 6 | ; 7 | 8 | Button.propTypes = { 9 | action: PropTypes.func.isRequired, 10 | actionLabel: PropTypes.string.isRequired, 11 | }; 12 | 13 | export default Button; 14 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { PropTypes } from 'react'; 4 | 5 | const Message = ({ message }: { message: string }) =>
{message}
; 6 | 7 | Message.propTypes = { 8 | message: PropTypes.string.isRequired, 9 | }; 10 | 11 | export default Message; 12 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import Button from '../components/button'; 5 | import { makeBark } from '../actions/dog-actions'; 6 | 7 | const mapDispatchToProps = dispatch => ({ 8 | action: () => { dispatch(makeBark()); }, 9 | actionLabel: 'Bark', 10 | }); 11 | 12 | export default connect(null, mapDispatchToProps)(Button); 13 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import Message from '../components/message'; 5 | 6 | const mapStateToProps = state => ({ 7 | message: state.getIn(['dog', 'hasBarked']) ? 'The dog barked' : 'The dog did not bark', 8 | }); 9 | 10 | export default connect(mapStateToProps)(Message); 11 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as Immutable from 'immutable'; 4 | import { MAKE_BARK } from '../actions/dog-actions'; 5 | 6 | const initialState = Immutable.Map({ 7 | hasBarked: false, 8 | }); 9 | 10 | const dogReducer = (state: Object = initialState, action: Object) => { 11 | switch (action.type) { 12 | case MAKE_BARK: 13 | return state.set('hasBarked', action.payload); 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default dogReducer; 20 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/server/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable no-console */ 4 | 5 | import Dog from '../shared/dog'; 6 | 7 | const toby = new Dog('Toby'); 8 | 9 | console.log(toby.bark()); 10 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | class Dog { 4 | name: string; 5 | 6 | constructor(name: string) { 7 | this.name = name; 8 | } 9 | 10 | bark(): string { 11 | return `Wah wah, I am ${this.name}`; 12 | } 13 | 14 | barkInConsole() { 15 | /* eslint-disable no-console */ 16 | console.log(this.bark()); 17 | /* eslint-enable no-console */ 18 | } 19 | 20 | } 21 | 22 | export default Dog; 23 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/test/client/state-test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */ 4 | 5 | import { createStore } from 'redux'; 6 | import { combineReducers } from 'redux-immutable'; 7 | import { should } from 'chai'; 8 | import { describe, it, beforeEach } from 'mocha'; 9 | import dogReducer from '../../client/reducers/dog-reducer'; 10 | import { makeBark } from '../../client/actions/dog-actions'; 11 | 12 | should(); 13 | let store; 14 | 15 | describe('App State', () => { 16 | describe('Dog', () => { 17 | beforeEach(() => { 18 | store = createStore(combineReducers({ 19 | dog: dogReducer, 20 | })); 21 | }); 22 | describe('makeBark', () => { 23 | it('should make hasBarked go from false to true', () => { 24 | store.getState().getIn(['dog', 'hasBarked']).should.be.false; 25 | store.dispatch(makeBark()); 26 | store.getState().getIn(['dog', 'hasBarked']).should.be.true; 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tutorial/12-flow/src/test/shared/dog-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 2 | 3 | import chai from 'chai'; 4 | import { stub } from 'sinon'; 5 | import sinonChai from 'sinon-chai'; 6 | import { describe, it } from 'mocha'; 7 | import Dog from '../../shared/dog'; 8 | 9 | chai.should(); 10 | chai.use(sinonChai); 11 | 12 | describe('Shared', () => { 13 | describe('Dog', () => { 14 | describe('barkInConsole', () => { 15 | it('should print a bark string with its name', () => { 16 | stub(console, 'log'); 17 | new Dog('Test Toby').barkInConsole(); 18 | console.log.should.have.been.calledWith('Wah wah, I am Test Toby'); 19 | console.log.restore(); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tutorial/12-flow/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/2-packages/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | -------------------------------------------------------------------------------- /tutorial/2-packages/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 2 - ติดตั้งและใช้งาน package 2 | 3 | บทนี้เราจะสอนวิธีการติดตั้งและใช้งาน package กัน package คือโค้ดที่มีคนอื่นเคยเขียนไว้แล้ว และเราสามารถนำมาใช้ได้ ซึ่ง package นั้นสามารถเป็นอะไรก็ได้ ในตัวอย่างนี้เราจะลองใช้ package ที่ช่วยให้เราจัดการเรื่องสีได้ดีขึ้น ซึ่ง package ที่ว่านั้นเราไปเอาจากของคนอื่นเขามาใช้ ไม่ได้เขียนเอง 4 | 5 | - ติดตั้ง package ชื่อ `color` (เป็น community-made package) โดยสั่ง `yarn add color` 6 | 7 | เปิดไฟล์ `package.json` จะเห็นว่า Yarn ได้ทำการเพิ่ม `color` เข้าไปใน `dependencies` แล้วเรียบร้อย 8 | 9 | นอกจากนี้ โฟลเดอร์ `node_modules` ก็ยังถูกสร้างขึ้นมาเพื่อเก็บ package ไว้อีกด้วย 10 | 11 | - เพิ่ม `node_modules/` เข้าไปใน `.gitignore` (และสั่ง `git init` ด้วย เพื่อสร้าง repo ถ้ายังไม่ได้ทำ) 12 | 13 | ถ้าสังเกต คุณจะเห็นว่ามีไฟล์ `yarn.lock` ที่ Yarn generate ขึ้นมาด้วย ซึ่งไฟล์นี้นั้นก็ควรจะต้องถูก commit ด้วย เพื่อรับประกันว่าทุกคนในทีมจะใช้ package ที่มีเวอร์ชันตรงกันทุกคน แต่ถ้าหากคุณใช้ NPM แทน Yarn นั้น NPM จะมีไฟล์ที่ทำหน้าที่เดียวกัน เรียกว่า *shrinkwrap* 14 | 15 | - เพิ่ม `const Color = require('color');` ไปใน `index.js` 16 | - ใช้งาน package ดังกล่าวได้ เช่น `const redHexa = Color({r: 255, g: 0, b: 0}).hexString();` 17 | - เพิ่ม `console.log(redHexa)` 18 | - สั่ง `yarn start` จะเห็น `#FF0000` 19 | 20 | ยินดีด้วย! คุณได้ติดตั้งและใช้งาน package แล้ว! 21 | 22 | `color` ถูกใช้ในบทนี้เพื่อให้เข้าใจวิธีการใช้ package เล็กๆ ซึ่งเราไม่ต้องการใช้อีกแล้ว เราก็ลบทิ้งได้ โดย 23 | 24 | - สั่ง `yarn remove color` 25 | 26 | **Note**: ใน dependencies นั้นจะมีอยู่สองประเภท, `"dependencies"` กับ `"devDependencies"` `"devDependencies"` นั้นจะเป็น package ที่จำเป็นเฉพาะในตอน development เท่านั้น ไม่ได้นำไปใช้ตอน production (ปกติมักจะเป็นพวก package ที่เกี่ยวข้องกับการ build ต่างๆ, linters และอื่นๆ) ซึ่งสำหรับ `"devDependencies"` เราจะใช้ `yarn add --dev [package]` ในการเพิ่ม `"devDependencies"` 27 | 28 | บทถัดไป [บทที่ 3 - ตั้งค่าเพื่อใช้งาน ES6 โดยใช้ Babel และ Gulp](/tutorial/3-es6-babel-gulp) 29 | 30 | กลับไปยัง[บทที่แล้ว](/tutorial/1-node-npm-yarn-package-json) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 31 | -------------------------------------------------------------------------------- /tutorial/2-packages/index.js: -------------------------------------------------------------------------------- 1 | const Color = require('color'); 2 | 3 | const redHexa = Color({r: 255, g: 0, b: 0}).hexString(); 4 | 5 | console.log(redHexa); 6 | -------------------------------------------------------------------------------- /tutorial/2-packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "node .", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "yarn start" 9 | }, 10 | "dependencies": { 11 | "color": "^0.11.3" 12 | }, 13 | "repository": "verekia/js-stack-from-scratch", 14 | "author": "Jonathan Verrecchia - @verekia", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /tutorial/2-packages/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | clone@^1.0.2: 4 | version "1.0.2" 5 | resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" 6 | 7 | color-convert@^1.3.0: 8 | version "1.5.0" 9 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.5.0.tgz#7a2b4efb4488df85bca6443cb038b7100fbe7de1" 10 | 11 | color-name@^1.0.0: 12 | version "1.1.1" 13 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" 14 | 15 | color-string@^0.3.0: 16 | version "0.3.0" 17 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" 18 | dependencies: 19 | color-name "^1.0.0" 20 | 21 | color@^0.11.3: 22 | version "0.11.3" 23 | resolved "https://registry.yarnpkg.com/color/-/color-0.11.3.tgz#4bad1d0d52499dd00dbd6f0868442467e49394e6" 24 | dependencies: 25 | clone "^1.0.2" 26 | color-convert "^1.3.0" 27 | color-string "^0.3.0" 28 | 29 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 3 - ตั้งค่าเพื่อใช้งาน ES6 โดยใช้ Babel และ Gulp 2 | 3 | ตอนนี้เราจะมาใช้ ES6 syntax สำหรับการเขียนภาษา JavaScript กัน ซึ่งนับว่าเป็นอะไรที่พัฒนาขึ้นมาดีขึ้นมากเทียบกับ ES5 syntax "แบบเดิมๆ" ซึ่ง browsers ปัจจุบัน รวมถึงเหล่า JS environments ทั้งหลายที่มักใช้ๆ กันต่างเข้าใจ ES5 เป็นอย่างดี แต่ไม่เข้าใจ ES6 ดังนั้นทำให้เราต้องใช้เครื่องมือที่เรียกว่า Babel ในการแปลงจาก ES6 ไปเป็น ES5 4 | 5 | เพื่อที่จะใช้งาน Babel เราจะมีการใช้ Gulp ด้วย Gulp นั้นเป็น task runner ซึ่งจะคล้ายคลึงกับ tasks ที่อยู่ใน `scripts` ในไฟล์ `package.json` แต่ว่าเราจะเขียน task เหล่านั้นด้วยโค้ดภาษา JavaScript ซึ่งแน่นอนว่ามันก็ง่ายกว่า และชัดเจนกว่าเขียนเป็น JSON เยอะ ดังนั้น เราจะติดตั้ง Gulp และ Babel plugin สำหรับ Gulp ด้วย โดยทำตามนี้ 6 | 7 | - สั่ง `yarn add --dev gulp` 8 | - สั่ง `yarn add --dev gulp-babel` 9 | - สั่ง `yarn add --dev babel-preset-latest` 10 | - สั่ง `yarn add --dev del` (ใช้สำหรับ `clean` task เดี๋ยวจะมีในตัวอย่างด้านล่าง) 11 | - ในไฟล์ `package.json` เพิ่ม field `babel` ไว้เป็นค่า config ให้กับ Babel เพื่อให้ Babel นั้นใช้ presets ตัวล่าสุด แบบนี้ 12 | 13 | ```json 14 | "babel": { 15 | "presets": [ 16 | "latest" 17 | ] 18 | }, 19 | ``` 20 | 21 | **หมายเหตุ**: ไฟล์ `.babelrc` ที่อยู่ที่โปรเจคของเราก็สามารถใช้งานแทน field `babel` ที่อยู่ในไฟล์ `package.json` ได้เหมือนกัน ซึ่งในความเป็นจริง โปรเจคของเราก็จะมีขนาดใหญ่ขึ้นมากเรื่องๆ ดังนั้นการเอา config ของ Babel ไว้ใน `package.json` ก็เป็นสิ่งที่ดี เว้นแต่ว่า Babel config จะเริ่มใหญ่ขึ้นเรื่อยๆ จึงค่อยแยกออกมาอีกไฟล์ 22 | 23 | - ย้ายไฟล์ `index.js` ไปไว้ในโฟลเดอร์ชื่อว่า `src` (สร้างโฟลเดอร์ใหม่ หากยังไม่มี) ในโฟลเดอร์นี้เราจะเขียนโค้ดภาษา JavaScript แบบ ES6 กัน โดยเมื่อทำการ compile โค้ด ES6 เสร็จแล้ว โค้ด ES5 ที่ได้จะอยู่ในโฟลเดอร์ `lib` โดยที่ Gulp กับ Babel จะเป็นคนจัดการในการ compile และสร้างโฟลเดอร์ `lib` ขึ้นมาเอง แล้วให้ทำการลบโค้ดที่เกี่ยวกับ `color` ที่หลงเหลือจากตอนที่แล้ว และเขียนโค้ดด้านล่างแทน 24 | 25 | ```javascript 26 | const str = 'ES6'; 27 | console.log(`Hello ${str}`); 28 | ``` 29 | 30 | โค้ดนี้เรามีการใช้สิ่งที่เรียกว่า *template string* ซึ่งเป็นฟีเจอร์ใหม่ใน ES6 ที่เราสามารถแทรกตัวแปรเข้าไปใน string ได้โดยตรง ไม่จำเป็นต้องทำการต่อ string แบบเดิมๆ อีกต่อไป โดยใช้ `${}` 31 | 32 | สังเกตดูจะเห็นว่า template strings จะถูกเขียนโดยใช้สัญลักษณ์ **backquotes** (` ) 33 | 34 | - ต่อมาให้เราทำการสร้างไฟล์ `gulpfile.js` ในไฟล์ให้เขียนโค้ดดังต่อไปนี้ 35 | 36 | ```javascript 37 | const gulp = require('gulp'); 38 | const babel = require('gulp-babel'); 39 | const del = require('del'); 40 | const exec = require('child_process').exec; 41 | 42 | const paths = { 43 | allSrcJs: 'src/**/*.js', 44 | libDir: 'lib', 45 | }; 46 | 47 | gulp.task('clean', () => { 48 | return del(paths.libDir); 49 | }); 50 | 51 | gulp.task('build', ['clean'], () => { 52 | return gulp.src(paths.allSrcJs) 53 | .pipe(babel()) 54 | .pipe(gulp.dest(paths.libDir)); 55 | }); 56 | 57 | gulp.task('main', ['build'], (callback) => { 58 | exec(`node ${paths.libDir}`, (error, stdout) => { 59 | console.log(stdout); 60 | return callback(error); 61 | }); 62 | }); 63 | 64 | gulp.task('watch', () => { 65 | gulp.watch(paths.allSrcJs, ['main']); 66 | }); 67 | 68 | gulp.task('default', ['watch', 'main']); 69 | 70 | ``` 71 | 72 | เรามาอธิบายโค้ดทั้งหมดนี้ให้เข้าใจกันก่อน 73 | 74 | Gulp นั้นมี API ที่ค่อนข้างจะเข้าใจง่าย ใช้คำตรงไปตรงมา สิ่งที่เราทำก็คือ เรานิยาม `gulp.task` จำนวนมากที่อ้างอิงถึงไฟล์ได้ผ่าน `gulp.src` แล้ว chain คำสั่งต่อๆ กันด้วยคำสั่ง `.pipe()` (เช่น คำสั่ง `babel()` เป็นต้น) และได้ output ออกมาเป็นไฟล์ใหม่ โดยใช้ `gulp.dest` ในการอ้างอิง path ปลายทาง 75 | 76 | นอกจากนี้เรายังมี `gulp.watch` ที่จะมองการเปลี่ยนแปลงของไฟล์ที่เราสนใจ เมื่อไฟล์เกิดการเปลี่ยนแปลง เราจะสั่งงานตาม tasks ที่เราระบุไว้ใน array (เช่น `['build']`) เป็นพารามิเตอร์ตัวที่สอง นอกจากนี้ยังมี API อีกมากมายให้ใช้ ซึ่งหากสนใจสามารถดูใน [documentation](https://github.com/gulpjs/gulp) ของ Gulp ได้ 77 | 78 | ในบรรทัดต้นๆ เราประกาศ object `paths` เพื่อเก็บ path ของไฟล์ที่เราจะใช้ทั้งหมด นั่นคือ path ของ source file `allSrcJs` และ path ของปลายทางที่เราต้องการ `libDir` และการทำแบบนี้ก็ช่วยให้เรา Don't Repeat Yourself (DRY) ได้อีกด้วย 79 | 80 | หลังจากนั้น เรานิยาม tasks (งาน) มา 5 งาน: `build`, `clean`, `main`, `watch`, และ `default` 81 | 82 | - `build` จะเป็น task ที่ Babel จะถูกเรียกใช้เพื่อแปลง source file ทั้งหมดที่เราเขียนในโฟลเดอร์ `src` และบันทึกผลสุดท้ายที่แปลงสำเร็จแล้วลงโฟลเดอร์ `lib` 83 | - `clean` เป็น task ที่เราจะทำการเคลียร์ข้อมูลในโฟลเดอร์ `lib` ที่ถูกสร้างขึ้นมาเองด้วย Gulp ทั้งหมด โดยจะถูกเรียกก่อนที่จะทำ task `build` ทุกครั้ง ซึ่ง task นี้มีประโยชน์ในการลบไฟล์เก่าทิ้งไปให้หมด เพื่อให้เหมือนเป็นการที่เราจะ sync ผลลัพธ์ให้ตรงกับต้นฉบับในโฟลเดอร์ `src` อยู่ตลอด โดยเราใช้ package ชื่อว่า `del` ในการลบไฟล์ทิ้ง ซึ่งเหมาะกับการใช้คู่กันกับ Gulp's stream (ซึ่งวิธีนี้เป็นคำ[แนะนำ](https://github.com/gulpjs/gulp/blob/master/docs/recipes/delete-files-folder.md) ในการลบไฟล์ที่ดี โดยใช้ Gulp) 84 | - `main` มีค่าใกล้เคียงกับการสั่ง `node .` ในครั้งที่แล้ว แต่ครั้งนี้เราต้องการรันโค้ดที่อยู่ภายใน `lib/index.js` แทน เพราะปกติ Node จะมองหา `index.js` เป็นค่า default เราแค่สั่ง `node lib` ก็ได้ผลเหมือนเดิม (ในที่นี้เราใช้ตัวแปร `libDir` เลยเพื่อให้เรานั้นไม่ทำอะไรซ้ำซ้อนหลายๆ รอบ) ส่วนคำสั่ง `require('child_process').exec` และ `exec` ใน task นั้นเป็น function ของ Node ที่ให้เรา execute shell command ได้ เราส่งตัวแปร `stdout` ไปให้กับ `console.log()` และให้ return error ที่อาจเกิดขึ้นได้โดยใช้ callback function ของ `gulp.task` มาช่วย โอเค หากคุณยังรู้สึกงงๆ อยู่บ้าง ก็ไม่เป็นไร ขอสรุปว่า task นี้จะแค่สั่ง `node lib` ให้กับเราโดยอัตโนมัติ 85 | - `watch` จะสั่งรัน task `main` เมื่อ filesystem พบว่ามีการเปลี่ยนแปลงเกิดขึ้นกับ file ที่เราระบุไว้ในฟังก์ชัน (ในทีนี้ก็คือ หากไฟล์ในโฟลเดอร์ `src` เกิดการเปลี่ยนแปลง ให้สั่งงาน task `main` ทันที) 86 | - `default` เป็น task พิเศษที่จะทำงานเมื่อเราสั่งแค่ `gulp` เฉยๆ จาก Command Line (CLI) ในเคสนี้เราจะให้รัน task `watch` กับ `main` 87 | 88 | **หมายเหตุ**: คุณอาจจะกำลังสงสัยว่าทำไมเราเขียน ES6 ใน Gulp file ของเราได้ ทั้งๆ ที่เราไม่ได้แปลงมันเป็น ES5 ด้วย Babel เลยนี่หนา? นั่นเป็นเพราะว่าเราเลือกใช้ Node เวอร์ชันใหม่ที่รองรับการใช้ ES6 อยู่แต่แรกแล้ว ทำให้เราเขียนโค้ด ES6 บน Gulp file ได้ (ดังนั้น โปรดเช็คให้มั่นใจว่าใช้งาน Node เวอร์ชันที่ใหม่กว่า 6.5.0 เป็นต้นไปแล้ว โดยใช้คำสั่ง `node -v` ในการเช็ค) 89 | 90 | เอาล่ะ! เรามาดูกันว่า task เหล่านี้ใช้งานได้ 91 | 92 | - ใน `package.json` เปลี่ยน `start` script ให้เป็น `"start": "gulp"` 93 | - สั่ง `yarn start` เราควรจะเห็น "Hello ES6" ขึ้นมา แล้ว Gulp จะเริ่มมองการเปลี่ยนแปลงของไฟล์ ให้ลองเขียนโค้ดผิดๆ ดูในไฟล์ `src/index.js` เพื่อดูว่า Gulp จะแสดงผล error โดยอัตโนมัติ หลังจากที่คุณเซฟไฟล์ใหม่แล้ว 94 | - เพิ่มโฟลเดอร์ `/lib/` ไปใน `.gitignore` (ไม่จำเป็นที่จะต้อง commit โค้ดในโฟลเดอรนี้ลงไป เพราะอย่างไรก็ตามเราสามารถทำการ compile ได้อยู่แล้ว เพียงแค่มี source code) 95 | 96 | บทถัดไป [บทที่ 4 - การใช้ ES6 syntax ในการเขียน Class](/tutorial/4-es6-syntax-class) 97 | 98 | กลับไปยัง[บทที่แล้ว](/tutorial/2-packages) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 99 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const del = require('del'); 4 | const exec = require('child_process').exec; 5 | 6 | const paths = { 7 | allSrcJs: 'src/**/*.js', 8 | libDir: 'lib', 9 | }; 10 | 11 | gulp.task('clean', () => { 12 | return del(paths.libDir); 13 | }); 14 | 15 | gulp.task('build', ['clean'], () => { 16 | return gulp.src(paths.allSrcJs) 17 | .pipe(babel()) 18 | .pipe(gulp.dest(paths.libDir)); 19 | }); 20 | 21 | gulp.task('main', ['build'], (callback) => { 22 | exec(`node ${paths.libDir}`, (error, stdout) => { 23 | console.log(stdout); 24 | return callback(error); 25 | }); 26 | }); 27 | 28 | gulp.task('watch', () => { 29 | gulp.watch(paths.allSrcJs, ['main']); 30 | }); 31 | 32 | gulp.task('default', ['watch', 'main']); 33 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "latest" 13 | ] 14 | }, 15 | "devDependencies": { 16 | "babel-preset-latest": "^6.16.0", 17 | "del": "^2.2.2", 18 | "gulp": "^3.9.1", 19 | "gulp-babel": "^6.1.2" 20 | }, 21 | "repository": "verekia/js-stack-from-scratch", 22 | "author": "Jonathan Verrecchia - @verekia", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /tutorial/3-es6-babel-gulp/src/index.js: -------------------------------------------------------------------------------- 1 | const str = 'ES6 now'; 2 | console.log(`Hello ${str}`); 3 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 4 - การใช้ ES6 syntax ในการเขียน Class 2 | 3 | - สร้างไฟล์ใหม่ `src/dog.js` ที่มีโค้ด ES6 class ดังนี้ 4 | 5 | ```javascript 6 | class Dog { 7 | constructor(name) { 8 | this.name = name; 9 | } 10 | 11 | bark() { 12 | return `Wah wah, I am ${this.name}`; 13 | } 14 | } 15 | 16 | module.exports = Dog; 17 | ``` 18 | 19 | หากคุณคุ้นเคยกับการเขียนโปรแกรมแบบ OOP ในภาษาอื่นๆ จะดูไม่แปลกใจกับคลาสของ ES6 มากนัก ซึ่งคลาสนั้นสามารถ export ให้ภายนอกสามารถใช้งานได้ ผ่านคำสั่ง `module.exports` 20 | 21 | และแน่นอน โค้ดต่างๆ ของ ES6 สามารถใช้งานในคลาสได้ทั้งหมด เช่น `const`, `let` รวมถึง "template strings" อย่างที่ใช้ใน function `bark()` รวมถึง arrow functions เช่น `(param) => { console.log('Hi'); }`) ก็ทำได้ แต่ในตัวอย่างของบทนี้เราจะไม่มีการใช้ arrow function เพื่อให้เกิดความคุ้นเคยกับ syntax แบบเดิมก่อนในการสร้าง class 22 | 23 | ในไฟล์ `src/index.js` เขียนโค้ดดังต่อไปนี้ 24 | 25 | ```javascript 26 | const Dog = require('./dog'); 27 | 28 | const toby = new Dog('Toby'); 29 | 30 | console.log(toby.bark()); 31 | ``` 32 | 33 | ถ้าเราสังเกต จะต่างกับ package `color` ที่เราเคยใช้ เมื่อเราต้องการ require ไฟล์มาใช้ เราจะใช้ `./` ใน require() เพื่อทำการ require class มาใช้งาน 34 | 35 | - สั่ง `yarn start` แล้วเราจะเห็นคำว่า 'Wah Wah, I am Toby' 36 | 37 | - ดูภายในโค้ดที่ถูก generate ภายในโฟลเดอร์ `lib` เพื่อดูโค้ดที่ถูก compile แล้วว่าหน้าตาเป็นอย่างไร (เช่น เราจะพบว่า มีการใช้ `var` แทนที่จะเป็น `const`) 38 | 39 | บทถัดไป [บทที่ 5 - การใช้ ES6 syntax ในการสร้าง modules](/tutorial/5-es6-modules-syntax) 40 | 41 | กลับไปยัง[บทที่แล้ว](/tutorial/3-es6-babel-gulp) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 42 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const del = require('del'); 4 | const exec = require('child_process').exec; 5 | 6 | const paths = { 7 | allSrcJs: 'src/**/*.js', 8 | libDir: 'lib', 9 | }; 10 | 11 | gulp.task('clean', () => { 12 | return del(paths.libDir); 13 | }); 14 | 15 | gulp.task('build', ['clean'], () => { 16 | return gulp.src(paths.allSrcJs) 17 | .pipe(babel()) 18 | .pipe(gulp.dest(paths.libDir)); 19 | }); 20 | 21 | gulp.task('main', ['build'], (callback) => { 22 | exec(`node ${paths.libDir}`, (error, stdout) => { 23 | console.log(stdout); 24 | return callback(error); 25 | }); 26 | }); 27 | 28 | gulp.task('watch', () => { 29 | gulp.watch(paths.allSrcJs, ['main']); 30 | }); 31 | 32 | gulp.task('default', ['watch', 'main']); 33 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "latest" 13 | ] 14 | }, 15 | "devDependencies": { 16 | "babel-preset-latest": "^6.16.0", 17 | "del": "^2.2.2", 18 | "gulp": "^3.9.1", 19 | "gulp-babel": "^6.1.2" 20 | }, 21 | "repository": "verekia/js-stack-from-scratch", 22 | "author": "Jonathan Verrecchia - @verekia", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/src/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | module.exports = Dog; 13 | -------------------------------------------------------------------------------- /tutorial/4-es6-syntax-class/src/index.js: -------------------------------------------------------------------------------- 1 | const Dog = require('./dog'); 2 | 3 | const toby = new Dog('Toby'); 4 | 5 | console.log(toby.bark()); 6 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 5 - การใช้ ES6 syntax ในการสร้าง modules 2 | 3 | ในบทนี้เราจะแทน `const Dog = require('./dog')` จากบทที่แล้ว ด้วย `import Dog from './dog'` แทน ซึ่งเป็น syntax ใหม่สำหรับการจัดการ modules ของ ES6 (ซึ่งจะตรงข้ามกับ modules syntax ของ "CommonJS") 4 | 5 | ใน `dog.js` เราจะแทน `module.exports = Dog` ด้วย `export default Dog` (เช่นเดียวกับด้านบน คำสั่งนี้เป็น syntax ใหม่ใน ES6) 6 | 7 | ให้จำไว้ว่า ใน `dog.js` ชื่อ `Dog` นั้นจะถูกใช้เฉพาะในการ `export` แต่ว่าเราก็สามารถ export anonymous class (คลาสที่ไม่มีชื่อ) ออกไปได้เช่นกับ ดังตัวอย่าง 8 | 9 | ```javascript 10 | export default class { 11 | constructor(name) { 12 | this.name = name; 13 | } 14 | 15 | bark() { 16 | return `Wah wah, I am ${this.name}`; 17 | } 18 | } 19 | ``` 20 | 21 | ซึ่งทำให้ในไฟล์ `index.js` นั้นเราไม่จำเป็นต้องใช้ 'Dog' สำหรับการ `import` เข้ามาแล้ว เราจะตั้งชื่อเป็นอะไรก็ได้ เพราะคลาสที่ถูก export ออกมานั้นเป็น anonymous class ทำให้เราตั้งชื่อเป็นอะไรก็ได้ เพื่ออ้างอิงถึงคลาสดังกล่าว ดังเช่นตัวอย่างด้านล่างนี้ 22 | 23 | ```javascript 24 | import Cat from './dog'; 25 | 26 | const toby = new Cat('Toby'); 27 | ``` 28 | 29 | แต่แน่นอนว่า เราก็ควรจะใช้ชื่อให้ตรงกับชื่อคลาส หรือ module ที่เรา import มาอยู่แล้ว (คงแปลกน่าดูที่เราไป import Cat มาจาก dog) แต่ก็มีบางกรณีที่เราอาจจะตั้งชื่อไม่ตรงกับสิ่งที่เรา import มา เช่น `const babel = require('gulp-babel')` ใน Gulp file 30 | 31 | อ้าว แล้วทีนี้ใน `gulpfile.js` เราไม่ใช้ `require()` แล้วแต่เราใช้ `import` แทนได้ไหม? ช่าวร้ายคือแม้ว่า Node เวอร์ชันล่าสุดจะรองรับฟีเจอร์ของ ES6 ได้เป็นส่วนมาก แต่ก็ยังไม่รองรับการใช้ ES6 modules 32 | 33 | แต่ในความโชคร้าย ก็มีความโชคดีที่เราสามารถให้ Gulp เรียก Babel มาช่วยงานเราได้ โดยเพียงแค่เปลี่ยนชื่อไฟล์จาก `gulpfile.js` เป็น `gulpfile.babel.js` Babel จะช่วยให้เราจัดการ `import` modules ของ Gulp แบบ ES6 modules ได้ โดยขั้นตอนง่ายๆ เพียงแค่ 34 | 35 | - เปลี่ยนชื่อไฟล์ `gulpfile.js` เป็น `gulpfile.babel.js` 36 | 37 | - แทนคำสั่ง `require()` ด้วย ES6 modules import ดังนี้ 38 | 39 | ```javascript 40 | import gulp from 'gulp'; 41 | import babel from 'gulp-babel'; 42 | import del from 'del'; 43 | import { exec } from 'child_process'; 44 | ``` 45 | 46 | สังเกตว่า syntax ของการนำ `exec` มาใช้จาก `child_process` นั้นดูดีทีเดียวเชียว 47 | 48 | - สั่ง `yarn start` เราก็จะเห็นคำว่า "Wah wah, I am Toby" เหมือนเดิม 49 | 50 | บทถัดไป [บทที่ 6 - การใช้ ESLint](/tutorial/6-eslint) 51 | 52 | กลับไปยัง[บทที่แล้ว](/tutorial/4-es6-syntax-class) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents). 53 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import babel from 'gulp-babel'; 3 | import del from 'del'; 4 | import { exec } from 'child_process'; 5 | 6 | const paths = { 7 | allSrcJs: 'src/**/*.js', 8 | libDir: 'lib', 9 | }; 10 | 11 | gulp.task('clean', () => { 12 | return del(paths.libDir); 13 | }); 14 | 15 | gulp.task('build', ['clean'], () => { 16 | return gulp.src(paths.allSrcJs) 17 | .pipe(babel()) 18 | .pipe(gulp.dest(paths.libDir)); 19 | }); 20 | 21 | gulp.task('main', ['build'], (callback) => { 22 | exec(`node ${paths.libDir}`, (error, stdout) => { 23 | console.log(stdout); 24 | return callback(error); 25 | }); 26 | }); 27 | 28 | gulp.task('watch', () => { 29 | gulp.watch(paths.allSrcJs, ['main']); 30 | }); 31 | 32 | gulp.task('default', ['watch', 'main']); 33 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "latest" 13 | ] 14 | }, 15 | "devDependencies": { 16 | "babel-preset-latest": "^6.16.0", 17 | "del": "^2.2.2", 18 | "gulp": "^3.9.1", 19 | "gulp-babel": "^6.1.2" 20 | }, 21 | "repository": "verekia/js-stack-from-scratch", 22 | "author": "Jonathan Verrecchia - @verekia", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/src/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/5-es6-modules-syntax/src/index.js: -------------------------------------------------------------------------------- 1 | import Dog from './dog'; 2 | 3 | const toby = new Dog('Toby'); 4 | 5 | console.log(toby.bark()); 6 | -------------------------------------------------------------------------------- /tutorial/6-eslint/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | -------------------------------------------------------------------------------- /tutorial/6-eslint/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 6 - การใช้ ESLint 2 | 3 | ในบทนี้เราจะสอนการ 'lint' (ตรวจสอบ) โค้ดของเราเพื่อหา issues ต่างๆ ที่อาจเกิดขึ้นได้ ซึ่ง ESLint เป็น linter ตัวหนึ่งสำหรับการทำ lint กับโค้ดของ ES6 โดยที่เราสามารถกำหนดกฎ (rules) ต่างๆ ที่เราต้องการให้โค้ดเราจะเป็นได้ ซึ่งเราจะใช้กฎที่สร้างโดย Airbnb โดยที่กฎของ Airbnb นั้นมีการใช้งาน plugin บางตัวด้วย ทำให้เราต้องติดตั้ง plugin เหล่านั่นเพื่อที่จะใช้กฎดังกล่าวที่ Airbnb เขียนเอาไว้ โดยทำตามนี้ 4 | 5 | - สั่ง `yarn add --dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y@2.2.3 eslint-plugin-react` 6 | 7 | ดังที่เห็น เราสามารถติดตั้ง package หลายๆ ตัวได้ด้วยคำสั่งเดียว ซึ่ง package เหล่านี้ก็จะถูกเพิ่มไปใน `package.json` โดยอัตโนมัติ 8 | 9 | ใน `package.json` เพิ่ม field `eslintConfig` หน้าตาตามนี้ 10 | 11 | ```json 12 | "eslintConfig": { 13 | "extends": "airbnb", 14 | "plugins": [ 15 | "import" 16 | ] 17 | }, 18 | ``` 19 | 20 | ส่วนของ `plugins` จะเป็นจุดที่บอก ESLint ว่าเราใช้ import syntax ของ ES6 21 | 22 | **หมายเหตุ**: ไฟล์ `.eslintrc.js`, `.eslintrc.json`, หรือ `.eslintrc.yaml` ที่อยู่ใน root ของโปรเจคนั้นใช้แทน `eslintConfig` ที่อยู่ใน `package.json` เหมือนกัน คล้ายๆ กับ configuration ของ Babel เราพยายามจะไม่ให้ root ของโปรเจคมีไฟล์เยอะเกินไป แต่ถ้าหาก ESLint config คุณเริ่มใหญ่ขึ้นเรื่อยๆ ก็ควรจะเขียนไฟล์แยกออกมา 23 | 24 | เราจะสร้าง Gulp task เพื่อสั่งรัน ESLint ให้กับเรา ดังนั้นเราต้องติดตั้ง plugin ให้กับ Gulp ด้วยเช่นเดียวกัน 25 | 26 | - สั่ง `yarn add --dev gulp-eslint` 27 | 28 | เพิ่ม task เหล่านี้ลงไปในไฟล์ `gulpfile.babel.js`: 29 | 30 | ```javascript 31 | import eslint from 'gulp-eslint'; 32 | 33 | const paths = { 34 | allSrcJs: 'src/**/*.js', 35 | gulpFile: 'gulpfile.babel.js', 36 | libDir: 'lib', 37 | }; 38 | 39 | // [...] 40 | 41 | gulp.task('lint', () => { 42 | return gulp.src([ 43 | paths.allSrcJs, 44 | paths.gulpFile, 45 | ]) 46 | .pipe(eslint()) 47 | .pipe(eslint.format()) 48 | .pipe(eslint.failAfterError()); 49 | }); 50 | ``` 51 | 52 | เราบอกให้ task นี้ของ Gulp ให้ทำการ include `gulpfile.babel.js` และไฟล์ JS ที่อยู่ในโฟลเดอร์ `src` ทั้งหมด เป็น source ต้นทางของ task (นั่นคือเราจะทำการ linting `gulpfile.babel.js` ด้วย) 53 | 54 | แก้ไข `build` Gulp task โดยให้ task `lint` ทำงานก่อนที่จะทำ task `build` และ `clean` 55 | 56 | ```javascript 57 | gulp.task('build', ['lint', 'clean'], () => { 58 | // ... 59 | }); 60 | ``` 61 | 62 | - สั่ง `yarn start` เราจะพบ linting error จำนวนมากภายใน Gulpfile และ warning การใช้ `console.log()` ใน `index.js` 63 | 64 | หนึ่งใน issue ที่คุณพบคือ `'gulp' should be listed in the project's dependencies, not devDependencies (import/no-extraneous-dependencies)` ซึ่งจริงๆ แล้ว error นี้เป็น False Negative ที่ฟ้อง error ผิด ทั้งๆ ที่จริงๆ แล้วมันไม่ควร error แบบนี้ เพราะ ESLint ไม่สามารถรู้ได้ว่าไฟล์ JS ไฟล์ไหนนั้นใช้สำหรับการ build เท่านั้น และไฟล์ไหนไม่ใช้ในการ build ซึ่ง gulp นั้นจัดว่าเป็น devDependencies อยู่แล้ว เพราะใช้เฉพาะตอน build เท่านั้น 65 | 66 | ดังนั้น เราต้องการความช่วยเหลือเล็กๆ น้อยๆ โดยทำการ comment บางอย่างลงไปในโค้ด ใน `gulpfile.babel.js` ให้เพิ่ม comment นี้ไปที่บรรทัดบนสุดว่า 67 | 68 | ```javascript 69 | /* eslint-disable import/no-extraneous-dependencies */ 70 | ``` 71 | 72 | ทำให้ ESLint จะไม่นำกฎ `import/no-extraneous-dependencies` มาใช้ตรวจสอบในไฟล์นี้ 73 | 74 | ตอนนี้เราจะเหลือแค่ issue `Unexpected block statement surrounding arrow body (arrow-body-style)` ซึ่งอันนี้เป็น issue ที่แจ้งมาดี เพราะ ESLint บอกว่ามีวิธีที่ดีกว่านี้ในการเขียนโค้ด 75 | 76 | ```javascript 77 | () => { 78 | return 1; 79 | } 80 | ``` 81 | 82 | ให้เป็นแบบนี้แทน 83 | 84 | ```javascript 85 | () => 1 86 | ``` 87 | 88 | เพราะเมื่อ function มีหน้าที่แค่ return อย่างเดียว เราไม่จำเป็นต้องมี { }, คำสั่ง return และ semicolon ใน ES6 ก็ได้ 89 | 90 | ดังนั้น เราก็อัพเดตโค้ดใน Gulp file ให้เป็นแบบนี้แทน 91 | 92 | ```javascript 93 | gulp.task('lint', () => 94 | gulp.src([ 95 | paths.allSrcJs, 96 | paths.gulpFile, 97 | ]) 98 | .pipe(eslint()) 99 | .pipe(eslint.format()) 100 | .pipe(eslint.failAfterError()) 101 | ); 102 | 103 | gulp.task('clean', () => del(paths.libDir)); 104 | 105 | gulp.task('build', ['lint', 'clean'], () => 106 | gulp.src(paths.allSrcJs) 107 | .pipe(babel()) 108 | .pipe(gulp.dest(paths.libDir)) 109 | ); 110 | ``` 111 | 112 | และ issue สุดท้ายที่หลงเหลือนั้นจะเกี่ยวกับ `console.log()` ซึ่งหากเราต้องการให้เราใช้ `console.log()` ใน `index.js` ได้ แทนที่จะเตือนเราในตัวอย่างนี้ คุณคงก็คงจะเดาได้ว่าต้องทำอะไร เราก็เพิ่ม `/* eslint-disable no-console */` ไว้ที่ด้านบนสุดของไฟล์ `index.js` สิ เหมือนกับที่ทำใน Gulpfile ของเรา 113 | 114 | - สั่ง `yarn start` แล้วเราจะพบการแจ้งเตือนจาก ESLint อีกแล้ว! 115 | 116 | **หมายเหตุ**: ในบทนี้เราใช้ ESLint ภายใน console ซึ่งดีมากเพื่อหา error ตอนที่เราทำการ build หรือก่อนที่เราจะ push โค้ดขึ้นไป แต่ก็จะเป็นการดีกว่านี้มากถ้าคุณจะใช้ ESLint ใน IDE ของคุณได้ แต่**ไม่แนะนำ**ให้ใช้ linting ตัวปกติของ IDE ในการ linting ES6 ให้ลองตั้งค่า IDE ตัวที่คุณใช้ เพื่อให้ใช้ตัว linting ที่อยู่ในโฟลเดอร์ `node_modules` เพื่อให้คุณสามารถใช้การตั้งค่าของโปรเจคที่คนอื่นๆ เขียนไว้ให้ รวมถึงใช้ preset ของ Airbnb ได้อีกด้วย หากไม่ทำแล้วคุณก็จะได้ตัว linting แบบปกติของ ES6 ไปใช้แทน 117 | 118 | บทถัดไป [บทที่ 7 - พัฒนาแอพฝั่ง Client โดยใช้ Webpack](/tutorial/7-client-webpack) 119 | 120 | กลับไปยัง[บทที่แล้ว](/tutorial/5-es6-modules-syntax) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 121 | -------------------------------------------------------------------------------- /tutorial/6-eslint/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-console */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import { exec } from 'child_process'; 8 | 9 | const paths = { 10 | allSrcJs: 'src/**/*.js', 11 | gulpFile: 'gulpfile.babel.js', 12 | libDir: 'lib', 13 | }; 14 | 15 | gulp.task('lint', () => 16 | gulp.src([ 17 | paths.allSrcJs, 18 | paths.gulpFile, 19 | ]) 20 | .pipe(eslint()) 21 | .pipe(eslint.format()) 22 | .pipe(eslint.failAfterError()) 23 | ); 24 | 25 | gulp.task('clean', () => del(paths.libDir)); 26 | 27 | gulp.task('build', ['lint', 'clean'], () => 28 | gulp.src(paths.allSrcJs) 29 | .pipe(babel()) 30 | .pipe(gulp.dest(paths.libDir)) 31 | ); 32 | 33 | gulp.task('main', ['build'], (callback) => { 34 | exec(`node ${paths.libDir}`, (error, stdout) => { 35 | console.log(stdout); 36 | return callback(error); 37 | }); 38 | }); 39 | 40 | gulp.task('watch', () => { 41 | gulp.watch(paths.allSrcJs, ['main']); 42 | }); 43 | 44 | gulp.task('default', ['watch', 'main']); 45 | -------------------------------------------------------------------------------- /tutorial/6-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ] 15 | }, 16 | "babel": { 17 | "presets": [ 18 | "latest" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "babel-preset-latest": "^6.16.0", 23 | "del": "^2.2.2", 24 | "eslint": "^3.8.1", 25 | "eslint-config-airbnb": "^12.0.0", 26 | "eslint-plugin-import": "^2.0.1", 27 | "eslint-plugin-jsx-a11y": "^2.2.3", 28 | "eslint-plugin-react": "^6.4.1", 29 | "gulp": "^3.9.1", 30 | "gulp-babel": "^6.1.2", 31 | "gulp-eslint": "^3.0.1" 32 | }, 33 | "repository": "verekia/js-stack-from-scratch", 34 | "author": "Jonathan Verrecchia - @verekia", 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /tutorial/6-eslint/src/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/6-eslint/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from './dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 7 - พัฒนาแอพฝั่ง Client โดยใช้ Webpack 2 | 3 | ## จัดโครงสร้างไฟล์ให้กับแอพของเรา 4 | 5 | - สร้างโฟลเดอร์ชื่อ `dist` ที่ root ของโปรเจค และเพิ่มไฟล์ `index.html` ลงไปดังนี้ 6 | 7 | ```html 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ``` 18 | 19 | ในโฟลเดอร์ `src` ให้สร้างโฟลเดอร์ย่อย `server`, `shared`, `client` และทำการย้ายไฟล์ `index.js` ไปยัง `server` และ `dog.js` ไปยัง `shared` แล้วให้ทำการสร้างไฟล์ `app.js` ไปไว้ในโฟลเดอร์ `client` 20 | 21 | ณ ตอนนี้เราจะยังไม่ทำอะไรกับ Node back-end แต่การแบ่งโฟลเดอร์แบบนี้จะช่วยให้เราเห็นภาพว่าของแต่ละอย่างอยู่ที่ใด ดังนั้นคุณต้องแก้โค้ด `import Dog from './dog'` ใน `server/index.js` ด้วย `import Dog from '../shared/dog';` แทน ไม่เช่นนั้นแล้ว ESLint จะตรวจเจอ error ที่แจ้งว่า unresolved modules 22 | 23 | เขียนโค้ดดังต่อไปนี้ใน `client/app.js` 24 | 25 | ```javascript 26 | import Dog from '../shared/dog'; 27 | 28 | const browserToby = new Dog('Browser Toby'); 29 | 30 | document.querySelector('.app').innerText = browserToby.bark(); 31 | ``` 32 | 33 | เพิ่ม JSON field เหล่านี้ไปใน `package.json` ข้างใน field `eslintConfig` 34 | 35 | ```json 36 | "env": { 37 | "browser": true 38 | } 39 | ``` 40 | 41 | เพื่อให้เราใช้ตัวแปร เช่น `window` หรือ `document` ที่ใช้งานบน web browser ได้ทันที โดยไม่ให้ ESLint บ่นกับเราเรื่องของตัวแปรที่ไม่โดนประกาศไว้ (undeclared variables) 42 | 43 | ถ้าคุณต้องการใช้ฟีเจอร์ใหม่ๆ ของ ES6 ในฝั่ง client เช่น `Promise` คุณต้องเพิ่ม [Babel Polyfill](https://babeljs.io/docs/usage/polyfill/) เข้าไปในโค้ดของฝั่ง client ด้วย 44 | 45 | - สั่ง `yarn add babel-polyfill` 46 | 47 | และด้านบนสุดของ `app.js` ก่อนทุกๆ อย่าง ให้เพิ่ม import ด้านล่างลงไป 48 | 49 | ```javascript 50 | import 'babel-polyfill'; 51 | ``` 52 | 53 | ซึ่งการเพิ่ม polyfill นั้นจะเป็นการให้ bundle ที่คุณได้นั้นมีขนาดใหญ่ขึ้น ดังนั้นควรจะเพิ่ม polyfill เฉพาะฟีเจอร์ที่คุณต้องการใช้เท่านั้น ซึ่งในตัวอย่างบทถัดๆ ไปจะแสดงให้เห็นในเรื่องของการเพิ่ม polyfill เฉพาะที่เราต้องการ 54 | 55 | ## Webpack 56 | 57 | ใน Node environment ทั้งหลายทั้งปวงนั้น คุณสามารถสั่ง `import` หลายๆ ไฟล์ได้ และ Node จะเป็นคนจัดหา file เหล่านั้นใน filesystem ให้เอง แต่ว่าใน web browser นั้นไม่มีระบบ filesystem ให้ใช้ ทำให้การ `import` นั้นดูเหมือนเป็นการ import ไปหาความว่างเปล่า ไม่รู้จะไปเอา module จากไหนมา ดังนั้นในการที่ไฟล์เริ่มต้นของเรา (entry point file) `app.js` ให้รู้ว่าต้องไป import อะไรมาใช้บ้าง เราจะทำการ "bundle" dependencies ทั้งหมดที่เราต้องการใช้ รวมมาเป็นไฟล์เดียว ซึ่ง Webpack เป็นเครื่องมือที่มีหน้าที่ในการทำเรื่องดังกล่าว 58 | 59 | Webpack จะมีการใช้ config file เหมือนๆ กับ Gulp แต่มีชื่อเรียกว่า `webpack.config.js` ซึ่งแน่นอนว่าเราสามารถใช้ ES6 imports กับ exports ได้ แบบเดียวกับที่ Gulp ขอให้ Babel ช่วยจัดการให้หน่อย โดยการตั้งชื่อไฟล์เป็น `webpack.config.babel.js` 60 | 61 | - สร้างไฟล์เปล่า `webpack.config.babel.js` 62 | 63 | - เพิ่ม `webpack.config.babel.js` ไปใน task 'lint' ของ Gulp โดยการเพิ่มค่าเข้าไปใน constant `paths` ดังนี้ 64 | 65 | ```javascript 66 | const paths = { 67 | allSrcJs: 'src/**/*.js', 68 | gulpFile: 'gulpfile.babel.js', 69 | webpackFile: 'webpack.config.babel.js', 70 | libDir: 'lib', 71 | distDir: 'dist', 72 | }; 73 | 74 | // [...] 75 | 76 | gulp.task('lint', () => 77 | gulp.src([ 78 | paths.allSrcJs, 79 | paths.gulpFile, 80 | paths.webpackFile, 81 | ]) 82 | .pipe(eslint()) 83 | .pipe(eslint.format()) 84 | .pipe(eslint.failAfterError()) 85 | ); 86 | ``` 87 | 88 | เราต้องสอนให้ Webpack นั้นทำการ process ES6 files โดยใช้ Babel (คล้ายกับการสอนให้ Gulp ทำการ process ES6 files โดยใช้ `gulp-babel`) ใน Webpack เมื่อเราต้องการ process ไฟล์ที่ไม่ใช่ JavaScript ธรรมดาๆ นั้น เราจะใช้สิ่งที่เรียกว่า *loaders* ในการ process ไฟล์ดังกล่าว ดังนั้น เรามาติดตั้ง Babel loader ให้กับ Webpack โดยให้ 89 | 90 | - สั่ง `yarn add --dev babel-loader` 91 | 92 | - เขียนโค้ดดังต่อไปนี้ลงไปใน `webpack.config.babel.js` 93 | 94 | ```javascript 95 | export default { 96 | output: { 97 | filename: 'client-bundle.js', 98 | }, 99 | devtool: 'source-map', 100 | module: { 101 | loaders: [ 102 | { 103 | test: /\.jsx?$/, 104 | loader: 'babel-loader', 105 | exclude: [/node_modules/], 106 | }, 107 | ], 108 | }, 109 | resolve: { 110 | extensions: ['', '.js', '.jsx'], 111 | }, 112 | }; 113 | ``` 114 | 115 | เรามาดูรายละเอียดของไฟล์นี้กัน 116 | 117 | เราต้องการให้ไฟล์นี้ถูก `export` เพื่อให้ Webpack อ่าน โดยในส่วนของ `output.filename` นั้นจะบอกชื่อไฟล์ bundle ที่เราต้องการ 118 | 119 | `devtool: 'source-map'` จะเป็นการเปิดใช้ source map เพื่อให้ทำการ debug บน web browser ได้ง่ายขึ้น 120 | 121 | ใน `module.loaders` เรามี `test` ที่เขียนเป็น JavaScript regex เพื่อใช้ในการ test ว่าไฟล์ไหนที่ต้องโดน process โดย `babel-loader` ซึ่งเราต้องการให้ทั้งไฟล์ที่มีนามสกุล `.js` และ `.jsx` (สำหรับ React ในบทถัดไป) ถูก process ด้วย `babel-loader` เราจึงใส่ regex `/\.jsx?$/` ลงไป และนอกจากนี้เราต้องการให้โฟลเดอร์ `node_modules` ไม่ถูก process ด้วย จึงใส่ไว้ใน field exclude เพื่อให้เมื่อมีการ `import` package ที่อยู่ในโฟลเดอร์ `node_modules` ตัว Babel จะไม่ยุ่งกับไฟล์เหล่านั้น เพื่อลดเวลาในการ build ลงไปอีก 122 | 123 | ใน `resolve` จะเป็นการบอก Webpack ว่าไฟล์นามสกุลไหนบ้างที่จะสามารถ `import` เข้ามาในโค้ดได้ โดยใช้ path แบบไม่ต้องระบุนามสกุล เช่น `import Foo from './foo'` โดยที่ `foo` อาจจะเป็นได้ทั้ง `foo.js` หรือ `foo.jsx` ตามที่เราบอกใน resolve 124 | 125 | โอเค ตอนนี้เราได้ set up Webpack แล้วเรียบร้อย แต่เรายังต้องหาวิธีที่จะ*สั่งให้ Webpack ทำงาน* 126 | 127 | ## ใช้งาน Webpack คู่กับ Gulp 128 | 129 | Webpack นั้นมีความสามารถสูงมาก สามารถทำอะไรได้เยอะแยะมากมาย จนเราอาจจะนำมันไปใช้แทน Gulp ก็ยังได้ สำหรับโปรเจคที่ทำบนฝั่ง Client เป็นหลัก แต่ Gulp นั้นเป็น tool ที่ทำงานทั่วๆ ไปได้ (เทียบกับ Webpack แล้ว Webpack จะเจาะจงสำหรับ Front-End มากกว่า) ซึ่งจะเป็นการที่ดีกว่าที่เราจะให้ Gulp เป็นคนทำ linting, รัน test หรือทำ Back-End tasks ทั้งหลายทั้งปวง และ Gulp ยังถือว่าเข้าใจง่ายกว่า Webpack config สำหรับมือใหม่ ดังนั้นการที่ให้ Webpack สามารถใช้งานผ่าน Gulp ได้จึงเป็นไอเดียที่ดี 130 | 131 | ดังนั้น เรามาสร้าง Gulp task เพื่อให้รัน Webpack กัน เปิดไฟล์ `gulpfile.babel.js` ขึ้นมา 132 | 133 | ตอนนี้เราไม่ต้องการให้ `main` task สั่งรัน `node lib/` อีกต่อไป เนื่องจากเราต้องการเปิด `index.html` ขึ้นมาเพื่อรันแอพแทน 134 | 135 | - ลบ `import { exec } from 'child_process'` ออก 136 | 137 | คล้ายๆ กับ plugin ของ Gulp package 'webpack-stream' จะช่วยให้เราใช้ Webpack ใน Gulp ได้ง่ายดายขึ้นมาก 138 | 139 | - ติดตั้ง package ด้วยคำสั่ง `yarn add --dev webpack-stream` 140 | 141 | - เพิ่ม `import` 142 | 143 | ```javascript 144 | import webpack from 'webpack-stream'; 145 | import webpackConfig from './webpack.config.babel'; 146 | ``` 147 | 148 | บรรทัดที่สอง จะเป็นการหยิบ config file ที่เราเขียนไว้เข้ามา 149 | 150 | เหมือนที่ได้กล่าวไว้ข้างต้น ในบทถัดไปเราจะมีการใช้ไฟล์ `.jsx` (ในฝั่ง client รวมถึงในฝั่ง server ด้วยในอนาคต) ดังนั้น เราควรจะต้องตั้งค่าเผื่ออนาคตไว้ตั้งแต่ตอนนี้เลย ด้วยการทำตามนี้ 151 | 152 | - เปลี่ยนตัวแปร constant ให้เป็นไปตามนี้ 153 | 154 | ```javascript 155 | const paths = { 156 | allSrcJs: 'src/**/*.js?(x)', 157 | serverSrcJs: 'src/server/**/*.js?(x)', 158 | sharedSrcJs: 'src/shared/**/*.js?(x)', 159 | clientEntryPoint: 'src/client/app.js', 160 | gulpFile: 'gulpfile.babel.js', 161 | webpackFile: 'webpack.config.babel.js', 162 | libDir: 'lib', 163 | distDir: 'dist', 164 | }; 165 | ``` 166 | 167 | `.js?(x)` เป็น pattern ธรรมดาเพื่อใช้ match `.js` หรือ `.jsx` ไฟล์ได้ 168 | 169 | ดังนั้น ในตอนนี้เราจะมีตัวแปร constant เพื่อบอก path และชื่อไฟล์ต่างๆ ให้ใช้ภายในแอพของเราแล้ว 170 | 171 | - แก้ `main` task ให้เป็นดังนี้ 172 | 173 | ```javascript 174 | gulp.task('main', ['lint', 'clean'], () => 175 | gulp.src(paths.clientEntryPoint) 176 | .pipe(webpack(webpackConfig)) 177 | .pipe(gulp.dest(paths.distDir)) 178 | ); 179 | ``` 180 | 181 | **หมายเหตุ**: task `build` ของเรานั้นจะทำการแปลงโค้ด ES6 เป็น ES5 สำหรับทุกไฟล์ที่มีนามสกุล `.js` ภายในโฟลเดอร์ `src` ซึ่งในตอนนี้เราแบ่งโค้ดออกเป็นสามส่วนคือ `server`, `shared` และ `client` ทำให้เราต้องแก้ task เพื่อให้ compile เฉพาะไฟล์ใน `server` และ `shared` เท่านั้น (เพราะ Webpack จะจัดการเรื่องของ `client` ให้แล้ว) อย่างไรก็ตาม ในบทเรื่อง Testing เราจะให้ Gulp นั้น compile โค้ดจากฝั่ง `client` ที่เป็นตัวทำ testing ด้วย ซึ่ง Webpack จะไม่ยุ่งเกี่ยวกับโค้ดของการ testing ดังนั้นจนกว่าจะไปถึงบทถัดๆ ไป โค้ดในส่วนนี้เหมือนเป็นการเขียนอะไรที่ซ้ำซ้อนจนไม่มีความจำเป็น ผม(ผู้เขียน) ต้องการให้ทุกคนเข้าใจว่านี่ไม่ใช่เรื่องร้ายแรงมากสำหรับตอนนี้ โดยเฉพาะเราจะไม่มีการใช้ task `build` และโฟลเดอร์ `lib` อีกต่อไป จนกว่าจะถึงบทดังกล่าว ดังนั้นในตอนนี้สิ่งที่เราสนใจนั้นก็คือ client bundle เท่านั้น 182 | 183 | - สั่ง `yarn start` เราจะเห็นว่า Webpack กำลัง build ไฟล์ `client-bundle.js` เมื่อเสร็จแล้วให้เปิดไฟล์ `index.html` ขึ้นมาใน web browser เราจะเห็นคำว่า "Wah wah, I am Browser Toby" ขึ้นมา 184 | 185 | อย่างสุดท้าย เราจะพบว่า ไฟล์ `dist/client-bundle.js` กับ `dist/client-bundle.js.map` จะไม่สามารถถูก clean ได้ด้วย task `clean` ที่เราเขียนไว้ 186 | 187 | - เพิ่ม `clientBundle: 'dist/client-bundle.js?(.map)'` ใน `paths` ของเรา และแก้ไข `clean` task ให้เป็นแบบนี้ เพื่อให้เราทำการ clean ถูกโฟลเดอร์ 188 | 189 | ```javascript 190 | gulp.task('clean', () => del([ 191 | paths.libDir, 192 | paths.clientBundle, 193 | ])); 194 | ``` 195 | 196 | - เพิ่ม `/dist/client-bundle.js*` เข้าไปในไฟล์ `.gitignore` 197 | 198 | บทถัดไป [8 - React](/tutorial/8-react) 199 | 200 | กลับไปยัง[บทที่แล้ว](/tutorial/6-eslint) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 201 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.js', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | } 18 | }, 19 | "babel": { 20 | "presets": [ 21 | "latest" 22 | ] 23 | }, 24 | "devDependencies": { 25 | "babel-loader": "^6.2.5", 26 | "babel-preset-latest": "^6.16.0", 27 | "del": "^2.2.2", 28 | "eslint": "^3.8.1", 29 | "eslint-config-airbnb": "^12.0.0", 30 | "eslint-plugin-import": "^2.0.1", 31 | "eslint-plugin-jsx-a11y": "^2.2.2", 32 | "eslint-plugin-react": "^6.4.1", 33 | "gulp": "^3.9.1", 34 | "gulp-babel": "^6.1.2", 35 | "gulp-eslint": "^3.0.1", 36 | "webpack-stream": "^3.2.0" 37 | }, 38 | "dependencies": { 39 | "babel-polyfill": "^6.16.0" 40 | }, 41 | "repository": "verekia/js-stack-from-scratch", 42 | "author": "Jonathan Verrecchia - @verekia", 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/src/client/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const browserToby = new Dog('Browser Toby'); 6 | 7 | document.querySelector('.app').innerText = browserToby.bark(); 8 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/7-client-webpack/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/8-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/8-react/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 8 - React 2 | 3 | ในบทนี้เราจะมาสร้างแอพเรา โดยใช้ React เป็น Front-End 4 | 5 | แรกสุด เราติดตั้ง React และ ReactDOM ก่อน 6 | 7 | - สั่ง `yarn add react react-dom` 8 | 9 | package ทั้งสองอันนี้จะไปอยู่ใน `"dependencies"` ไม่ใช่ `"devDependencies"` เพราะทั้งสองอันนี้ไม่ใช่ build tools แต่เป็น client bundle ที่เราต้องใช้ตอน production 10 | 11 | แก้ชื่อไฟล์ `src/client/app.js` เป็น `src/client/app.jsx` และเขียนโค้ด React และ JSX ดังต่อไปนี้ลงไป 12 | 13 | ```javascript 14 | import 'babel-polyfill'; 15 | 16 | import React, { PropTypes } from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import Dog from '../shared/dog'; 19 | 20 | const dogBark = new Dog('Browser Toby').bark(); 21 | 22 | const App = props => ( 23 |
24 | The dog says: {props.message} 25 |
26 | ); 27 | 28 | App.propTypes = { 29 | message: PropTypes.string.isRequired, 30 | }; 31 | 32 | ReactDOM.render(, document.querySelector('.app')); 33 | ``` 34 | 35 | **หมายเหตุ**: ถ้าคุณไม่คุ้นเคยกับ React หรือ PropTypes ให้ไปเรียนรู้เบสิคของ React มาก่อน แล้วกลับมายัง Tutorial นี้ต่อ เพราะในบทถัดๆ ไปจะมีคอนเซปต่างๆ ของ React ที่จะถูกใช้อย่างมาก ทำให้เป็นเรื่องดีหากคุณมีความเข้าใจเกี่ยวกับ React ระดับหนึ่งก่อนที่จะไปต่อจากนี้ 36 | 37 | ใน Gulpfile เปลี่ยนค่าของ `clientEntryPoint` ให้เป็นนามสกุล `.jsx` ด้วย 38 | 39 | ```javascript 40 | clientEntryPoint: 'src/client/app.jsx', 41 | ``` 42 | 43 | เนื่องจากเรามีการใช้ syntax ของ JSX เราต้องบอกให้ Babel รู้ว่าเราต้องแปลง JSX ด้วยนะ โดยการที่ติดตั้ง preset React Babel ที่จะสอนให้ Babel รู้จักวิธีการ process syntax ของ JSX โดยให้สั่ง `yarn add --dev babel-preset-react` และเปลี่ยน `babel` ที่อยู่ใน `package.json` ให้เป็นแบบนี้ 44 | 45 | ```json 46 | "babel": { 47 | "presets": [ 48 | "latest", 49 | "react" 50 | ] 51 | }, 52 | ``` 53 | 54 | ตอนนี้ หลังจากสั่ง `yarn start` เมื่อเราเปิด `index.html` เราจะเห็น "The dog says: Wah wah, I am Browser Toby" ที่ถูก render ด้วย React 55 | 56 | บทถัดไป [บทที่ 9 - Redux](/tutorial/9-redux) 57 | 58 | กลับไปยัง[บทที่แล้ว](/tutorial/7-client-webpack) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 59 | -------------------------------------------------------------------------------- /tutorial/8-react/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/8-react/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.jsx', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/8-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | } 18 | }, 19 | "babel": { 20 | "presets": [ 21 | "latest", 22 | "react" 23 | ] 24 | }, 25 | "dependencies": { 26 | "babel-polyfill": "^6.16.0", 27 | "react": "^15.3.2", 28 | "react-dom": "^15.3.2" 29 | }, 30 | "devDependencies": { 31 | "babel-loader": "^6.2.5", 32 | "babel-preset-latest": "^6.16.0", 33 | "babel-preset-react": "^6.16.0", 34 | "del": "^2.2.2", 35 | "eslint": "^3.8.1", 36 | "eslint-config-airbnb": "^12.0.0", 37 | "eslint-plugin-import": "^2.0.1", 38 | "eslint-plugin-jsx-a11y": "^2.2.3", 39 | "eslint-plugin-react": "^6.4.1", 40 | "gulp": "^3.9.1", 41 | "gulp-babel": "^6.1.2", 42 | "gulp-eslint": "^3.0.1", 43 | "webpack-stream": "^3.2.0" 44 | }, 45 | "repository": "verekia/js-stack-from-scratch", 46 | "author": "Jonathan Verrecchia - @verekia", 47 | "license": "MIT" 48 | } 49 | -------------------------------------------------------------------------------- /tutorial/8-react/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React, { PropTypes } from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Dog from '../shared/dog'; 6 | 7 | const dogBark = new Dog('Browser Toby').bark(); 8 | 9 | const App = props => ( 10 |
11 | The dog says: {props.message} 12 |
13 | ); 14 | 15 | App.propTypes = { 16 | message: PropTypes.string.isRequired, 17 | }; 18 | 19 | ReactDOM.render(, document.querySelector('.app')); 20 | -------------------------------------------------------------------------------- /tutorial/8-react/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/8-react/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/8-react/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tutorial/9-redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | /lib/ 5 | /dist/client-bundle.js* 6 | -------------------------------------------------------------------------------- /tutorial/9-redux/README.md: -------------------------------------------------------------------------------- 1 | # บทที่ 9 - Redux 2 | 3 | ในบทนี้ (ซึ่งจะมีความยากเยอะมาก) เราจะเพิ่ม [Redux](http://redux.js.org/) เข้ามาใช้ในแอพของเรา แล้วเราจะใช้มันคู่กับ React 4 | 5 | Redux นั้นเป็นคนที่จัดการเกี่ยวกับ state ของแอพเรา Redux จะมีสิ่งที่เรียกว่า **store** ที่เป็น JavaScript object​ ธรรมดา เพื่อบ่งบอกถึง state ของแอพ **actions** ที่ปกติจะถูกเรียกใช้โดย users และ **reducers** ที่สามารถมองเป็นคนที่จัดการ action ที่เกิดขึ้นก็ได้ โดย Reducers จะเป็นคนเปลี่ยนแปลง state ของแอพ (ซึ่งก็ถูกเก็บใน *store*) เมื่อ state มีการเปลี่ยนแปลงขึ้นมาแล้ว จะทำให้แอพ (หน้า view) เกิดการเปลี่ยนแปลงขึ้น 6 | 7 | ตัวอย่างหลักการของ Redux แบบเข้าใจง่ายขึ้นนั้นสามารถดูได้[ที่นี่](http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/9) 8 | 9 | เพื่อที่จะสาธิตวิธีการใช้งาน Redux ให้ง่ายและ simple ที่สุดเท่าที่จะเป็นไปได้ แอพที่เราจะทำจะประกอบด้วย message กับ button message จะบอกว่าตอนนี้หมาของเรานั่นเห่าหรือไม่เห่า (ตอนเริ่มต้น หมาจะไม่เห่า) และ button นั้นจะเป็นการสั่งให้หมาเห่า ซึ่งทำให้ message ต้องมีการอัพเดต (บอกว่าหมาเห่า) 10 | 11 | เราต้องการ package เพิ่มเติมสองตัวในบทนี้ `redux` และ `react-redux` 12 | 13 | - สั่ง `yarn add redux react-redux` 14 | 15 | เราเริ่มต้นโดยการสร้างโฟลเดอร์ขึ้นมาสองอัน `src/client/actions` และ `src/client/reducers` 16 | 17 | - ในโฟลเดอร์ `actions` สร้างไฟล์ `dog-actions.js` ใน `dog-actions.js` เขียนโค้ดตามนี้ 18 | 19 | ```javascript 20 | export const MAKE_BARK = 'MAKE_BARK'; 21 | 22 | export const makeBark = () => ({ 23 | type: MAKE_BARK, 24 | payload: true, 25 | }); 26 | ``` 27 | 28 | ในโค้ดนั้นเรามีการนิยาม action type `MAKE_BARK` และ function (หรืออีกชื่อหนึ่งคือ *action creator*) ชื่อ `makeBark` ที่จะเป็นคนไปเรียก action `MAKE_BARK` ทั้งคู่จะถูก export ออกไป เนื่องจากเราต้องการทั้งสองค่าในไฟล์อื่นๆ โดย action นั้นจะสร้าง [Flux Standard Action](https://github.com/acdlite/flux-standard-action) model ขึ้นมา โดยมีสอง attributes คือ `type` กับ `payload` 29 | 30 | - ใน `reducers` สร้างไฟล์ `dog-reducer.js` ใน `dog-reducer.js` เขียนโค้ดตามนี้ 31 | 32 | ```javascript 33 | import { MAKE_BARK } from '../actions/dog-actions'; 34 | 35 | const initialState = { 36 | hasBarked: false, 37 | }; 38 | 39 | const dogReducer = (state = initialState, action) => { 40 | switch (action.type) { 41 | case MAKE_BARK: 42 | return { hasBarked: action.payload }; 43 | default: 44 | return state; 45 | } 46 | }; 47 | 48 | export default dogReducer; 49 | ``` 50 | 51 | ในนี้เราจะกำหนด state เริ่มต้นให้กับแอพเรา ซึ่งเป็น object ที่มี property `hasBarked` โดยให้เริ่มเป็น `false` (หมาไม่เห่า) และประกาศ `dogReducer` ให้เป็น function ที่มีหน้าที่ในการจัดการ state ตาม action ที่ได้รับมา ซึ่ง state นั้นจะไม่สามารถแก้ไขได้ตรงๆ ใน function แต่จะ return state object ก้อนใหม่ขึ้นมาแทน (สังเกตว่ามีการ `return { hasBarked: action.payload };` มาแทน) 52 | 53 | - เราจะแก้ไข `app.jsx` เพื่อให้สร้าง *store* ขึ้นมา โดยเราสามารถเขียนไฟล์นี้ใหม่ได้โดยมีโค้ดตามนี้แทน 54 | 55 | ```javascript 56 | import React from 'react'; 57 | import ReactDOM from 'react-dom'; 58 | import { createStore, combineReducers } from 'redux'; 59 | import { Provider } from 'react-redux'; 60 | import dogReducer from './reducers/dog-reducer'; 61 | import BarkMessage from './containers/bark-message'; 62 | import BarkButton from './containers/bark-button'; 63 | 64 | const store = createStore(combineReducers({ 65 | dog: dogReducer, 66 | })); 67 | 68 | ReactDOM.render( 69 | 70 |
71 | 72 | 73 |
74 |
75 | , document.querySelector('.app') 76 | ); 77 | ``` 78 | 79 | store ของเราจะถูกสร้างโดย Redux function `createStore` โดย store object นั้นจะรวม reducers ทุกตัวที่เรามี โดยใช้ function `combineReducers` ของ Redux (ในกรณีนี้เรามี reducer ตัวเดียว) reducer แต่ละตัวจะถูกตั้งชื่อที่นี่ โดยเราตั้งชื่อ reducer ของเราว่า `dog` 80 | 81 | ซึ่งส่วนประกอบทั้งหมดก่อนหน้านี้ ถือว่าเป็น pure Redux ทั้งหมด ยังไม่มี React เลย เราต้้องผูกทั้งสองอย่างเข้าด้วยกัน โดยใช้ `react-redux` เพื่อให้ `react-redux` ทำการ pass store เข้าไปในแอพที่ใช้ React ของเรา ซึ่งแอพของเราทั้งหมดจะต้องโดน wrap ด้วย `` component ที่ component นี้มี child component ได้แค่คนเดียวเท่านั้น ทำให้เราต้องสร้าง `
` ขึ้นมา และใน `
` นี้จะมี elements หลักของแอพเราสองอัน คือ `BarkMessage` และ `BarkButton` 82 | 83 | ถ้าเราสังเกตดูในส่วนของ `import` จะพบว่า `BarkMessage` และ `BarkButton` ถูก import มาจากโฟลเดอร์ `containers` ตอนนี้ถึงเวลาแล้วที่เราจะอธิบาย concept ของ **Components** และ **Containers** แบบจริงๆ จังๆ สักที 84 | 85 | *Components* นั้นเป็น *dumb* React components ธรรมดาๆ ซึ่ง เพราะ Components นั้นจะไม่รู้เรื่องเกี่ยวกับ Redux state เลยแม้แต่น้อย แต่กับ *Containers* นั้นจะกลับกัน Containers เป็น *smart* components ที่รู้จักเกี่ยวกับ state ของ Redux และตอนนี้เรากำลังจะทำ *เชื่อมต่อ* smart component ไปกับ dumb components ดังนี้ 86 | 87 | - สร้างโฟลเดอร์ 2 อัน `src/client/components` กับ `src/client/containers` 88 | 89 | - ในโฟลเดอร์ `components` สร้างไฟล์ดังต่อไปนี้ 90 | 91 | **button.jsx** 92 | 93 | ```javascript 94 | import React, { PropTypes } from 'react'; 95 | 96 | const Button = ({ action, actionLabel }) => ; 97 | 98 | Button.propTypes = { 99 | action: PropTypes.func.isRequired, 100 | actionLabel: PropTypes.string.isRequired, 101 | }; 102 | 103 | export default Button; 104 | ``` 105 | 106 | และไฟล์ **message.jsx**: 107 | 108 | ```javascript 109 | import React, { PropTypes } from 'react'; 110 | 111 | const Message = ({ message }) =>
{message}
; 112 | 113 | Message.propTypes = { 114 | message: PropTypes.string.isRequired, 115 | }; 116 | 117 | export default Message; 118 | 119 | ``` 120 | 121 | ทั้งสองอันนี้เป็นตัวอย่างของ *dumb* components เราจะพบว่าทั้งคู่นั้นต่างไม่มี logic ใดๆ เลย มีหน้าที่เพียงแค่โชว์สิ่งที่มีคนขอให้โชว์ ผ่านสิ่งที่เรียกว่า **props** ใน React ข้อแตกต่างอย่างชัดเจนของ `button.jsx` กับ `message.jsx` นั้นคือ `Button` มี **action** ส่งมาใน props ของมัน ซึ่ง action ที่ถูกส่งมาจะผูกติดกับ event `onClick` ของปุ่ม นอกจากนี้ในมุมมองของแอพเรา เราจะพบว่า label ของ `Button` จะไม่ถูกเปลี่ยนแปลงเลย อย่างไรก็ตาม `Message` component นั้นจะเปลี่ยนค่าไปตาม state ของแอพเรา 122 | 123 | ย้ำอีกครั้งว่า *components* จะไม่รู้อะไรเกี่ยวกับ Redux **actions** หรือ **state** ของแอพเรา นั่นทำให้เราต้องสร้าง smart **containers** ขึ้นมาครอบ เพื่อส่ง *actions* หรือ *data* ที่ถูกต้องไปให้กับ dumb components ทั้งสองตัว 124 | 125 | - ในโฟลเดอร์ `containers` ให้สร้างไฟล์ 126 | 127 | **bark-button.js** 128 | 129 | ```javascript 130 | import { connect } from 'react-redux'; 131 | import Button from '../components/button'; 132 | import { makeBark } from '../actions/dog-actions'; 133 | 134 | const mapDispatchToProps = dispatch => ({ 135 | action: () => { dispatch(makeBark()); }, 136 | actionLabel: 'Bark', 137 | }); 138 | 139 | export default connect(null, mapDispatchToProps)(Button); 140 | ``` 141 | 142 | และไฟล์ **bark-message.js**: 143 | 144 | ```javascript 145 | import { connect } from 'react-redux'; 146 | import Message from '../components/message'; 147 | 148 | const mapStateToProps = state => ({ 149 | message: state.dog.hasBarked ? 'The dog barked' : 'The dog did not bark', 150 | }); 151 | 152 | export default connect(mapStateToProps)(Message); 153 | ``` 154 | 155 | `BarkButton` จะเชื่อมกับ `Button` โดยมี `makeBark` action และ `dispatch` method ของ Redux ส่วน `BarkMessage` จะเชื่อม state ของแอพกับ `Message` เมื่อ state เกิดการเปลี่ยนแปลง `Message` จะทำการ re-render component โดยอัตโนมัติ การเชื่อมต่อทั้งหมดนี้จะทำผ่าน function ชื่อว่า `connect` ของ `react-redux` 156 | 157 | - ตอนนี้ลองสั่ง `yarn start` และเปิด `index.html` ขึ้นมา เราจะเห็นคำว่า "The dog did not bark" และมีปุ่มหนึ่งปุ่ม เมื่อคุณกดปุ่ม ข้อความควรจะเป็นคำว่า "The dog barked" 158 | 159 | บทถัดไป [บทที่ 10 - Immutable JS และ Redux Improvements](/tutorial/10-immutable-redux-improvements) 160 | 161 | กลับไปยัง[บทที่แล้ว](/tutorial/8-react) หรือไปที่[สารบัญ](https://github.com/MicroBenz/js-stack-from-scratch#table-of-contents) 162 | -------------------------------------------------------------------------------- /tutorial/9-redux/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tutorial/9-redux/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | import gulp from 'gulp'; 4 | import babel from 'gulp-babel'; 5 | import eslint from 'gulp-eslint'; 6 | import del from 'del'; 7 | import webpack from 'webpack-stream'; 8 | import webpackConfig from './webpack.config.babel'; 9 | 10 | const paths = { 11 | allSrcJs: 'src/**/*.js?(x)', 12 | serverSrcJs: 'src/server/**/*.js?(x)', 13 | sharedSrcJs: 'src/shared/**/*.js?(x)', 14 | clientEntryPoint: 'src/client/app.jsx', 15 | clientBundle: 'dist/client-bundle.js?(.map)', 16 | gulpFile: 'gulpfile.babel.js', 17 | webpackFile: 'webpack.config.babel.js', 18 | libDir: 'lib', 19 | distDir: 'dist', 20 | }; 21 | 22 | gulp.task('lint', () => 23 | gulp.src([ 24 | paths.allSrcJs, 25 | paths.gulpFile, 26 | paths.webpackFile, 27 | ]) 28 | .pipe(eslint()) 29 | .pipe(eslint.format()) 30 | .pipe(eslint.failAfterError()) 31 | ); 32 | 33 | gulp.task('clean', () => del([ 34 | paths.libDir, 35 | paths.clientBundle, 36 | ])); 37 | 38 | gulp.task('build', ['lint', 'clean'], () => 39 | gulp.src(paths.allSrcJs) 40 | .pipe(babel()) 41 | .pipe(gulp.dest(paths.libDir)) 42 | ); 43 | 44 | gulp.task('main', ['lint', 'clean'], () => 45 | gulp.src(paths.clientEntryPoint) 46 | .pipe(webpack(webpackConfig)) 47 | .pipe(gulp.dest(paths.distDir)) 48 | ); 49 | 50 | gulp.task('watch', () => { 51 | gulp.watch(paths.allSrcJs, ['main']); 52 | }); 53 | 54 | gulp.task('default', ['watch', 'main']); 55 | -------------------------------------------------------------------------------- /tutorial/9-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-scratch", 3 | "version": "1.0.0", 4 | "description": "JavaScript Stack from Scratch - Step-by-step tutorial to build a modern JavaScript stack", 5 | "scripts": { 6 | "start": "gulp", 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "tutorial-test": "gulp main" 9 | }, 10 | "eslintConfig": { 11 | "extends": "airbnb", 12 | "plugins": [ 13 | "import" 14 | ], 15 | "env": { 16 | "browser": true 17 | } 18 | }, 19 | "babel": { 20 | "presets": [ 21 | "latest", 22 | "react" 23 | ] 24 | }, 25 | "dependencies": { 26 | "babel-polyfill": "^6.16.0", 27 | "react": "^15.3.2", 28 | "react-dom": "^15.3.2", 29 | "react-redux": "^4.4.5", 30 | "redux": "^3.6.0" 31 | }, 32 | "devDependencies": { 33 | "babel-loader": "^6.2.5", 34 | "babel-preset-latest": "^6.16.0", 35 | "babel-preset-react": "^6.16.0", 36 | "del": "^2.2.2", 37 | "eslint": "^3.8.1", 38 | "eslint-config-airbnb": "^12.0.0", 39 | "eslint-plugin-import": "^2.0.1", 40 | "eslint-plugin-jsx-a11y": "^2.2.3", 41 | "eslint-plugin-react": "^6.4.1", 42 | "gulp": "^3.9.1", 43 | "gulp-babel": "^6.1.2", 44 | "gulp-eslint": "^3.0.1", 45 | "webpack-stream": "^3.2.0" 46 | }, 47 | "repository": "verekia/js-stack-from-scratch", 48 | "author": "Jonathan Verrecchia - @verekia", 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/actions/dog-actions.js: -------------------------------------------------------------------------------- 1 | export const MAKE_BARK = 'MAKE_BARK'; 2 | 3 | export const makeBark = () => ({ 4 | type: MAKE_BARK, 5 | payload: true, 6 | }); 7 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { createStore, combineReducers } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import dogReducer from './reducers/dog-reducer'; 8 | import BarkMessage from './containers/bark-message'; 9 | import BarkButton from './containers/bark-button'; 10 | 11 | const store = createStore(combineReducers({ 12 | dog: dogReducer, 13 | })); 14 | 15 | ReactDOM.render( 16 | 17 |
18 | 19 | 20 |
21 |
22 | , document.querySelector('.app') 23 | ); 24 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/components/button.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Button = ({ action, actionLabel }) => ; 4 | 5 | Button.propTypes = { 6 | action: PropTypes.func.isRequired, 7 | actionLabel: PropTypes.string.isRequired, 8 | }; 9 | 10 | export default Button; 11 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Message = ({ message }) =>
{message}
; 4 | 5 | Message.propTypes = { 6 | message: PropTypes.string.isRequired, 7 | }; 8 | 9 | export default Message; 10 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/containers/bark-button.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Button from '../components/button'; 3 | import { makeBark } from '../actions/dog-actions'; 4 | 5 | const mapDispatchToProps = dispatch => ({ 6 | action: () => { dispatch(makeBark()); }, 7 | actionLabel: 'Bark', 8 | }); 9 | 10 | export default connect(null, mapDispatchToProps)(Button); 11 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/containers/bark-message.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Message from '../components/message'; 3 | 4 | const mapStateToProps = state => ({ 5 | message: state.dog.hasBarked ? 'The dog barked' : 'The dog did not bark', 6 | }); 7 | 8 | export default connect(mapStateToProps)(Message); 9 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/client/reducers/dog-reducer.js: -------------------------------------------------------------------------------- 1 | import { MAKE_BARK } from '../actions/dog-actions'; 2 | 3 | const initialState = { 4 | hasBarked: false, 5 | }; 6 | 7 | const dogReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case MAKE_BARK: 10 | return { hasBarked: action.payload }; 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | export default dogReducer; 17 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import Dog from '../shared/dog'; 4 | 5 | const toby = new Dog('Toby'); 6 | 7 | console.log(toby.bark()); 8 | -------------------------------------------------------------------------------- /tutorial/9-redux/src/shared/dog.js: -------------------------------------------------------------------------------- 1 | 2 | class Dog { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | 7 | bark() { 8 | return `Wah wah, I am ${this.name}`; 9 | } 10 | } 11 | 12 | export default Dog; 13 | -------------------------------------------------------------------------------- /tutorial/9-redux/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | abbrev@1: 4 | version "1.0.9" 5 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" 6 | 7 | acorn-jsx@^3.0.1: 8 | version "3.0.1" 9 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" 10 | dependencies: 11 | acorn "^3.0.4" 12 | 13 | acorn@^3.0.4: 14 | version "3.3.0" 15 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" 16 | 17 | ansi-regex@^2.0.0: 18 | version "2.0.0" 19 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" 20 | 21 | ansi-styles@^2.2.1: 22 | version "2.2.1" 23 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 24 | 25 | aproba@^1.0.3: 26 | version "1.0.4" 27 | resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.0.4.tgz#2713680775e7614c8ba186c065d4e2e52d1072c0" 28 | 29 | are-we-there-yet@~1.1.2: 30 | version "1.1.2" 31 | resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3" 32 | dependencies: 33 | delegates "^1.0.0" 34 | readable-stream "^2.0.0 || ^1.1.13" 35 | 36 | argparse@^1.0.7: 37 | version "1.0.9" 38 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" 39 | dependencies: 40 | sprintf-js "~1.0.2" 41 | 42 | array-find-index@^1.0.1: 43 | version "1.0.2" 44 | resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" 45 | 46 | array-index@^1.0.0: 47 | version "1.0.0" 48 | resolved "https://registry.yarnpkg.com/array-index/-/array-index-1.0.0.tgz#ec56a749ee103e4e08c790b9c353df16055b97f9" 49 | dependencies: 50 | debug "^2.2.0" 51 | es6-symbol "^3.0.2" 52 | 53 | asn1@~0.2.3: 54 | version "0.2.3" 55 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 56 | 57 | assert-plus@^0.2.0: 58 | version "0.2.0" 59 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" 60 | 61 | assert-plus@^1.0.0: 62 | version "1.0.0" 63 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 64 | 65 | asynckit@^0.4.0: 66 | version "0.4.0" 67 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 68 | 69 | aws-sign2@~0.6.0: 70 | version "0.6.0" 71 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" 72 | 73 | aws4@^1.2.1: 74 | version "1.5.0" 75 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" 76 | 77 | babel-runtime@^6.0.0: 78 | version "6.11.6" 79 | resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.11.6.tgz#6db707fef2d49c49bfa3cb64efdb436b518b8222" 80 | dependencies: 81 | core-js "^2.4.0" 82 | regenerator-runtime "^0.9.5" 83 | 84 | balanced-match@^0.4.1: 85 | version "0.4.2" 86 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 87 | 88 | bcrypt-pbkdf@^1.0.0: 89 | version "1.0.0" 90 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" 91 | dependencies: 92 | tweetnacl "^0.14.3" 93 | 94 | bl@^1.0.0, bl@~1.1.2: 95 | version "1.1.2" 96 | resolved "https://registry.yarnpkg.com/bl/-/bl-1.1.2.tgz#fdca871a99713aa00d19e3bbba41c44787a65398" 97 | dependencies: 98 | readable-stream "~2.0.5" 99 | 100 | block-stream@*: 101 | version "0.0.9" 102 | resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" 103 | dependencies: 104 | inherits "~2.0.0" 105 | 106 | boom@2.x.x: 107 | version "2.10.1" 108 | resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" 109 | dependencies: 110 | hoek "2.x.x" 111 | 112 | brace-expansion@^1.0.0: 113 | version "1.1.6" 114 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" 115 | dependencies: 116 | balanced-match "^0.4.1" 117 | concat-map "0.0.1" 118 | 119 | buffer-shims@^1.0.0: 120 | version "1.0.0" 121 | resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" 122 | 123 | builtin-modules@^1.0.0: 124 | version "1.1.1" 125 | resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" 126 | 127 | bytes@^2.4.0: 128 | version "2.4.0" 129 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" 130 | 131 | camelcase@^3.0.0: 132 | version "3.0.0" 133 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" 134 | 135 | caseless@~0.11.0: 136 | version "0.11.0" 137 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" 138 | 139 | chalk@^1.1.1: 140 | version "1.1.3" 141 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 142 | dependencies: 143 | ansi-styles "^2.2.1" 144 | escape-string-regexp "^1.0.2" 145 | has-ansi "^2.0.0" 146 | strip-ansi "^3.0.0" 147 | supports-color "^2.0.0" 148 | 149 | clone@^1.0.2: 150 | version "1.0.2" 151 | resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" 152 | 153 | cmd-shim@^2.0.1: 154 | version "2.0.2" 155 | resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" 156 | dependencies: 157 | graceful-fs "^4.1.2" 158 | mkdirp "~0.5.0" 159 | 160 | code-point-at@^1.0.0: 161 | version "1.0.1" 162 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.0.1.tgz#1104cd34f9b5b45d3eba88f1babc1924e1ce35fb" 163 | dependencies: 164 | number-is-nan "^1.0.0" 165 | 166 | combined-stream@^1.0.5, combined-stream@~1.0.5: 167 | version "1.0.5" 168 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 169 | dependencies: 170 | delayed-stream "~1.0.0" 171 | 172 | commander@^2.9.0: 173 | version "2.9.0" 174 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 175 | dependencies: 176 | graceful-readlink ">= 1.0.0" 177 | 178 | concat-map@0.0.1: 179 | version "0.0.1" 180 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 181 | 182 | console-control-strings@^1.0.0, console-control-strings@~1.1.0: 183 | version "1.1.0" 184 | resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 185 | 186 | core-js@^2.4.0: 187 | version "2.4.1" 188 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" 189 | 190 | core-util-is@~1.0.0: 191 | version "1.0.2" 192 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 193 | 194 | cryptiles@2.x.x: 195 | version "2.0.5" 196 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" 197 | dependencies: 198 | boom "2.x.x" 199 | 200 | currently-unhandled@^0.4.1: 201 | version "0.4.1" 202 | resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" 203 | dependencies: 204 | array-find-index "^1.0.1" 205 | 206 | d@^0.1.1, d@~0.1.1: 207 | version "0.1.1" 208 | resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" 209 | dependencies: 210 | es5-ext "~0.10.2" 211 | 212 | dashdash@^1.12.0: 213 | version "1.14.0" 214 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.0.tgz#29e486c5418bf0f356034a993d51686a33e84141" 215 | dependencies: 216 | assert-plus "^1.0.0" 217 | 218 | death@^1.0.0: 219 | version "1.0.0" 220 | resolved "https://registry.yarnpkg.com/death/-/death-1.0.0.tgz#4d46e15488d4b636b699f0671b04632d752fd2de" 221 | 222 | debug@^2.2.0: 223 | version "2.2.0" 224 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" 225 | dependencies: 226 | ms "0.7.1" 227 | 228 | defaults@^1.0.3: 229 | version "1.0.3" 230 | resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" 231 | dependencies: 232 | clone "^1.0.2" 233 | 234 | delayed-stream@~1.0.0: 235 | version "1.0.0" 236 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 237 | 238 | delegates@^1.0.0: 239 | version "1.0.0" 240 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 241 | 242 | detect-indent@^4.0.0: 243 | version "4.0.0" 244 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" 245 | dependencies: 246 | repeating "^2.0.0" 247 | 248 | diff@^2.2.1: 249 | version "2.2.3" 250 | resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" 251 | 252 | doctrine@^1.2.2: 253 | version "1.5.0" 254 | resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" 255 | dependencies: 256 | esutils "^2.0.2" 257 | isarray "^1.0.0" 258 | 259 | ecc-jsbn@~0.1.1: 260 | version "0.1.1" 261 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" 262 | dependencies: 263 | jsbn "~0.1.0" 264 | 265 | end-of-stream@^1.0.0: 266 | version "1.1.0" 267 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.1.0.tgz#e9353258baa9108965efc41cb0ef8ade2f3cfb07" 268 | dependencies: 269 | once "~1.3.0" 270 | 271 | entities@~1.1.1: 272 | version "1.1.1" 273 | resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" 274 | 275 | err-code@^1.0.0: 276 | version "1.1.1" 277 | resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.1.tgz#739d71b6851f24d050ea18c79a5b722420771d59" 278 | 279 | es5-ext@^0.10.7, es5-ext@~0.10.11, es5-ext@~0.10.2: 280 | version "0.10.12" 281 | resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" 282 | dependencies: 283 | es6-iterator "2" 284 | es6-symbol "~3.1" 285 | 286 | es6-iterator@2: 287 | version "2.0.0" 288 | resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" 289 | dependencies: 290 | d "^0.1.1" 291 | es5-ext "^0.10.7" 292 | es6-symbol "3" 293 | 294 | es6-symbol@^3.0.2, es6-symbol@~3.1, es6-symbol@3: 295 | version "3.1.0" 296 | resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" 297 | dependencies: 298 | d "~0.1.1" 299 | es5-ext "~0.10.11" 300 | 301 | escape-string-regexp@^1.0.2: 302 | version "1.0.5" 303 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 304 | 305 | eslint-plugin-react@5.2.2: 306 | version "5.2.2" 307 | resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-5.2.2.tgz#7db068e1f5487f6871e4deef36a381c303eac161" 308 | dependencies: 309 | doctrine "^1.2.2" 310 | jsx-ast-utils "^1.2.1" 311 | 312 | esutils@^2.0.2: 313 | version "2.0.2" 314 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 315 | 316 | extend@^3.0.0, extend@~3.0.0: 317 | version "3.0.0" 318 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" 319 | 320 | extsprintf@1.0.2: 321 | version "1.0.2" 322 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" 323 | 324 | forever-agent@~0.6.1: 325 | version "0.6.1" 326 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 327 | 328 | form-data@~2.0.0: 329 | version "2.0.0" 330 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.0.0.tgz#6f0aebadcc5da16c13e1ecc11137d85f9b883b25" 331 | dependencies: 332 | asynckit "^0.4.0" 333 | combined-stream "^1.0.5" 334 | mime-types "^2.1.11" 335 | 336 | fs.realpath@^1.0.0: 337 | version "1.0.0" 338 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 339 | 340 | fstream@^1.0.0, fstream@^1.0.2: 341 | version "1.0.10" 342 | resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822" 343 | dependencies: 344 | graceful-fs "^4.1.2" 345 | inherits "~2.0.0" 346 | mkdirp ">=0.5 0" 347 | rimraf "2" 348 | 349 | gauge@~2.6.0: 350 | version "2.6.0" 351 | resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.6.0.tgz#d35301ad18e96902b4751dcbbe40f4218b942a46" 352 | dependencies: 353 | aproba "^1.0.3" 354 | console-control-strings "^1.0.0" 355 | has-color "^0.1.7" 356 | has-unicode "^2.0.0" 357 | object-assign "^4.1.0" 358 | signal-exit "^3.0.0" 359 | string-width "^1.0.1" 360 | strip-ansi "^3.0.1" 361 | wide-align "^1.1.0" 362 | 363 | generate-function@^2.0.0: 364 | version "2.0.0" 365 | resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" 366 | 367 | generate-object-property@^1.1.0: 368 | version "1.2.0" 369 | resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" 370 | dependencies: 371 | is-property "^1.0.0" 372 | 373 | getpass@^0.1.1: 374 | version "0.1.6" 375 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" 376 | dependencies: 377 | assert-plus "^1.0.0" 378 | 379 | glob, glob@^7.0.3, glob@^7.0.5: 380 | version "7.1.1" 381 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" 382 | dependencies: 383 | fs.realpath "^1.0.0" 384 | inflight "^1.0.4" 385 | inherits "2" 386 | minimatch "^3.0.2" 387 | once "^1.3.0" 388 | path-is-absolute "^1.0.0" 389 | 390 | graceful-fs@^4.1.2: 391 | version "4.1.9" 392 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.9.tgz#baacba37d19d11f9d146d3578bc99958c3787e29" 393 | 394 | "graceful-readlink@>= 1.0.0": 395 | version "1.0.1" 396 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 397 | 398 | har-validator@~2.0.6: 399 | version "2.0.6" 400 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" 401 | dependencies: 402 | chalk "^1.1.1" 403 | commander "^2.9.0" 404 | is-my-json-valid "^2.12.4" 405 | pinkie-promise "^2.0.0" 406 | 407 | has-ansi@^2.0.0: 408 | version "2.0.0" 409 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 410 | dependencies: 411 | ansi-regex "^2.0.0" 412 | 413 | has-color@^0.1.7: 414 | version "0.1.7" 415 | resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" 416 | 417 | has-unicode@^2.0.0: 418 | version "2.0.1" 419 | resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 420 | 421 | hawk@~3.1.3: 422 | version "3.1.3" 423 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" 424 | dependencies: 425 | boom "2.x.x" 426 | cryptiles "2.x.x" 427 | hoek "2.x.x" 428 | sntp "1.x.x" 429 | 430 | hoek@2.x.x: 431 | version "2.16.3" 432 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 433 | 434 | http-signature@~1.1.0: 435 | version "1.1.1" 436 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" 437 | dependencies: 438 | assert-plus "^0.2.0" 439 | jsprim "^1.2.2" 440 | sshpk "^1.7.0" 441 | 442 | inflight@^1.0.4: 443 | version "1.0.6" 444 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 445 | dependencies: 446 | once "^1.3.0" 447 | wrappy "1" 448 | 449 | inherits@~2.0.0, inherits@~2.0.1, inherits@2: 450 | version "2.0.3" 451 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 452 | 453 | ini@^1.3.4: 454 | version "1.3.4" 455 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" 456 | 457 | invariant@^2.2.0: 458 | version "2.2.1" 459 | resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.1.tgz#b097010547668c7e337028ebe816ebe36c8a8d54" 460 | dependencies: 461 | loose-envify "^1.0.0" 462 | 463 | is-builtin-module@^1.0.0: 464 | version "1.0.0" 465 | resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" 466 | dependencies: 467 | builtin-modules "^1.0.0" 468 | 469 | is-finite@^1.0.0: 470 | version "1.0.2" 471 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" 472 | dependencies: 473 | number-is-nan "^1.0.0" 474 | 475 | is-fullwidth-code-point@^1.0.0: 476 | version "1.0.0" 477 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 478 | dependencies: 479 | number-is-nan "^1.0.0" 480 | 481 | is-my-json-valid@^2.12.4: 482 | version "2.15.0" 483 | resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" 484 | dependencies: 485 | generate-function "^2.0.0" 486 | generate-object-property "^1.1.0" 487 | jsonpointer "^4.0.0" 488 | xtend "^4.0.0" 489 | 490 | is-property@^1.0.0: 491 | version "1.0.2" 492 | resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" 493 | 494 | is-typedarray@~1.0.0: 495 | version "1.0.0" 496 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 497 | 498 | is-utf8@^0.2.0: 499 | version "0.2.1" 500 | resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" 501 | 502 | isarray@^1.0.0, isarray@~1.0.0: 503 | version "1.0.0" 504 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 505 | 506 | isexe@^1.1.1: 507 | version "1.1.2" 508 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" 509 | 510 | isstream@~0.1.2: 511 | version "0.1.2" 512 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 513 | 514 | jodid25519@^1.0.0: 515 | version "1.0.2" 516 | resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" 517 | dependencies: 518 | jsbn "~0.1.0" 519 | 520 | js-tokens@^1.0.1: 521 | version "1.0.3" 522 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-1.0.3.tgz#14e56eb68c8f1a92c43d59f5014ec29dc20f2ae1" 523 | 524 | jsbn@~0.1.0: 525 | version "0.1.0" 526 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" 527 | 528 | json-schema@0.2.3: 529 | version "0.2.3" 530 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 531 | 532 | json-stringify-safe@~5.0.1: 533 | version "5.0.1" 534 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 535 | 536 | jsonpointer@^4.0.0: 537 | version "4.0.0" 538 | resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" 539 | 540 | jsprim@^1.2.2: 541 | version "1.3.1" 542 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" 543 | dependencies: 544 | extsprintf "1.0.2" 545 | json-schema "0.2.3" 546 | verror "1.3.6" 547 | 548 | jsx-ast-utils@^1.2.1: 549 | version "1.3.2" 550 | resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.3.2.tgz#dff658782705352111f9865d40471bc4a955961e" 551 | dependencies: 552 | acorn-jsx "^3.0.1" 553 | object-assign "^4.1.0" 554 | 555 | leven@^2.0.0: 556 | version "2.0.0" 557 | resolved "https://registry.yarnpkg.com/leven/-/leven-2.0.0.tgz#74c45744439550da185801912829f61d22071bc1" 558 | 559 | linkify-it@^2.0.0: 560 | version "2.0.2" 561 | resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.2.tgz#994629a4adfa5a7d34e08c075611575ab9b6fcfc" 562 | dependencies: 563 | uc.micro "^1.0.1" 564 | 565 | loose-envify@^1.0.0: 566 | version "1.2.0" 567 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.2.0.tgz#69a65aad3de542cf4ee0f4fe74e8e33c709ccb0f" 568 | dependencies: 569 | js-tokens "^1.0.1" 570 | 571 | loud-rejection@^1.2.0: 572 | version "1.6.0" 573 | resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" 574 | dependencies: 575 | currently-unhandled "^0.4.1" 576 | signal-exit "^3.0.0" 577 | 578 | markdown-it@^8.0.1: 579 | version "8.0.1" 580 | resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.0.1.tgz#ff60e2103d17896cb6c57407baa9766f916495cb" 581 | dependencies: 582 | argparse "^1.0.7" 583 | entities "~1.1.1" 584 | linkify-it "^2.0.0" 585 | mdurl "^1.0.1" 586 | uc.micro "^1.0.3" 587 | 588 | markdownlint: 589 | version "0.3.0" 590 | resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.3.0.tgz#af428c09cf6a5dcc32fa561161d43981d6609bf2" 591 | dependencies: 592 | markdown-it "^8.0.1" 593 | 594 | mdurl@^1.0.1: 595 | version "1.0.1" 596 | resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" 597 | 598 | mime-db@~1.24.0: 599 | version "1.24.0" 600 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.24.0.tgz#e2d13f939f0016c6e4e9ad25a8652f126c467f0c" 601 | 602 | mime-types@^2.1.11, mime-types@~2.1.7: 603 | version "2.1.12" 604 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.12.tgz#152ba256777020dd4663f54c2e7bc26381e71729" 605 | dependencies: 606 | mime-db "~1.24.0" 607 | 608 | minimatch@^3.0.2, minimatch@^3.0.3: 609 | version "3.0.3" 610 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" 611 | dependencies: 612 | brace-expansion "^1.0.0" 613 | 614 | minimist@0.0.8: 615 | version "0.0.8" 616 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 617 | 618 | mkdirp@^0.5.0, mkdirp@^0.5.1, "mkdirp@>=0.5 0", mkdirp@~0.5.0: 619 | version "0.5.1" 620 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 621 | dependencies: 622 | minimist "0.0.8" 623 | 624 | ms@0.7.1: 625 | version "0.7.1" 626 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" 627 | 628 | mute-stream@~0.0.4: 629 | version "0.0.6" 630 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" 631 | 632 | node-emoji@^1.0.4: 633 | version "1.4.1" 634 | resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.4.1.tgz#c9fa0cf91094335bcb967a6f42b2305c15af2ebc" 635 | dependencies: 636 | string.prototype.codepointat "^0.2.0" 637 | 638 | node-gyp@^3.2.1: 639 | version "3.4.0" 640 | resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.4.0.tgz#dda558393b3ecbbe24c9e6b8703c71194c63fa36" 641 | dependencies: 642 | fstream "^1.0.0" 643 | glob "^7.0.3" 644 | graceful-fs "^4.1.2" 645 | minimatch "^3.0.2" 646 | mkdirp "^0.5.0" 647 | nopt "2 || 3" 648 | npmlog "0 || 1 || 2 || 3" 649 | osenv "0" 650 | path-array "^1.0.0" 651 | request "2" 652 | rimraf "2" 653 | semver "2.x || 3.x || 4 || 5" 654 | tar "^2.0.0" 655 | which "1" 656 | 657 | node-uuid@~1.4.7: 658 | version "1.4.7" 659 | resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f" 660 | 661 | "nopt@2 || 3": 662 | version "3.0.6" 663 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" 664 | dependencies: 665 | abbrev "1" 666 | 667 | "npmlog@0 || 1 || 2 || 3": 668 | version "3.1.2" 669 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-3.1.2.tgz#2d46fa874337af9498a2f12bb43d8d0be4a36873" 670 | dependencies: 671 | are-we-there-yet "~1.1.2" 672 | console-control-strings "~1.1.0" 673 | gauge "~2.6.0" 674 | set-blocking "~2.0.0" 675 | 676 | number-is-nan@^1.0.0: 677 | version "1.0.1" 678 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 679 | 680 | oauth-sign@~0.8.1: 681 | version "0.8.2" 682 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 683 | 684 | object-assign@^4.1.0: 685 | version "4.1.0" 686 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" 687 | 688 | object-path@^0.11.2: 689 | version "0.11.2" 690 | resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.2.tgz#74bf3b3c5a7f2024d75e333f12021353fa9d485e" 691 | 692 | once@^1.3.0: 693 | version "1.4.0" 694 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 695 | dependencies: 696 | wrappy "1" 697 | 698 | once@~1.3.0: 699 | version "1.3.3" 700 | resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" 701 | dependencies: 702 | wrappy "1" 703 | 704 | os-homedir@^1.0.0: 705 | version "1.0.2" 706 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 707 | 708 | os-tmpdir@^1.0.0: 709 | version "1.0.2" 710 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 711 | 712 | osenv@0: 713 | version "0.1.3" 714 | resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.3.tgz#83cf05c6d6458fc4d5ac6362ea325d92f2754217" 715 | dependencies: 716 | os-homedir "^1.0.0" 717 | os-tmpdir "^1.0.0" 718 | 719 | path-array@^1.0.0: 720 | version "1.0.1" 721 | resolved "https://registry.yarnpkg.com/path-array/-/path-array-1.0.1.tgz#7e2f0f35f07a2015122b868b7eac0eb2c4fec271" 722 | dependencies: 723 | array-index "^1.0.0" 724 | 725 | path-is-absolute@^1.0.0: 726 | version "1.0.1" 727 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 728 | 729 | pinkie-promise@^2.0.0: 730 | version "2.0.1" 731 | resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" 732 | dependencies: 733 | pinkie "^2.0.0" 734 | 735 | pinkie@^2.0.0: 736 | version "2.0.4" 737 | resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" 738 | 739 | process-nextick-args@~1.0.6: 740 | version "1.0.7" 741 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 742 | 743 | proper-lockfile@^1.1.3: 744 | version "1.2.0" 745 | resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-1.2.0.tgz#ceff5dd89d3e5f10fb75e1e8e76bc75801a59c34" 746 | dependencies: 747 | err-code "^1.0.0" 748 | extend "^3.0.0" 749 | graceful-fs "^4.1.2" 750 | retry "^0.10.0" 751 | 752 | qs@~6.2.0: 753 | version "6.2.1" 754 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" 755 | 756 | read@^1.0.7: 757 | version "1.0.7" 758 | resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" 759 | dependencies: 760 | mute-stream "~0.0.4" 761 | 762 | readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13": 763 | version "2.1.5" 764 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" 765 | dependencies: 766 | buffer-shims "^1.0.0" 767 | core-util-is "~1.0.0" 768 | inherits "~2.0.1" 769 | isarray "~1.0.0" 770 | process-nextick-args "~1.0.6" 771 | string_decoder "~0.10.x" 772 | util-deprecate "~1.0.1" 773 | 774 | readable-stream@~2.0.5: 775 | version "2.0.6" 776 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" 777 | dependencies: 778 | core-util-is "~1.0.0" 779 | inherits "~2.0.1" 780 | isarray "~1.0.0" 781 | process-nextick-args "~1.0.6" 782 | string_decoder "~0.10.x" 783 | util-deprecate "~1.0.1" 784 | 785 | regenerator-runtime@^0.9.5: 786 | version "0.9.5" 787 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz#403d6d40a4bdff9c330dd9392dcbb2d9a8bba1fc" 788 | 789 | repeating@^2.0.0: 790 | version "2.0.1" 791 | resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 792 | dependencies: 793 | is-finite "^1.0.0" 794 | 795 | request-capture-har@^1.1.4: 796 | version "1.1.4" 797 | resolved "https://registry.yarnpkg.com/request-capture-har/-/request-capture-har-1.1.4.tgz#e6ad76eb8e7a1714553fdbeef32cd4518e4e2013" 798 | 799 | request@^2.75.0, request@2: 800 | version "2.75.0" 801 | resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93" 802 | dependencies: 803 | aws-sign2 "~0.6.0" 804 | aws4 "^1.2.1" 805 | bl "~1.1.2" 806 | caseless "~0.11.0" 807 | combined-stream "~1.0.5" 808 | extend "~3.0.0" 809 | forever-agent "~0.6.1" 810 | form-data "~2.0.0" 811 | har-validator "~2.0.6" 812 | hawk "~3.1.3" 813 | http-signature "~1.1.0" 814 | is-typedarray "~1.0.0" 815 | isstream "~0.1.2" 816 | json-stringify-safe "~5.0.1" 817 | mime-types "~2.1.7" 818 | node-uuid "~1.4.7" 819 | oauth-sign "~0.8.1" 820 | qs "~6.2.0" 821 | stringstream "~0.0.4" 822 | tough-cookie "~2.3.0" 823 | tunnel-agent "~0.4.1" 824 | 825 | retry@^0.10.0: 826 | version "0.10.0" 827 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.0.tgz#649e15ca408422d98318161935e7f7d652d435dd" 828 | 829 | rimraf@^2.5.0, rimraf@2: 830 | version "2.5.4" 831 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" 832 | dependencies: 833 | glob "^7.0.5" 834 | 835 | roadrunner@^1.1.0: 836 | version "1.1.0" 837 | resolved "https://registry.yarnpkg.com/roadrunner/-/roadrunner-1.1.0.tgz#1180a30d64e1970d8f55dd8cb0da8ffccecad71e" 838 | 839 | semver@^5.1.0, "semver@2.x || 3.x || 4 || 5": 840 | version "5.3.0" 841 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" 842 | 843 | set-blocking@~2.0.0: 844 | version "2.0.0" 845 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 846 | 847 | signal-exit@^3.0.0: 848 | version "3.0.1" 849 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81" 850 | 851 | sntp@1.x.x: 852 | version "1.0.9" 853 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" 854 | dependencies: 855 | hoek "2.x.x" 856 | 857 | spdx-correct@~1.0.0: 858 | version "1.0.2" 859 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" 860 | dependencies: 861 | spdx-license-ids "^1.0.2" 862 | 863 | spdx-expression-parse@~1.0.0: 864 | version "1.0.4" 865 | resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" 866 | 867 | spdx-license-ids@^1.0.2: 868 | version "1.2.2" 869 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" 870 | 871 | sprintf-js@~1.0.2: 872 | version "1.0.3" 873 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 874 | 875 | sshpk@^1.7.0: 876 | version "1.10.1" 877 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" 878 | dependencies: 879 | asn1 "~0.2.3" 880 | assert-plus "^1.0.0" 881 | dashdash "^1.12.0" 882 | getpass "^0.1.1" 883 | optionalDependencies: 884 | bcrypt-pbkdf "^1.0.0" 885 | ecc-jsbn "~0.1.1" 886 | jodid25519 "^1.0.0" 887 | jsbn "~0.1.0" 888 | tweetnacl "~0.14.0" 889 | 890 | string_decoder@~0.10.x: 891 | version "0.10.31" 892 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 893 | 894 | string-width@^1.0.1: 895 | version "1.0.2" 896 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 897 | dependencies: 898 | code-point-at "^1.0.0" 899 | is-fullwidth-code-point "^1.0.0" 900 | strip-ansi "^3.0.0" 901 | 902 | string.prototype.codepointat@^0.2.0: 903 | version "0.2.0" 904 | resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" 905 | 906 | stringstream@~0.0.4: 907 | version "0.0.5" 908 | resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" 909 | 910 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 911 | version "3.0.1" 912 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 913 | dependencies: 914 | ansi-regex "^2.0.0" 915 | 916 | strip-bom@^2.0.0: 917 | version "2.0.0" 918 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 919 | dependencies: 920 | is-utf8 "^0.2.0" 921 | 922 | supports-color@^2.0.0: 923 | version "2.0.0" 924 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 925 | 926 | tar-stream@^1.5.2: 927 | version "1.5.2" 928 | resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.2.tgz#fbc6c6e83c1a19d4cb48c7d96171fc248effc7bf" 929 | dependencies: 930 | bl "^1.0.0" 931 | end-of-stream "^1.0.0" 932 | readable-stream "^2.0.0" 933 | xtend "^4.0.0" 934 | 935 | tar@^2.0.0, tar@^2.2.1: 936 | version "2.2.1" 937 | resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" 938 | dependencies: 939 | block-stream "*" 940 | fstream "^1.0.2" 941 | inherits "2" 942 | 943 | tough-cookie@~2.3.0: 944 | version "2.3.1" 945 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.1.tgz#99c77dfbb7d804249e8a299d4cb0fd81fef083fd" 946 | 947 | tunnel-agent@~0.4.1: 948 | version "0.4.3" 949 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" 950 | 951 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 952 | version "0.14.3" 953 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.3.tgz#3da382f670f25ded78d7b3d1792119bca0b7132d" 954 | 955 | uc.micro@^1.0.1, uc.micro@^1.0.3: 956 | version "1.0.3" 957 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" 958 | 959 | user-home@^2.0.0: 960 | version "2.0.0" 961 | resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" 962 | dependencies: 963 | os-homedir "^1.0.0" 964 | 965 | util-deprecate@~1.0.1: 966 | version "1.0.2" 967 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 968 | 969 | validate-npm-package-license@^3.0.1: 970 | version "3.0.1" 971 | resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" 972 | dependencies: 973 | spdx-correct "~1.0.0" 974 | spdx-expression-parse "~1.0.0" 975 | 976 | verror@1.3.6: 977 | version "1.3.6" 978 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" 979 | dependencies: 980 | extsprintf "1.0.2" 981 | 982 | which@1: 983 | version "1.2.11" 984 | resolved "https://registry.yarnpkg.com/which/-/which-1.2.11.tgz#c8b2eeea6b8c1659fa7c1dd4fdaabe9533dc5e8b" 985 | dependencies: 986 | isexe "^1.1.1" 987 | 988 | wide-align@^1.1.0: 989 | version "1.1.0" 990 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" 991 | dependencies: 992 | string-width "^1.0.1" 993 | 994 | wrappy@1: 995 | version "1.0.2" 996 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 997 | 998 | xtend@^4.0.0: 999 | version "4.0.1" 1000 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 1001 | 1002 | yarn@^0.16.1: 1003 | version "0.16.1" 1004 | resolved "https://registry.yarnpkg.com/yarn/-/yarn-0.16.1.tgz#956501387a9e8584aad481355c8612d876bc35d8" 1005 | dependencies: 1006 | babel-runtime "^6.0.0" 1007 | bytes "^2.4.0" 1008 | camelcase "^3.0.0" 1009 | chalk "^1.1.1" 1010 | cmd-shim "^2.0.1" 1011 | commander "^2.9.0" 1012 | death "^1.0.0" 1013 | debug "^2.2.0" 1014 | defaults "^1.0.3" 1015 | detect-indent "^4.0.0" 1016 | diff "^2.2.1" 1017 | eslint-plugin-react "5.2.2" 1018 | ini "^1.3.4" 1019 | invariant "^2.2.0" 1020 | is-builtin-module "^1.0.0" 1021 | leven "^2.0.0" 1022 | loud-rejection "^1.2.0" 1023 | minimatch "^3.0.3" 1024 | mkdirp "^0.5.1" 1025 | node-emoji "^1.0.4" 1026 | node-gyp "^3.2.1" 1027 | object-path "^0.11.2" 1028 | proper-lockfile "^1.1.3" 1029 | read "^1.0.7" 1030 | repeating "^2.0.0" 1031 | request "^2.75.0" 1032 | request-capture-har "^1.1.4" 1033 | rimraf "^2.5.0" 1034 | roadrunner "^1.1.0" 1035 | semver "^5.1.0" 1036 | strip-bom "^2.0.0" 1037 | tar "^2.2.1" 1038 | tar-stream "^1.5.2" 1039 | user-home "^2.0.0" 1040 | validate-npm-package-license "^3.0.1" 1041 | 1042 | --------------------------------------------------------------------------------