dispatch(addTab())} className={style.add} />
24 |
25 |
sendEvent('star', 'click')}
29 | />
30 |
31 | )
32 |
33 | Tabs.propTypes = {
34 | tabs: PropTypes.arrayOf(PropTypes.object),
35 | dispatch: PropTypes.func,
36 | }
37 |
38 | export default Tabs
39 |
--------------------------------------------------------------------------------
/src/components/navigator/index.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import classNames from 'classnames'
3 | import style from './navigator.css'
4 | import Back from '../back'
5 | import Forward from '../forward'
6 | import Reload from '../reload'
7 | import Star from '../star'
8 | import Setting from '../setting'
9 | import Input from './input'
10 | import { load } from '../../actions'
11 |
12 | const handleSubmit = dispatch => (event) => {
13 | event.preventDefault()
14 | return dispatch(load())
15 | }
16 |
17 | const Navigator = ({ url, isInputFocus, input, dispatch }) => (
18 |
19 |
20 |
21 |
22 |
31 |
32 |
33 | )
34 |
35 | Navigator.propTypes = {
36 | url: PropTypes.string.isRequired,
37 | isInputFocus: PropTypes.bool.isRequired,
38 | input: PropTypes.string.isRequired,
39 | dispatch: PropTypes.func.isRequired,
40 | }
41 |
42 | export default Navigator
43 |
--------------------------------------------------------------------------------
/src/reducers/tabs.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_TAB,
3 | CLOSE_TAB,
4 | UPDATE_URL,
5 | LOAD_START,
6 | LOAD_SUCCESS,
7 | LOAD_FAIL,
8 | } from '../actions'
9 | import { getTitle } from '../util'
10 |
11 | export default function tabs(state = {}, action) {
12 | switch (action.type) {
13 | case ADD_TAB:
14 | return state.set(action.id, {
15 | title: 'New Tab',
16 | url: '',
17 | isLoading: false,
18 | isLoaded: false,
19 | isFailed: false,
20 | })
21 | case CLOSE_TAB:
22 | return state.without(action.id)
23 | case UPDATE_URL:
24 | return state.setIn([action.id, 'url'], action.url)
25 | case LOAD_START:
26 | return state
27 | .setIn([action.id, 'isLoading'], true)
28 | .setIn([action.id, 'isLoaded'], false)
29 | .setIn([action.id, 'url'], action.url)
30 | .setIn([action.id, 'title'], getTitle(action.url))
31 | case LOAD_SUCCESS:
32 | return state
33 | .setIn([action.id, 'isLoading'], false)
34 | .setIn([action.id, 'isLoaded'], true)
35 | case LOAD_FAIL:
36 | return state
37 | .setIn([action.id, 'isLoading'], false)
38 | .setIn([action.id, 'isFailed'], true)
39 | default:
40 | return state
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chrome UI
2 |
3 | Chrome UI built with web stack.
4 |
5 | Demo: https://pd4d10.github.io/chrome-ui/
6 |
7 | ## Trouble Shooting
8 |
9 | ### Webpage content is not loaded, just showing blank
10 |
11 | Many websites like Google, Twitter or GitHub, refuse to show content in cross domain iframe, for secure reason.
12 |
13 | Try `en.wikipedia.org` instead.
14 |
15 | If you press F12 to open devtools, you will see messages like follows:
16 |
17 | ```
18 | Refused to display 'https://www.google.com/' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'.
19 | Refused to display 'https://twitter.com/' in a frame because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'".
20 | Refused to display 'https://github.com/' in a frame because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'none'".
21 | ```
22 |
23 | ### It is messed up in my browser
24 |
25 | It is tested only on Chrome, for now.
26 |
27 | If you find a bug or style mess, please submit an issue. Thanks!
28 |
29 | ## Roadmap
30 |
31 | * Add tests
32 | * Incognito mode theme
33 |
34 | ## Contribute
35 |
36 | ```sh
37 | git clone https://github.com/pd4d10/chrome-ui.git
38 | yarn
39 | yarn start
40 |
41 | # You can also use npm instead:
42 | npm install
43 | npm start
44 | ```
45 |
46 | ## License
47 |
48 | MIT
49 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | import { capitalize } from 'lodash'
2 |
3 | // Add 'https://' prefix to a URL
4 | // Must be 'https://' here because GitHub pages is loaded over HTTPS
5 | // HTTP request will be blocked by default
6 | export function completeUrl(url) {
7 | if (/^https:\/\//.test(url)) {
8 | return url
9 | }
10 |
11 | if (/^http:\/\/(.*)/.test(url)) {
12 | return url.replace(/^http/, 'https')
13 | }
14 |
15 | return `https://${url}`
16 | }
17 |
18 | // Guess favicon from url
19 | export function getFavicon(url) {
20 | const a = document.createElement('a')
21 | a.href = completeUrl(url)
22 |
23 | return `${a.protocol}//${a.host}/favicon.ico`
24 | }
25 |
26 | // Fake title
27 | export function getTitle(url) {
28 | const arr = url.split('.')
29 | switch (arr.length) {
30 | case 2: // twitter.com
31 | return capitalize(arr[0])
32 | case 3: // www.google.com
33 | case 4: // www.google.co.uk
34 | return capitalize(arr[1])
35 | default:
36 | return url
37 | }
38 | }
39 |
40 | // Github start widget
41 | export function getGithubWidget() {
42 | return '
'
43 | }
44 |
45 | // Send analytic event
46 | export function sendEvent(...args) {
47 | if (window.ga) {
48 | window.ga('send', 'event', ...args)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/navigator/navigator.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --hover: #ccc;
3 | --border: #bbb;
4 | --nav: #f2f2f2;
5 | --icon: #6d6d6d;
6 | --icon-hover: #dfdfdf;
7 | --icon-focus: #aaa;
8 | }
9 |
10 | .nav {
11 | display: flex;
12 | align-items: center;
13 | background: var(--nav);
14 | border-top: 1px solid var(--border);
15 | border-bottom: 1px solid #ddd;
16 | padding-left: 4px;
17 | padding-right: 4px;
18 | height: 38px;
19 | & a {
20 | border-radius: 2px;
21 | margin-left: 2px;
22 | margin-right: 2px;
23 | padding: 4px;
24 | height: 24px;
25 | &:hover {
26 | background: var(--icon-hover);
27 | }
28 | &:focus {
29 | background: var(--icon-focus);
30 | }
31 | }
32 | & svg {
33 | width: 16px;
34 | height: 16px;
35 | & path {
36 | fill: var(--icon);
37 | }
38 | }
39 | & form {
40 | flex-grow: 1;
41 | display: flex;
42 | align-items: center;
43 | background: #fff;
44 | height: 28px;
45 | border-radius: 4px;
46 | margin-left: 4px;
47 | margin-right: 2px;
48 | border: 1px solid #ccc;
49 | &.active {
50 | border: 1px solid #399df7;
51 | height: 26px;
52 | }
53 | & svg {
54 | margin-right: 4px;
55 | }
56 | & input {
57 | display: block;
58 | border: none;
59 | width: 100%;
60 | padding-left: 20px;
61 | font-size: 13.6px;
62 | &:focus {
63 | outline: none;
64 | }
65 | &:-webkit-autofill {
66 | background-color: #fff;
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
Chrome UI
17 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var fs = require('fs');
3 |
4 | // Make sure any symlinks in the project folder are resolved:
5 | // https://github.com/facebookincubator/create-react-app/issues/637
6 | var appDirectory = fs.realpathSync(process.cwd());
7 | function resolveApp(relativePath) {
8 | return path.resolve(appDirectory, relativePath);
9 | }
10 |
11 | // We support resolving modules according to `NODE_PATH`.
12 | // This lets you use absolute paths in imports inside large monorepos:
13 | // https://github.com/facebookincubator/create-react-app/issues/253.
14 |
15 | // It works similar to `NODE_PATH` in Node itself:
16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
17 |
18 | // We will export `nodePaths` as an array of absolute paths.
19 | // It will then be used by Webpack configs.
20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box.
21 |
22 | var nodePaths = (process.env.NODE_PATH || '')
23 | .split(process.platform === 'win32' ? ';' : ':')
24 | .filter(Boolean)
25 | .map(resolveApp);
26 |
27 | // config after eject: we're in ./config/
28 | module.exports = {
29 | appBuild: resolveApp('build'),
30 | appPublic: resolveApp('public'),
31 | appHtml: resolveApp('public/index.html'),
32 | appIndexJs: resolveApp('src/index.js'),
33 | appPackageJson: resolveApp('package.json'),
34 | appSrc: resolveApp('src'),
35 | testsSetup: resolveApp('src/setupTests.js'),
36 | appNodeModules: resolveApp('node_modules'),
37 | ownNodeModules: resolveApp('node_modules'),
38 | nodePaths: nodePaths
39 | };
40 |
41 |
42 |
43 | // config before publish: we're in ./packages/react-scripts/config/
44 | if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1) {
45 | module.exports = {
46 | appBuild: resolveOwn('../../../build'),
47 | appPublic: resolveOwn('../template/public'),
48 | appHtml: resolveOwn('../template/public/index.html'),
49 | appIndexJs: resolveOwn('../template/src/index.js'),
50 | appPackageJson: resolveOwn('../package.json'),
51 | appSrc: resolveOwn('../template/src'),
52 | testsSetup: resolveOwn('../template/src/setupTests.js'),
53 | appNodeModules: resolveOwn('../node_modules'),
54 | ownNodeModules: resolveOwn('../node_modules'),
55 | nodePaths: nodePaths
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/tab/tab.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background: #dbdbdb;
3 | --hover: #eee;
4 | --border: #bbb;
5 | --nav: #f2f2f2;
6 | }
7 |
8 | .tab {
9 | flex-basis: 218px;
10 | display: flex;
11 | min-width: 0; /* http://stackoverflow.com/questions/34934586/white-space-nowrap-and-flexbox-did-not-work-in-chrome */
12 | align-items: center;
13 | position: relative;
14 | background-color: var(--background);
15 | border-top: 1px solid var(--border);
16 | margin: 0 5px;
17 | font-size: 0;
18 | transition: background-color .5s;
19 | &:hover {
20 | background: var(--hover);
21 | &::before, &::after {
22 | background: var(--hover);
23 | }
24 | }
25 | &.active {
26 | background: var(--nav);
27 | height: 29px;
28 | border-bottom: 1px solid var(--nav);
29 | transition: all 0s;
30 | &::before, &::after {
31 | z-index: 10;
32 | transition: all 0s;
33 | align-self: flex-start;
34 | height: 28px;
35 | background: var(--nav);
36 | border-bottom: 1px solid var(--nav);
37 | }
38 | }
39 | & .favicon {
40 | z-index: 100;
41 | width: 16px;
42 | height: 16px;
43 | margin-left: 4px;
44 | margin-right: 4px;
45 | user-select: none;
46 | }
47 | & .close {
48 | fill: #000;
49 | width: 12px;
50 | height: 12px;
51 | border-radius: 50%;
52 | &:hover {
53 | fill: #fff;
54 | background: #f00;
55 | }
56 | }
57 | &::before {
58 | content: '';
59 | position: absolute;
60 | z-index: 0;
61 | left: 0;
62 | width: 16px;
63 | height: 28px;
64 | background-color: var(--background);
65 | border-left: 1px solid var(--border);
66 | border-bottom: 1px solid var(--border);
67 | transform: skewx(-25deg);
68 | transform-origin: left top;
69 | transition: background-color .5s;
70 | }
71 | &::after {
72 | content: '';
73 | position: absolute;
74 | z-index: 1;
75 | right: 0;
76 | width: 16px;
77 | height: 28px;
78 | background-color: var(--background);
79 | border-right: 1px solid var(--border);
80 | border-bottom: 1px solid var(--border);
81 | transform: skewx(25deg);
82 | transform-origin: right top;
83 | transition: background-color .5s;
84 | }
85 |
86 | & .content {
87 | flex-grow: 1;
88 | padding-left: 2px;
89 | font-size: 12.6px;
90 | line-height: 28px;
91 | cursor: default;
92 | max-width: 200px;
93 | user-select: none;
94 | white-space: nowrap;
95 | overflow: hidden;
96 | text-overflow: clip;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import { v4 } from 'uuid'
2 |
3 | export const ADD_TAB = 'ADD_TAB'
4 | export const CLOSE_TAB = 'CLOSE_TAB'
5 | export const SELECT_TAB = 'SELECT_TAB'
6 | export const UPDATE_URL = 'UPDATE_TITLE'
7 | export const CHANGE_INPUT = 'CHANGE_INPUT'
8 | export const FOCUS_INPUT = 'FOCUS_INPUT'
9 | export const BLUR_INPUT = 'BLUR_INPUT'
10 |
11 | export const LOAD_START = 'LOAD_START'
12 | export const LOAD_SUCCESS = 'LOAD_SUCCESS'
13 | export const LOAD_FAIL = 'LOAD_FAIL'
14 |
15 | export const addTab = () => {
16 | // This is an ugly hack.
17 | // When new tab is opened, location input should focus.
18 | // It is too complex to do this in React way. 🌧
19 | document.querySelector('#location').focus()
20 | return {
21 | type: ADD_TAB,
22 | id: v4(),
23 | }
24 | }
25 |
26 | export const closeTab = id => (dispatch, getState) => {
27 | const state = getState()
28 | const { tabs } = state
29 | const allIds = Object.keys(tabs)
30 |
31 | // Do not close last tab
32 | if (allIds.length <= 1) {
33 | alert('This is the last tab :)') // eslint-disable-line
34 | return
35 | }
36 |
37 | // When active tab is closed, make right tab active
38 | // If no right, make left active
39 | let activeId = state.activeTab
40 | if (id === state.activeTab) {
41 | const position = allIds.indexOf(id)
42 | if (position < allIds.length - 1) {
43 | activeId = allIds[position + 1]
44 | } else {
45 | activeId = allIds[position - 1]
46 | }
47 | }
48 |
49 | dispatch({
50 | type: CLOSE_TAB,
51 | id,
52 | activeId,
53 | url: tabs[activeId].url,
54 | })
55 | }
56 |
57 | export const selectTab = id => (dispatch, getState) => dispatch({
58 | type: SELECT_TAB,
59 | id,
60 | url: getState().tabs[id].url,
61 | })
62 |
63 | export const updateUrl = ({ id, url }) => ({
64 | type: UPDATE_URL,
65 | id,
66 | url,
67 | })
68 |
69 | export const changeInput = value => ({
70 | type: CHANGE_INPUT,
71 | value,
72 | })
73 |
74 | export const focusInput = value => ({
75 | type: FOCUS_INPUT,
76 | value,
77 | })
78 |
79 | export const blurInput = () => ({
80 | type: BLUR_INPUT,
81 | })
82 |
83 | const loadStart = ({ id, url }) => ({
84 | type: LOAD_START,
85 | id,
86 | url,
87 | })
88 |
89 | export const loadSuccess = id => ({
90 | type: LOAD_SUCCESS,
91 | id,
92 | })
93 |
94 | export const load = () => (dispatch, getState) => {
95 | // Ugly hack either
96 | // Make loaction input blur after submit
97 | document.querySelector('#location').blur()
98 |
99 | const state = getState()
100 | const id = state.activeTab
101 | const url = state.input
102 | dispatch(loadStart({ id, url }))
103 | }
104 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chrome-ui",
3 | "version": "1.0.0",
4 | "author": "pd4d10
",
5 | "homepage": "https://pd4d10.github.io/chrome-ui/",
6 | "devDependencies": {
7 | "autoprefixer": "6.5.1",
8 | "babel-core": "6.17.0",
9 | "babel-eslint": "7.0.0",
10 | "babel-jest": "16.0.0",
11 | "babel-loader": "6.2.5",
12 | "babel-preset-react-app": "^1.0.0",
13 | "case-sensitive-paths-webpack-plugin": "1.1.4",
14 | "chalk": "1.1.3",
15 | "connect-history-api-fallback": "1.3.0",
16 | "cross-spawn": "4.0.2",
17 | "css-loader": "0.25.0",
18 | "detect-port": "1.0.1",
19 | "dotenv": "2.0.0",
20 | "eslint": "^3.9.1",
21 | "eslint-config-airbnb": "^13.0.0",
22 | "eslint-loader": "1.6.0",
23 | "eslint-plugin-flowtype": "2.21.0",
24 | "eslint-plugin-import": "^2.1.0",
25 | "eslint-plugin-jsx-a11y": "2.2.3",
26 | "eslint-plugin-react": "^6.6.0",
27 | "extract-text-webpack-plugin": "1.0.1",
28 | "file-loader": "0.9.0",
29 | "filesize": "3.3.0",
30 | "find-cache-dir": "0.1.1",
31 | "fs-extra": "0.30.0",
32 | "gh-pages": "^0.12.0",
33 | "gzip-size": "3.0.0",
34 | "html-webpack-plugin": "2.24.0",
35 | "http-proxy-middleware": "0.17.2",
36 | "jest": "16.0.2",
37 | "json-loader": "0.5.4",
38 | "object-assign": "4.1.0",
39 | "path-exists": "2.1.0",
40 | "postcss-cssnext": "^2.9.0",
41 | "postcss-import": "^9.0.0",
42 | "postcss-loader": "1.0.0",
43 | "promise": "7.1.1",
44 | "react-dev-utils": "^0.3.0",
45 | "recursive-readdir": "2.1.0",
46 | "rimraf": "2.5.4",
47 | "strip-ansi": "3.0.1",
48 | "style-loader": "0.13.1",
49 | "url-loader": "0.5.7",
50 | "webpack": "1.13.2",
51 | "webpack-dev-server": "1.16.2",
52 | "webpack-manifest-plugin": "1.1.0",
53 | "whatwg-fetch": "1.0.0"
54 | },
55 | "dependencies": {
56 | "classnames": "^2.2.5",
57 | "history": "^4.5.0",
58 | "lodash": "^4.17.4",
59 | "react": "^15.4.0",
60 | "react-dom": "^15.4.0",
61 | "react-redux": "^4.4.6",
62 | "redux": "^3.6.0",
63 | "redux-thunk": "^2.1.0",
64 | "seamless-immutable": "^7.0.1",
65 | "uuid": "^3.0.1"
66 | },
67 | "scripts": {
68 | "start": "node scripts/start.js",
69 | "build": "node scripts/build.js",
70 | "test": "node scripts/test.js --env=jsdom",
71 | "lint": "eslint src/*",
72 | "deploy": "gh-pages -d build"
73 | },
74 | "jest": {
75 | "moduleFileExtensions": [
76 | "jsx",
77 | "js",
78 | "json"
79 | ],
80 | "moduleNameMapper": {
81 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/FileStub.js",
82 | "^.+\\.css$": "/config/jest/CSSStub.js"
83 | },
84 | "setupFiles": [
85 | "/config/polyfills.js"
86 | ],
87 | "testPathIgnorePatterns": [
88 | "/(build|docs|node_modules)/"
89 | ],
90 | "testEnvironment": "node"
91 | },
92 | "babel": {
93 | "presets": [
94 | "react-app"
95 | ]
96 | },
97 | "eslintConfig": {
98 | "extends": "airbnb",
99 | "env": {
100 | "browser": true
101 | },
102 | "rules": {
103 | "semi": [
104 | "error",
105 | "never"
106 | ],
107 | "react/jsx-filename-extension": "off"
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // Do this as the first thing so that any code reading it knows the right env.
3 | process.env.NODE_ENV = 'production';
4 |
5 | // Load environment variables from .env file. Suppress warnings using silent
6 | // if this file is missing. dotenv will never modify any environment variables
7 | // that have already been set.
8 | // https://github.com/motdotla/dotenv
9 | require('dotenv').config({silent: true});
10 |
11 | var chalk = require('chalk');
12 | var fs = require('fs-extra');
13 | var path = require('path');
14 | var filesize = require('filesize');
15 | var gzipSize = require('gzip-size').sync;
16 | var rimrafSync = require('rimraf').sync;
17 | var webpack = require('webpack');
18 | var config = require('../config/webpack.config.prod');
19 | var paths = require('../config/paths');
20 | var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
21 | var recursive = require('recursive-readdir');
22 | var stripAnsi = require('strip-ansi');
23 |
24 | // Warn and crash if required files are missing
25 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
26 | process.exit(1);
27 | }
28 |
29 | // Input: /User/dan/app/build/static/js/main.82be8.js
30 | // Output: /static/js/main.js
31 | function removeFileNameHash(fileName) {
32 | return fileName
33 | .replace(paths.appBuild, '')
34 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3);
35 | }
36 |
37 | // Input: 1024, 2048
38 | // Output: "(+1 KB)"
39 | function getDifferenceLabel(currentSize, previousSize) {
40 | var FIFTY_KILOBYTES = 1024 * 50;
41 | var difference = currentSize - previousSize;
42 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0;
43 | if (difference >= FIFTY_KILOBYTES) {
44 | return chalk.red('+' + fileSize);
45 | } else if (difference < FIFTY_KILOBYTES && difference > 0) {
46 | return chalk.yellow('+' + fileSize);
47 | } else if (difference < 0) {
48 | return chalk.green(fileSize);
49 | } else {
50 | return '';
51 | }
52 | }
53 |
54 | // First, read the current file sizes in build directory.
55 | // This lets us display how much they changed later.
56 | recursive(paths.appBuild, (err, fileNames) => {
57 | var previousSizeMap = (fileNames || [])
58 | .filter(fileName => /\.(js|css)$/.test(fileName))
59 | .reduce((memo, fileName) => {
60 | var contents = fs.readFileSync(fileName);
61 | var key = removeFileNameHash(fileName);
62 | memo[key] = gzipSize(contents);
63 | return memo;
64 | }, {});
65 |
66 | // Remove all content but keep the directory so that
67 | // if you're in it, you don't end up in Trash
68 | rimrafSync(paths.appBuild + '/*');
69 |
70 | // Start the webpack build
71 | build(previousSizeMap);
72 |
73 | // Merge with the public folder
74 | copyPublicFolder();
75 | });
76 |
77 | // Print a detailed summary of build files.
78 | function printFileSizes(stats, previousSizeMap) {
79 | var assets = stats.toJson().assets
80 | .filter(asset => /\.(js|css)$/.test(asset.name))
81 | .map(asset => {
82 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name);
83 | var size = gzipSize(fileContents);
84 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)];
85 | var difference = getDifferenceLabel(size, previousSize);
86 | return {
87 | folder: path.join('build', path.dirname(asset.name)),
88 | name: path.basename(asset.name),
89 | size: size,
90 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '')
91 | };
92 | });
93 | assets.sort((a, b) => b.size - a.size);
94 | var longestSizeLabelLength = Math.max.apply(null,
95 | assets.map(a => stripAnsi(a.sizeLabel).length)
96 | );
97 | assets.forEach(asset => {
98 | var sizeLabel = asset.sizeLabel;
99 | var sizeLength = stripAnsi(sizeLabel).length;
100 | if (sizeLength < longestSizeLabelLength) {
101 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength);
102 | sizeLabel += rightPadding;
103 | }
104 | console.log(
105 | ' ' + sizeLabel +
106 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)
107 | );
108 | });
109 | }
110 |
111 | // Print out errors
112 | function printErrors(summary, errors) {
113 | console.log(chalk.red(summary));
114 | console.log();
115 | errors.forEach(err => {
116 | console.log(err.message || err);
117 | console.log();
118 | });
119 | }
120 |
121 | // Create the production build and print the deployment instructions.
122 | function build(previousSizeMap) {
123 | console.log('Creating an optimized production build...');
124 | webpack(config).run((err, stats) => {
125 | if (err) {
126 | printErrors('Failed to compile.', [err]);
127 | process.exit(1);
128 | }
129 |
130 | if (stats.compilation.errors.length) {
131 | printErrors('Failed to compile.', stats.compilation.errors);
132 | process.exit(1);
133 | }
134 |
135 | console.log(chalk.green('Compiled successfully.'));
136 | console.log();
137 |
138 | console.log('File sizes after gzip:');
139 | console.log();
140 | printFileSizes(stats, previousSizeMap);
141 | console.log();
142 |
143 | var openCommand = process.platform === 'win32' ? 'start' : 'open';
144 | var homepagePath = require(paths.appPackageJson).homepage;
145 | var publicPath = config.output.publicPath;
146 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) {
147 | // "homepage": "http://user.github.io/project"
148 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
149 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
150 | console.log();
151 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
152 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:');
153 | console.log();
154 | console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages');
155 | console.log();
156 | console.log('Add the following script in your ' + chalk.cyan('package.json') + '.');
157 | console.log();
158 | console.log(' ' + chalk.dim('// ...'));
159 | console.log(' ' + chalk.yellow('"scripts"') + ': {');
160 | console.log(' ' + chalk.dim('// ...'));
161 | console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"gh-pages -d build"'));
162 | console.log(' }');
163 | console.log();
164 | console.log('Then run:');
165 | console.log();
166 | console.log(' ' + chalk.cyan('npm') + ' run deploy');
167 | console.log();
168 | } else if (publicPath !== '/') {
169 | // "homepage": "http://mywebsite.com/project"
170 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
171 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
172 | console.log();
173 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
174 | console.log();
175 | } else {
176 | // no homepage or "homepage": "http://mywebsite.com"
177 | console.log('The project was built assuming it is hosted at the server root.');
178 | if (homepagePath) {
179 | // "homepage": "http://mywebsite.com"
180 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
181 | console.log();
182 | } else {
183 | // no homepage
184 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.');
185 | console.log('For example, add this to build it for GitHub Pages:')
186 | console.log();
187 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(','));
188 | console.log();
189 | }
190 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
191 | console.log('You may also serve it locally with a static server:')
192 | console.log();
193 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server');
194 | console.log(' ' + chalk.cyan('pushstate-server') + ' build');
195 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000');
196 | console.log();
197 | }
198 | });
199 | }
200 |
201 | function copyPublicFolder() {
202 | fs.copySync(paths.appPublic, paths.appBuild, {
203 | dereference: true,
204 | filter: file => file !== paths.appHtml
205 | });
206 | }
207 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const autoprefixer = require('autoprefixer')
3 | const webpack = require('webpack')
4 | const findCacheDir = require('find-cache-dir')
5 | const HtmlWebpackPlugin = require('html-webpack-plugin')
6 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
7 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin')
8 | const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin')
9 | const getClientEnvironment = require('./env')
10 | const paths = require('./paths')
11 |
12 | // Webpack uses `publicPath` to determine where the app is being served from.
13 | // In development, we always serve from the root. This makes config easier.
14 | const publicPath = '/'
15 | // `publicUrl` is just like `publicPath`, but we will provide it to our app
16 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
17 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
18 | const publicUrl = ''
19 | // Get environment variables to inject into our app.
20 | const env = getClientEnvironment(publicUrl)
21 |
22 | // This is the development configuration.
23 | // It is focused on developer experience and fast rebuilds.
24 | // The production configuration is different and lives in a separate file.
25 | module.exports = {
26 | // This makes the bundle appear split into separate modules in the devtools.
27 | // We don't use source maps here because they can be confusing:
28 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875
29 | // You may want 'cheap-module-source-map' instead if you prefer source maps.
30 | devtool: 'eval',
31 | // These are the "entry points" to our application.
32 | // This means they will be the "root" imports that are included in JS bundle.
33 | // The first two entry points enable "hot" CSS and auto-refreshes for JS.
34 | entry: [
35 | // Include an alternative client for WebpackDevServer. A client's job is to
36 | // connect to WebpackDevServer by a socket and get notified about changes.
37 | // When you save a file, the client will either apply hot updates (in case
38 | // of CSS changes), or refresh the page (in case of JS changes). When you
39 | // make a syntax error, this client will display a syntax error overlay.
40 | // Note: instead of the default WebpackDevServer client, we use a custom one
41 | // to bring better experience for Create React App users. You can replace
42 | // the line below with these two lines if you prefer the stock client:
43 | // require.resolve('webpack-dev-server/client') + '?/',
44 | // require.resolve('webpack/hot/dev-server'),
45 | require.resolve('react-dev-utils/webpackHotDevClient'),
46 | // We ship a few polyfills by default:
47 | require.resolve('./polyfills'),
48 | // Finally, this is your app's code:
49 | paths.appIndexJs,
50 | // We include the app code last so that if there is a runtime error during
51 | // initialization, it doesn't blow up the WebpackDevServer client, and
52 | // changing JS code would still trigger a refresh.
53 | ],
54 | output: {
55 | // Next line is not used in dev but WebpackDevServer crashes without it:
56 | path: paths.appBuild,
57 | // Add /* filename */ comments to generated require()s in the output.
58 | pathinfo: true,
59 | // This does not produce a real file. It's just the virtual path that is
60 | // served by WebpackDevServer in development. This is the JS bundle
61 | // containing code from all our entry points, and the Webpack runtime.
62 | filename: 'static/js/bundle.js',
63 | // This is the URL that app is served from. We use "/" in development.
64 | publicPath,
65 | },
66 | resolve: {
67 | // This allows you to set a fallback for where Webpack should look for modules.
68 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here.
69 | // We use `fallback` instead of `root` because we want `node_modules` to "win"
70 | // if there any conflicts. This matches Node resolution mechanism.
71 | // https://github.com/facebookincubator/create-react-app/issues/253
72 | fallback: paths.nodePaths,
73 | // These are the reasonable defaults supported by the Node ecosystem.
74 | // We also include JSX as a common component filename extension to support
75 | // some tools, although we do not recommend using it, see:
76 | // https://github.com/facebookincubator/create-react-app/issues/290
77 | extensions: ['.js', '.json', '.jsx', ''],
78 | alias: {
79 | // Support React Native Web
80 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
81 | 'react-native': 'react-native-web',
82 | },
83 | },
84 |
85 | module: {
86 | // First, run the linter.
87 | // It's important to do this before Babel processes the JS.
88 | preLoaders: [
89 | {
90 | test: /\.(js|jsx)$/,
91 | loader: 'eslint',
92 | include: paths.appSrc,
93 | },
94 | ],
95 | loaders: [
96 | // Process JS with Babel.
97 | {
98 | test: /\.(js|jsx)$/,
99 | include: paths.appSrc,
100 | loader: 'babel',
101 | query: {
102 |
103 | // This is a feature of `babel-loader` for webpack (not Babel itself).
104 | // It enables caching results in ./node_modules/.cache/react-scripts/
105 | // directory for faster rebuilds. We use findCacheDir() because of:
106 | // https://github.com/facebookincubator/create-react-app/issues/483
107 | cacheDirectory: findCacheDir({
108 | name: 'react-scripts',
109 | }),
110 | },
111 | },
112 | // "postcss" loader applies autoprefixer to our CSS.
113 | // "css" loader resolves paths in CSS and adds assets as dependencies.
114 | // "style" loader turns CSS into JS modules that inject