From 7e618b23dfa622aa7df4300461f5c2bcaf5db075 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Sun, 28 Aug 2016 16:25:56 +0300 Subject: [PATCH] Hello World --- .babelrc | 9 ++ .eslintrc.json | 213 ++++++++++++++++++++++++++++++++ .gitignore | 2 + README.md | 2 +- package.json | 74 +++++++++++ scripts/.babelrc | 3 + scripts/i18n-collect.js | 183 +++++++++++++++++++++++++++ scripts/package.json | 17 +++ src/favicon.ico | Bin 0 -> 198 bytes src/index.ejs | 24 ++++ src/index.js | 13 ++ webpack-utils/cssUrl.js | 36 ++++++ webpack-utils/intl-loader.js | 21 ++++ webpack-utils/package.json | 10 ++ webpack.config.js | 232 +++++++++++++++++++++++++++++++++++ 15 files changed, 838 insertions(+), 1 deletion(-) create mode 100644 .babelrc create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 package.json create mode 100644 scripts/.babelrc create mode 100644 scripts/i18n-collect.js create mode 100644 scripts/package.json create mode 100644 src/favicon.ico create mode 100644 src/index.ejs create mode 100644 src/index.js create mode 100644 webpack-utils/cssUrl.js create mode 100644 webpack-utils/intl-loader.js create mode 100644 webpack-utils/package.json create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..0afdcb9 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": ["react", "es2015", "stage-0"], + "plugins": ["transform-runtime", ["react-intl", {"messagesDir": "./dist/messages/"}]], + "env": { + "development": { + "presets": ["react-hmre"] + } + } +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..e92b6b9 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,213 @@ +{ + "parser": "babel-eslint", + "plugins": [ + "react" + ], + + "ecmaFeatures": { + "jsx": true, + "modules": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "spread": true, + "arrowFunctions": true, + "blockBindings": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": true, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "restParams": true, + "superInFunctions": true, + "templateStrings": true, + "experimentalObjectRestSpread": true + }, + + "env": { + "browser": true, + "commonjs": true, + "es6": true + }, + + "extends": "eslint:recommended", + + // @see: http://eslint.org/docs/rules/ + "rules": { + // possible errors (including eslint:recommended) + "valid-jsdoc": ["warn", { + "requireParamDescription": false, + "requireReturn": false, + "requireReturnDescription": false, + "prefer": { + "returns": "return" + }, + "preferType": { + "String": "string", + "Object": "object", + "Number": "number", + "Function": "function" + } + }], + + // best practice + "block-scoped-var": "error", + "curly": "error", + "default-case": "error", + "dot-location": ["error", "property"], + "dot-notation": "error", + "eqeqeq": ["error", "smart"], + "no-alert": "error", + "no-caller": "error", + "no-case-declarations": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-pattern": "error", + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "warn", + "no-fallthrough": "error", + "no-floating-decimal": "warn", + "no-implied-eval": "error", + "no-invalid-this": "off", + "no-labels": "error", + "no-lone-blocks": "warn", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-native-reassign": "error", + "no-new-wrappers": "warn", + "no-new": "warn", + "no-octal-escape": "warn", + "no-octal": "error", + "no-proto": "error", + "no-redeclare": "warn", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unused-expressions": ["warn", {"allowShortCircuit": true, "allowTernary": true}], + "no-useless-call": "warn", + "no-useless-concat": "warn", + "no-void": "error", + "no-with": "error", + "radix": "error", + "wrap-iife": "error", + "yoda": "warn", + "no-constant-condition": "error", + + // strict mode + "strict": ["warn", "never"], // babel все сделает за нас + + // variables + "no-catch-shadow": "error", + "no-delete-var": "error", + "no-label-var": "error", + "no-shadow-restricted-names": "error", + "no-shadow": "off", + "no-undef-init": "error", + "no-undef": "error", + "no-use-before-define": ["warn", "nofunc"], + + // CommonJS + "no-mixed-requires": "warn", + "no-path-concat": "warn", + + // stylistic + "array-bracket-spacing": "error", + "block-spacing": ["error", "never"], + "brace-style": ["error", "1tbs", {"allowSingleLine": true}], + "comma-spacing": "error", + "comma-style": "error", + "comma-dangle": ["warn", "only-multiline"], + "computed-property-spacing": "error", + "consistent-this": ["error", "that"], + "camelcase": "warn", + "eol-last": "warn", + "id-length": ["error", {"min": 2, "exceptions": ["x", "y", "i", "$"]}], + "indent": ["error", 4, {"SwitchCase": 1}], + "jsx-quotes": "error", + "key-spacing": ["error", {"mode": "minimum"}], + "linebreak-style": "error", + "max-depth": "error", + "new-cap": "error", + "new-parens": "error", + "no-array-constructor": "warn", + "no-bitwise": "warn", + "no-lonely-if": "error", + "no-negated-condition": "warn", + "no-nested-ternary": "error", + "no-new-object": "error", + "no-spaced-func": "error", + "no-trailing-spaces": "warn", + "no-unneeded-ternary": "warn", + "one-var": ["error", "never"], + "operator-assignment": ["warn", "always"], + "operator-linebreak": ["error", "before"], + "padded-blocks": ["warn", "never"], + "quote-props": ["warn", "as-needed"], + "quotes": ["warn", "single"], + "semi": "error", + "semi-spacing": "error", + "keyword-spacing": "warn", + "space-before-blocks": "error", + "space-before-function-paren": ["error", "never"], + "space-in-parens": "warn", + "space-infix-ops": "error", + "space-unary-ops": "error", + "spaced-comment": "warn", + + // es6 + "arrow-body-style": "warn", + "arrow-parens": "error", + "arrow-spacing": "error", + "constructor-super": "error", + "generator-star-spacing": "warn", + "no-class-assign": "error", + "no-const-assign": "error", + "no-dupe-class-members": "error", + "no-this-before-super": "error", + "no-var": "warn", + "object-shorthand": "warn", + "prefer-arrow-callback": "warn", + "prefer-const": "warn", + "prefer-reflect": "warn", + "prefer-spread": "warn", + "prefer-template": "warn", + "require-yield": "error", + + // react + "react/display-name": "warn", + "react/forbid-prop-types": "warn", + "react/jsx-boolean-value": "warn", + "react/jsx-closing-bracket-location": "warn", + "react/jsx-curly-spacing": "warn", + "react/jsx-handler-names": ["warn", {"eventHandlerPrefix": "on", "eventHandlerPropPrefix": "on"}], + "react/jsx-indent-props": "warn", + "react/jsx-key": "warn", + "react/jsx-max-props-per-line": ["warn", {"maximum": 3}], + "react/jsx-no-bind": "warn", + "react/jsx-no-duplicate-props": "warn", + "react/jsx-no-literals": "warn", + "react/jsx-no-undef": "warn", + "react/jsx-pascal-case": "warn", + "react/jsx-uses-react": "warn", + "react/jsx-uses-vars": "warn", + "react/jsx-no-comment-textnodes": "warn", + "react/jsx-wrap-multilines": "warn", + "react/no-deprecated": "warn", + "react/no-did-mount-set-state": "warn", + "react/no-did-update-set-state": "warn", + "react/no-direct-mutation-state": "warn", + "react/require-render-return": "warn", + "react/no-is-mounted": "warn", + "react/no-multi-comp": "warn", + "react/no-string-refs": "warn", + "react/no-unknown-property": "warn", + "react/prefer-es6-class": "warn", + "react/prop-types": "warn", + "react/react-in-jsx-scope": "warn", + "react/self-closing-comp": "warn", + "react/sort-comp": ["warn", {"order": ["lifecycle", "render", "everything-else"]}] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/README.md b/README.md index b1b7161..2965834 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -init +# Hello World diff --git a/package.json b/package.json new file mode 100644 index 0000000..77de66b --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "email-renderer", + "version": "1.0.0", + "description": "", + "main": "src/index.js", + "repository": { + "type": "git", + "url": "git+ssh://git@gitlab.com/elyby/email-renderer.git" + }, + "keywords": [], + "author": "SleepWalker ", + "license": "private", + "bugs": { + "url": "https://gitlab.com/elyby/email-renderer/issues" + }, + "homepage": "https://gitlab.com/elyby/email-renderer#README", + "scripts": { + "start": "rm -rf dist/ && webpack-dev-server --progress --colors", + "up": "npm update", + "test": "karma start ./karma.conf.js", + "lint": "eslint ./src", + "i18n": "cd ./scripts && ./node_modules/.bin/babel-node i18n-collect.js", + "build": "rm -rf dist/ && webpack --progress --colors -p", + "render": "" + }, + "dependencies": { + "babel-polyfill": "^6.3.14", + "classnames": "^2.1.3", + "history": "^2.0.0", + "intl": "^1.2.2", + "intl-format-cache": "^2.0.4", + "intl-messageformat": "^1.1.0", + "react": "^15.0.0", + "react-dom": "^15.0.0", + "react-intl": "^2.0.0" + }, + "devDependencies": { + "babel-core": "^6.0.0", + "babel-eslint": "^6.0.0", + "babel-loader": "^6.0.0", + "babel-plugin-react-intl": "^2.0.0", + "babel-plugin-transform-runtime": "^6.3.13", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-react-hmre": "^1.0.1", + "babel-preset-stage-0": "^6.3.13", + "babel-runtime": "^6.0.0", + "bundle-loader": "^0.5.4", + "css-loader": "^0.23.0", + "eslint": "^3.1.1", + "eslint-plugin-react": "^6.0.0", + "exports-loader": "^0.6.3", + "extract-text-webpack-plugin": "^1.0.0", + "file-loader": "^0.9.0", + "html-loader": "^0.4.3", + "html-webpack-plugin": "^2.0.0", + "imports-loader": "^0.6.5", + "json-loader": "^0.5.4", + "loader-utils": "^0.2.15", + "node-sass": "^3.4.2", + "postcss-import": "^8.1.2", + "postcss-loader": "^0.9.0", + "postcss-scss": "^0.1.8", + "postcss-url": "SleepWalker/postcss-url#switch-to-async-api", + "raw-loader": "^0.5.1", + "sass-loader": "^4.0.0", + "scripts": "file:scripts", + "style-loader": "^0.13.0", + "url-loader": "^0.5.7", + "webpack": "^1.12.9", + "webpack-dev-server": "^1.14.0", + "webpack-utils": "file:webpack-utils" + } +} diff --git a/scripts/.babelrc b/scripts/.babelrc new file mode 100644 index 0000000..0522a89 --- /dev/null +++ b/scripts/.babelrc @@ -0,0 +1,3 @@ +{ + "breakConfig": true +} diff --git a/scripts/i18n-collect.js b/scripts/i18n-collect.js new file mode 100644 index 0000000..10c2e44 --- /dev/null +++ b/scripts/i18n-collect.js @@ -0,0 +1,183 @@ +/* eslint-disable no-console */ +import fs from 'fs'; +import {sync as globSync} from 'glob'; +import {sync as mkdirpSync} from 'mkdirp'; +import chalk from 'chalk'; +import prompt from 'prompt'; + +const MESSAGES_PATTERN = '../dist/messages/**/*.json'; +const LANG_DIR = '../src/i18n'; +const DEFAULT_LOCALE = 'en'; +const SUPPORTED_LANGS = [DEFAULT_LOCALE].concat('ru', 'be', 'uk'); + +/** + * Aggregates the default messages that were extracted from the app's + * React components via the React Intl Babel plugin. An error will be thrown if + * there are messages in different components that use the same `id`. The result + * is a flat collection of `id: message` pairs for the app's default locale. + */ +let idToFileMap = {}; +let duplicateIds = []; +const collectedMessages = globSync(MESSAGES_PATTERN) + .map((filename) => [filename, JSON.parse(fs.readFileSync(filename, 'utf8'))]) + .reduce((collection, [file, descriptors]) => { + descriptors.forEach(({id, defaultMessage}) => { + if (collection.hasOwnProperty(id)) { + duplicateIds.push(id); + } + + collection[id] = defaultMessage; + idToFileMap[id] = (idToFileMap[id] || []).concat(file); + }); + + return collection; + }, {}); + +if (duplicateIds.length) { + console.log('\nFound duplicated ids:'); + duplicateIds.forEach((id) => console.log(`${chalk.yellow(id)}:\n - ${idToFileMap[id].join('\n - ')}\n`)); + console.log(chalk.red('Please correct the errors above to proceed further!')); + return; +} + +duplicateIds = null; +idToFileMap = null; + +/** + * Making a diff with the previous DEFAULT_LOCALE version + */ +const defaultMessagesPath = `${LANG_DIR}/${DEFAULT_LOCALE}.json`; +let keysToUpdate = []; +let keysToAdd = []; +let keysToRemove = []; +const keysToRename = []; +const isNotMarked = (value) => value.slice(0, 2) !== '--'; +try { + const prevMessages = JSON.parse(fs.readFileSync(defaultMessagesPath, 'utf8')); + const prevMessagesMap = Object.entries(prevMessages).reduce((acc, [key, value]) => { + if (acc[value]) { + acc[value].push(key); + } else { + acc[value] = [key]; + } + + return acc; + }, {}); + keysToAdd = Object.keys(collectedMessages).filter((key) => !prevMessages[key]); + keysToRemove = Object.keys(prevMessages).filter((key) => !collectedMessages[key]).filter(isNotMarked); + keysToUpdate = Object.entries(prevMessages).reduce((acc, [key, message]) => + acc.concat(collectedMessages[key] && collectedMessages[key] !== message ? key : []) + , []); + + // detect keys to rename, mutating keysToAdd and keysToRemove + [].concat(keysToAdd).forEach((toKey) => { + const keys = prevMessagesMap[collectedMessages[toKey]] || []; + const fromKey = keys.find((fromKey) => keysToRemove.indexOf(fromKey) > -1); + + if (fromKey) { + keysToRename.push([fromKey, toKey]); + + keysToRemove.splice(keysToRemove.indexOf(fromKey), 1); + keysToAdd.splice(keysToAdd.indexOf(toKey), 1); + } + }); +} catch (err) { + console.log(chalk.yellow(`Can not read ${defaultMessagesPath}. The new file will be created.`), err); +} + +if (!keysToAdd.length && !keysToRemove.length && !keysToUpdate.length && !keysToRename.length) { + return console.log(chalk.green('Everything is up to date!')); +} + +console.log(chalk.magenta(`The diff relative to default locale (${DEFAULT_LOCALE}) is:`)); + +if (keysToRemove.length) { + console.log('The following keys will be removed:'); + console.log([chalk.red('\n - '), keysToRemove.join(chalk.red('\n - ')), '\n'].join('')); +} + +if (keysToAdd.length) { + console.log('The following keys will be added:'); + console.log([chalk.green('\n + '), keysToAdd.join(chalk.green('\n + ')), '\n'].join('')); +} + +if (keysToUpdate.length) { + console.log('The following keys will be updated:'); + console.log([chalk.yellow('\n @ '), keysToUpdate.join(chalk.yellow('\n @ ')), '\n'].join('')); +} + +if (keysToRename.length) { + console.log('The following keys will be renamed:\n'); + console.log(keysToRename.reduce((str, pair) => + [str, pair[0], chalk.yellow(' -> '), pair[1], '\n'].join('') + , '')); +} + +prompt.start(); +prompt.get({ + properties: { + apply: { + description: 'Apply changes? [Y/n]', + pattern: /^y|n$/i, + message: 'Please enter "y" or "n"', + default: 'y', + before: (value) => value.toLowerCase() === 'y' + } + } +}, (err, resp) => { + console.log('\n'); + + if (err || !resp.apply) { + return console.log(chalk.red('Aborted')); + } + + buildLocales(); + + console.log(chalk.green('All locales was successfuly built')); +}); + + +function buildLocales() { + mkdirpSync(LANG_DIR); + + SUPPORTED_LANGS.map((lang) => { + const destPath = `${LANG_DIR}/${lang}.json`; + + let newMessages = {}; + try { + newMessages = JSON.parse(fs.readFileSync(destPath, 'utf8')); + } catch (err) { + console.log(chalk.yellow(`Can not read ${destPath}. The new file will be created.`), err); + } + + keysToRename.forEach(([fromKey, toKey]) => { + newMessages[toKey] = newMessages[fromKey]; + delete newMessages[fromKey]; + }); + keysToRemove.forEach((key) => { + delete newMessages[key]; + }); + keysToUpdate.forEach((key) => { + newMessages[`--${key}`] = newMessages[key]; + newMessages[key] = collectedMessages[key]; + }); + keysToAdd.forEach((key) => { + newMessages[key] = collectedMessages[key]; + }); + + const sortedKeys = Object.keys(newMessages).sort((key1, key2) => { + key1 = key1.replace(/^\-+/, ''); + key2 = key2.replace(/^\-+/, ''); + + return key1 < key2 || !isNotMarked(key1) ? -1 : 1; + }); + + const sortedNewMessages = sortedKeys.reduce((acc, key) => { + acc[key] = newMessages[key]; + + return acc; + }, {}); + + fs.writeFileSync(destPath, JSON.stringify(sortedNewMessages, null, 4) + '\n'); + }); +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..25bea1e --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,17 @@ +{ + "name": "scripts", + "version": "1.0.0", + "description": "", + "main": "i18n-build.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "chalk": "^1.1.3", + "node-babel": "^0.1.2", + "prompt": "^1.0.0" + } +} diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bae0655702d26fbb17e2f6105316ce6051b77fa6 GIT binary patch literal 198 zcmZQzU<5(|0VXiLfq{WR42U&=SOAC>fEXkX0uEpjLNEa-5RR;4VEF(4Kavas1N#FA RW&?_XF#7}Se4rH7001E53gG|% literal 0 HcmV?d00001 diff --git a/src/index.ejs b/src/index.ejs new file mode 100644 index 0000000..a7f3821 --- /dev/null +++ b/src/index.ejs @@ -0,0 +1,24 @@ + + + + + Ely.by - Email Renderer + + + + <% if (htmlWebpackPlugin.files.favicon) { %> + + <% } %> + + + +
+ +<% for (var css in htmlWebpackPlugin.files.css) { %> + +<% } %> +<% for (var chunk in htmlWebpackPlugin.files.chunks) { %> + +<% } %> + + diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..22c732b --- /dev/null +++ b/src/index.js @@ -0,0 +1,13 @@ +import 'babel-polyfill'; + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { IntlProvider } from 'react-intl'; + +ReactDOM.render( + +
Hello world
+
, + document.getElementById('app') +); diff --git a/webpack-utils/cssUrl.js b/webpack-utils/cssUrl.js new file mode 100644 index 0000000..2362705 --- /dev/null +++ b/webpack-utils/cssUrl.js @@ -0,0 +1,36 @@ +// при использовании sass-loader теряется контекст в импортированных модулях +// из-за чего css-loader не может правильно обработать относительные url +// +// препроцессим урлы перед тем, как пропускать их через sass-loader +// урлы, начинающиеся с / будут оставлены как есть + +const cssUrl = require('postcss-url'); +const loaderUtils = require('loader-utils'); + +// /#.+$/ - strip #hash part of svg font url +const urlToRequest = (url) => loaderUtils.urlToRequest(url.replace(/\??#.+$/, ''), true); +const urlPostfix = (url) => { + var idx = url.indexOf('?#'); + + if (idx < 0) { + idx = url.indexOf('#'); + } + + return idx >= 0 ? url.slice(idx) : ''; +}; + +module.exports = function(loader) { + return cssUrl({ + url: (url, decl, from, dirname, to, options, result) => + new Promise((resolve, reject) => + loaderUtils.isUrlRequest(url) ? loader.loadModule(urlToRequest(url), (err, source) => + err ? reject(err) : resolve( + loader.exec(` + var __webpack_public_path__ = '${loader.options.output.publicPath}'; + ${source} + `) + urlPostfix(url) + ) + ) : resolve(url) + ) + }); +}; diff --git a/webpack-utils/intl-loader.js b/webpack-utils/intl-loader.js new file mode 100644 index 0000000..a6c1a59 --- /dev/null +++ b/webpack-utils/intl-loader.js @@ -0,0 +1,21 @@ +module.exports = function() { + this.cacheable && this.cacheable(); + + var moduleId = this.context + .replace(this.options.resolve.root, '') + .replace(/^\/|\/$/g, '') + .replace(/\//g, '.'); + + var content = this.inputValue[0]; + content = JSON.stringify(Object.keys(content).reduce(function(translations, key) { + translations[key] = { + id: moduleId + '.' + key, + defaultMessage: content[key] + }; + + return translations; + }, {})); + + return 'import { defineMessages } from \'react-intl\';' + + 'export default defineMessages(' + content + ')'; +}; diff --git a/webpack-utils/package.json b/webpack-utils/package.json new file mode 100644 index 0000000..fe35d66 --- /dev/null +++ b/webpack-utils/package.json @@ -0,0 +1,10 @@ +{ + "name": "webpack-utils", + "version": "1.0.0", + "description": "", + "keywords": [], + "author": "", + "dependencies": { + "loader-utils": "^0.2.12" + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..49dc020 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,232 @@ +/* eslint-env node */ + +var path = require('path'); + +var webpack = require('webpack'); +var loaderUtils = require('loader-utils'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var cssUrl = require('webpack-utils/cssUrl'); +var cssImport = require('postcss-import'); + +var vendor = Object.keys(require('./package.json').dependencies); + +const rootPath = path.resolve('./src'); + +const isProduction = process.argv.some((arg) => arg === '-p'); + +process.env.NODE_ENV = isProduction ? 'production' : 'development'; + +const CSS_CLASS_TEMPLATE = isProduction ? '[hash:base64:5]' : '[path][name]-[local]'; + +const fileCache = {}; + + +const cssLoaderQuery = { + modules: true, + importLoaders: 2, + url: false, + localIdentName: CSS_CLASS_TEMPLATE, + + /** + * cssnano options + */ + sourcemap: !isProduction, + autoprefixer: { + add: true, + remove: true, + browsers: ['last 2 versions'] + }, + safe: true, + // отключаем минификацию цветов, что бы она не ломала такие выражения: + // composes: black from './buttons.scss'; + colormin: false, + discardComments: { + removeAll: true + } +}; + +var webpackConfig = { + entry: { + app: path.join(__dirname, 'src'), + vendor: vendor + }, + + output: { + path: path.join(__dirname, 'dist'), + publicPath: '/', + filename: '[name].js?[hash]' + }, + + resolve: { + root: rootPath, + extensions: ['', '.js', '.jsx'] + }, + + devServer: { + host: 'localhost', + port: 8080, + // proxy: { + // '/api*': { + // headers: { + // host: config.apiHost.replace(/https?:|\//g, '') + // }, + // target: config.apiHost + // } + // }, + hot: true, + inline: true, + historyApiFallback: true + }, + + devtool: 'eval', + + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(process.env.NODE_ENV) + }, + __DEV__: !isProduction, + __PROD__: isProduction + }), + new HtmlWebpackPlugin({ + template: 'src/index.ejs', + favicon: 'src/favicon.ico', + hash: isProduction, + filename: 'index.html', + inject: false, + minify: { + collapseWhitespace: isProduction + } + }), + new webpack.ProvidePlugin({ + // window.fetch polyfill + fetch: 'imports?this=>self!exports?self.fetch!whatwg-fetch' + }), + new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js?[hash]') + ].concat(isProduction ? [ + new webpack.optimize.DedupePlugin(), + new webpack.optimize.UglifyJsPlugin() + ] : []), + + module: { + loaders: [ + { + test: /\.scss$/, + extractInProduction: true, + loader: 'style!css?' + JSON.stringify(cssLoaderQuery) + '!sass!postcss?syntax=postcss-scss' + }, + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel' + }, + { + test: /\.(png|gif|jpg|svg)$/, + loader: 'file', + query: { + name: 'assets/[name].[ext]?[hash]' + } + }, + { + test: /\.(woff|woff2|ttf)$/, + loader: 'file', + query: { + name: 'assets/fonts/[name].[ext]?[hash]' + } + + }, + { + test: /\.json$/, + exclude: /(intl|font)\.json/, + loader: 'json' + }, + { + test: /\.html$/, + loader: 'html' + }, + { + test: /\.intl\.json$/, + loader: 'babel!intl!json' + }, + { + test: /\.font\.(js|json)$/, + loader: 'raw!fontgen' + } + ] + }, + + resolveLoader: { + alias: { + intl: path.resolve('webpack-utils/intl-loader') + } + }, + + postcss() { + return [ + cssImport({ + path: rootPath, + addDependencyTo: webpack, + + resolve: ((defaultResolve) => + (url, basedir, importOptions) => + defaultResolve(loaderUtils.urlToRequest(url), basedir, importOptions) + )(require('postcss-import/lib/resolve-id')), + + load: ((defaultLoad) => + (filename, importOptions) => { + if (/\.font.(js|json)$/.test(filename)) { + if (!fileCache[filename] || !isProduction) { + // do not execute loader on the same file twice + // this is an overcome for a bug with ExtractTextPlugin, for isProduction === true + // when @imported files may be processed mutiple times + fileCache[filename] = new Promise((resolve, reject) => + this.loadModule(filename, (err, source) => + err ? reject(err) : resolve(this.exec(source)) + ) + ); + } + + return fileCache[filename]; + } + + return defaultLoad(filename, importOptions); + } + )(require('postcss-import/lib/load-content')) + }), + + cssUrl(this) + ]; + } +}; + +if (isProduction) { + webpackConfig.module.loaders.forEach((loader) => { + if (loader.extractInProduction) { + // remove style-loader from chain and pass through ExtractTextPlugin + const parts = loader.loader.split('!'); + + loader.loader = ExtractTextPlugin.extract( + parts[0], // style-loader + parts.slice(1) // css-loader and rest + .join('!') + .replace(/[&?]sourcemap/, '') + ); + } + }); + + webpackConfig.plugins.push(new ExtractTextPlugin('styles.css', { + allChunks: true + })); + + webpackConfig.devtool = false; +} + +if (!isProduction) { + webpackConfig.plugins.push( + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin() + ); +} + +module.exports = webpackConfig;