Browse Source

Huge breaking update to the world of 2021.

Based on create-react-app --template redux-typescript.

Some stuff still needs to be removed or fixed.
Tests likely fail.

But the basic connection logic works!
main
Titouan Rigoudy 4 years ago
parent
commit
ba8580cde4
50 changed files with 40217 additions and 683 deletions
  1. +0
    -19
      .babelrc
  2. +0
    -13
      .editorconfig
  3. +0
    -65
      .eslintrc
  4. +18
    -34
      .gitignore
  5. +0
    -1
      .npmrc
  6. +44
    -0
      README.md
  7. +39328
    -0
      package-lock.json
  8. +46
    -83
      package.json
  9. +0
    -0
      public/favicon.ico
  10. +33
    -0
      public/index.html
  11. BIN
      public/logo192.png
  12. BIN
      public/logo512.png
  13. +3
    -0
      public/robots.txt
  14. +39
    -0
      src/App.css
  15. +15
    -0
      src/App.test.tsx
  16. +58
    -0
      src/App.tsx
  17. +6
    -0
      src/app/hooks.ts
  18. +34
    -0
      src/app/store.ts
  19. +48
    -35
      src/components/ConnectForm.js
  20. +3
    -5
      src/components/Header.js
  21. +1
    -7
      src/components/LoginStatusPane.js
  22. +7
    -7
      src/components/SocketStatusPane.tsx
  23. +42
    -12
      src/components/SolsticeApp.js
  24. +21
    -35
      src/containers/ConnectPage.js
  25. +12
    -12
      src/containers/Footer.js
  26. +0
    -35
      src/createRoutes.js
  27. +79
    -0
      src/features/counter/Counter.module.css
  28. +68
    -0
      src/features/counter/Counter.tsx
  29. +6
    -0
      src/features/counter/counterAPI.ts
  30. +34
    -0
      src/features/counter/counterSlice.spec.ts
  31. +82
    -0
      src/features/counter/counterSlice.ts
  32. +0
    -11
      src/index.html
  33. +0
    -20
      src/index.js
  34. +21
    -0
      src/index.tsx
  35. +5
    -0
      src/modules/websocket/actions.ts
  36. +82
    -0
      src/modules/websocket/middleware.ts
  37. +47
    -0
      src/modules/websocket/slice.ts
  38. +1
    -0
      src/react-app-env.d.ts
  39. +1
    -1
      src/reducers/socket.js
  40. +5
    -0
      src/setupTests.ts
  41. +0
    -26
      src/store/configureStore.dev.js
  42. +0
    -20
      src/store/configureStore.js
  43. +0
    -6
      src/store/configureStore.prod.js
  44. +2
    -0
      src/utils/OrderedMap.js
  45. +0
    -45
      tools/build.js
  46. +0
    -45
      tools/buildHtml.js
  47. +0
    -19
      tools/distServer.js
  48. +0
    -44
      tools/srcServer.js
  49. +26
    -0
      tsconfig.json
  50. +0
    -83
      webpack.config.js

+ 0
- 19
.babelrc View File

@ -1,19 +0,0 @@
{
"presets": ["es2015", "react", "stage-1"],
"env": {
"development": {
"plugins": [
["react-transform", {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}, {
"transform": "react-transform-catch-errors",
"imports": ["react", "redbox-react"]
}]
}]
]
}
}
}

+ 0
- 13
.editorconfig View File

@ -1,13 +0,0 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

+ 0
- 65
.eslintrc View File

@ -1,65 +0,0 @@
{
"parser": "babel-eslint",
"extends": "eslint:recommended",
"plugins": [
"react"
],
"env": {
"es6": true,
"browser": true,
"node": true,
"jquery": true,
"mocha": true
},
"ecmaFeatures": {
"jsx": true,
"modules": true
},
"rules": {
"quotes": 0,
"no-console": 1,
"no-debugger": 1,
"no-var": 1,
"semi": [1, "always"],
"no-trailing-spaces": 0,
"eol-last": 0,
"no-unused-vars": 0,
"no-underscore-dangle": 0,
"no-alert": 0,
"no-lone-blocks": 0,
"jsx-quotes": 1,
"react/display-name": [ 1, {"ignoreTranspilerName": false }],
"react/forbid-prop-types": [1, {"forbid": ["any"]}],
"react/jsx-boolean-value": 1,
"react/jsx-closing-bracket-location": 0,
"react/jsx-curly-spacing": 1,
"react/jsx-indent-props": 0,
"react/jsx-key": 1,
"react/jsx-max-props-per-line": 0,
"react/jsx-no-bind": 0,
"react/jsx-no-duplicate-props": 1,
"react/jsx-no-literals": 0,
"react/jsx-no-undef": 1,
"react/jsx-pascal-case": 1,
"react/jsx-sort-prop-types": 0,
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/no-danger": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-update-set-state": 1,
"react/no-direct-mutation-state": 1,
"react/no-multi-comp": 1,
"react/no-set-state": 1,
"react/no-unknown-property": 1,
"react/prefer-es6-class": 1,
"react/prop-types": 1,
"react/react-in-jsx-scope": 1,
"react/require-extension": 1,
"react/self-closing-comp": 1,
"react/sort-comp": 1,
"react/wrap-multilines": 1
},
"globals": {
}
}

+ 18
- 34
.gitignore View File

@ -1,39 +1,23 @@
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Runtime data
pids
*.pid
*.seed
# dependencies
/node_modules
/.pnp
.pnp.js
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# testing
/coverage
# Coverage directory used by tools like istanbul
coverage
# production
/build
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
#dist folder
dist
#Webstorm metadata
.idea
# Mac files
# misc
.DS_Store
# Vim session file
Session.vim
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

+ 0
- 1
.npmrc View File

@ -1 +0,0 @@
save-exact=true

+ 44
- 0
README.md View File

@ -0,0 +1,44 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

+ 39328
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 46
- 83
package.json View File

@ -1,90 +1,53 @@
{
"name": "react-slingshot",
"version": "1.2.0",
"description": "Starter kit for creating apps with React and Redux",
"name": "solstice-web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"final-form": "^4.20.2",
"immutable": "^3",
"md5": "^2.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-final-form": "^6.5.3",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"typescript": "~4.1.5"
},
"scripts": {
"prestart": "npm run remove-dist",
"start": "parallelshell \"npm run lint:tools\" \"npm run test:watch\" \"npm run open:src\"",
"open:src": "babel-node tools/srcServer.js",
"open:dist": "babel-node tools/distServer.js",
"lint:tools": "eslint webpack.config.js tools",
"clean-dist": "npm run remove-dist && mkdir dist",
"remove-dist": "node_modules/.bin/rimraf ./dist",
"build:html": "babel-node tools/buildHtml.js",
"prebuild": "npm run clean-dist && npm run build:html",
"build": "npm run test && babel-node tools/build.js && npm run open:dist",
"test": "cross-env NODE_ENV=test mocha --reporter progress --compilers js:babel-core/register --recursive \"./src/**/*.spec.js\"",
"test:watch": "npm run test -- --watch"
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"author": "Cory House",
"license": "MIT",
"dependencies": {
"immutable": "~3.7.6",
"md5": "2.1.0",
"object-assign": "4.0.1",
"react": "0.14.7",
"react-dom": "0.14.7",
"react-immutable-proptypes": "~1.7.0",
"react-redux": "4.4.0",
"react-router": "2.4.1",
"redux": "3.3.1",
"redux-form": "~4.2.0",
"redux-logger": "~2.6.1",
"redux-promise": "~0.5.1",
"redux-thunk": "~2.0.1"
"eslintConfig": {
"extends": "react-app"
},
"devDependencies": {
"babel-cli": "6.5.1",
"babel-core": "6.5.1",
"babel-eslint": "5.0.0",
"babel-loader": "6.2.3",
"babel-plugin-react-display-name": "2.0.0",
"babel-plugin-react-transform": "2.0.0",
"babel-preset-es2015": "6.5.0",
"babel-preset-react": "6.5.0",
"babel-preset-stage-1": "6.5.0",
"browser-sync": "2.11.1",
"chai": "3.4.1",
"cheerio": "0.19.0",
"colors": "1.1.2",
"cross-env": "1.0.7",
"css-loader": "0.23.1",
"eslint": "2.2.0",
"eslint-loader": "1.3.0",
"eslint-plugin-react": "4.0.0",
"extract-text-webpack-plugin": "1.0.1",
"file-loader": "0.8.5",
"mocha": "2.3.4",
"node-sass": "3.4.2",
"parallelshell": "2.0.0",
"react-transform-catch-errors": "1.0.1",
"react-transform-hmr": "1.0.1",
"redbox-react": "1.2.0",
"rimraf": "2.5.0",
"sass-loader": "3.1.2",
"style-loader": "0.13.0",
"watch": "0.17.1",
"webpack": "1.12.11",
"webpack-dev-middleware": "1.4.0",
"webpack-hot-middleware": "2.6.0",
"yargs": "3.32.0"
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"keywords:": [
"react",
"reactjs",
"hot",
"reload",
"hmr",
"live",
"edit",
"webpack",
"redux",
"flux",
"boilerplate",
"starter"
],
"repository": {
"type": "git",
"url": "https://github.com/coryhouse/react-slingshot"
"devDependencies": {
"@types/jest": "^24.9.1",
"@types/node": "^12.20.17",
"@types/react": "^16.14.11",
"@types/react-dom": "^16.9.14",
"@types/react-redux": "^7.1.18",
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.8",
"sass": "^1.36.0",
"sass-loader": "^10.2.0"
}
}

favicon.ico → public/favicon.ico View File


+ 33
- 0
public/index.html View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Solstice Web UI</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png View File

Before After
Width: 192  |  Height: 192  |  Size: 4.1 KiB

BIN
public/logo512.png View File

Before After
Width: 512  |  Height: 512  |  Size: 12 KiB

+ 3
- 0
public/robots.txt View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

+ 39
- 0
src/App.css View File

@ -0,0 +1,39 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-float infinite 3s ease-in-out;
}
}
.App-header {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.App-link {
color: rgb(112, 76, 182);
}
@keyframes App-logo-float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(10px);
}
100% {
transform: translateY(0px);
}
}

+ 15
- 0
src/App.test.tsx View File

@ -0,0 +1,15 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(
<Provider store={store}>
<App />
</Provider>
);
expect(getByText(/learn/i)).toBeInTheDocument();
});

+ 58
- 0
src/App.tsx View File

@ -0,0 +1,58 @@
import React from 'react';
import logo from './logo.svg';
import { Counter } from './features/counter/Counter';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Counter />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<span>
<span>Learn </span>
<a
className="App-link"
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
>
React
</a>
<span>, </span>
<a
className="App-link"
href="https://redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux
</a>
<span>, </span>
<a
className="App-link"
href="https://redux-toolkit.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux Toolkit
</a>
,<span> and </span>
<a
className="App-link"
href="https://react-redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
React Redux
</a>
</span>
</header>
</div>
);
}
export default App;

+ 6
- 0
src/app/hooks.ts View File

@ -0,0 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

+ 34
- 0
src/app/store.ts View File

@ -0,0 +1,34 @@
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from "../features/counter/counterSlice";
// TODO: Rework these to use a slice, otherwise redux complains about
// non-serializable Immutable.js types.
//
// import login from "../reducers/login";
// import rooms from "../reducers/rooms";
// import users from "../reducers/users";
//
import socketMiddleware from "../modules/websocket/middleware";
import socketReducer from "../modules/websocket/slice";
export const store = configureStore({
reducer: {
counter: counterReducer,
//login,
//rooms,
socket: socketReducer,
//users,
},
middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat(socketMiddleware)
)
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;

+ 48
- 35
src/components/ConnectForm.js View File

@ -1,45 +1,58 @@
import React, {PropTypes} from "react";
import {reduxForm} from "redux-form";
import ImmutablePropTypes from "react-immutable-proptypes";
import React from "react";
import { useDispatch } from "react-redux";
import { Form, Field } from "react-final-form";
import SocketStatusPane from "./SocketStatusPane";
import { SocketRecord } from "../reducers/socket";
import { STATE_CLOSED } from "../constants/socket";
import {
SocketSliceState,
SocketState,
socketOpen
} from "../modules/websocket/slice";
const ConnectForm = (props) => {
const { fields: { url }, handleSubmit, socket, actions } = props;
interface Props {
socket: SocketSliceState,
};
const onSubmit = handleSubmit((values) => {
return actions.socket.open(values.url, actions.socketHandlers);
});
const ConnectForm: React.FC<Props> = ({ socket }) => {
const dispatch = useDispatch();
const isSocketClosed = socket.state === STATE_CLOSED;
const onSubmit = ({ url }) => {
dispatch(socketOpen(url));
};
return (
<div id="connect-form">
<h2>Connect to a solstice client</h2>
<form onSubmit={onSubmit}>
<input type="url" defaultValue="ws://localhost:2244" {...url}
required pattern="wss?://.+"/>
<button type="submit" disabled={!isSocketClosed}>
Connect
</button>
</form>
<SocketStatusPane
state={socket.state}
url={socket.url}
/>
</div>
);
};
const isSocketClosed = socket.state === SocketState.Closed;
ConnectForm.propTypes = {
fields: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
socket: ImmutablePropTypes.record.isRequired,
actions: PropTypes.object.isRequired
return (
<div id="connect-form">
<h2>Connect to a solstice client</h2>
<Form
onSubmit={onSubmit}
render={({ handleSubmit, submitting }) => (
<form onSubmit={handleSubmit}>
<Field
name="url"
component="input"
type="url"
defaultValue="ws://localhost:2244"
required
pattern="wss?://.+"
/>
<button
type="submit"
disabled={submitting || !isSocketClosed}>
Connect
</button>
</form>
)}
/>
<SocketStatusPane
state={socket.state}
url={socket.url}
/>
</div>
);
};
export default reduxForm({
form: "connect",
fields: ["url"]
})(ConnectForm);
export default ConnectForm;

+ 3
- 5
src/components/Header.js View File

@ -1,7 +1,7 @@
import React, { PropTypes } from "react";
import { Link } from "react-router";
import React from "react";
import { Link } from "react-router-dom";
const Header = (props) => {
const Header = () => {
return (
<header>
<h1>Solstice web UI</h1>
@ -15,6 +15,4 @@ const Header = (props) => {
);
};
Header.propTypes = {};
export default Header;

+ 1
- 7
src/components/LoginStatusPane.js View File

@ -10,6 +10,7 @@ import {
LOGIN_STATUS_FAILURE
} from "../constants/login";
// TODO: Define Props type.
class LoginStatusPane extends React.Component
{
constructor(props) {
@ -65,11 +66,4 @@ class LoginStatusPane extends React.Component
}
}
LoginStatusPane.propTypes = {
status: propTypeSymbol.isRequired,
username: PropTypes.string,
motd: PropTypes.string,
reason: PropTypes.string
};
export default LoginStatusPane;

src/components/SocketStatusPane.js → src/components/SocketStatusPane.tsx View File

@ -1,10 +1,15 @@
import React, { PropTypes } from "react";
import React from "react";
import {
STATE_OPENING, STATE_OPEN, STATE_CLOSING, STATE_CLOSED
} from "../constants/socket";
const SocketStatusPane = ({ state, url }) => {
type Props = {
state: number,
url: string,
};
const SocketStatusPane: React.FC<Props> = ({ state, url }) => {
let string;
switch (state) {
case STATE_OPENING:
@ -23,9 +28,4 @@ const SocketStatusPane = ({ state, url }) => {
return <div id="socket-status-pane">Connection status: {string}</div>;
};
SocketStatusPane.propTypes = {
state: PropTypes.number.isRequired,
url: PropTypes.string
};
export default SocketStatusPane;

+ 42
- 12
src/components/SolsticeApp.js View File

@ -1,21 +1,51 @@
import React, {PropTypes} from "react";
import React from "react";
import { connect } from "react-redux";
import { Switch, Redirect, Route, useLocation } from "react-router-dom";
import Header from "./Header";
import { STATE_OPEN } from "../constants/socket";
import ConnectPage from "../containers/ConnectPage";
import Footer from "../containers/Footer";
import { SocketRecord } from "../reducers/socket";
const ConnectedApp: React.FC<SocketRecord> = ({ socket, children }) => {
const location = useLocation();
if (socket.state !== STATE_OPEN) {
return (
<Redirect
to={{
pathname: "/connect",
state: { from: location },
}}
/>
);
}
const SolsticeApp = ({ children }) => (
return (
<div id="solstice-app">
<Header />
<main>
{children}
</main>
<Footer />
<Header />
<main>
{children}
</main>
<Footer />
</div>
);
};
const SolsticeApp = ({ socket, children }) => (
<Switch>
<Route path="/connect">
<ConnectPage />
</Route>
<Route path="/">
<ConnectedApp socket={socket}>
{children}
</ConnectedApp>
</Route>
</Switch>
);
SolsticeApp.propTypes = {
children: PropTypes.element
};
const mapStateToProps = ({ socket }) => ({ socket });
export default SolsticeApp;
export default connect(mapStateToProps)(SolsticeApp);

+ 21
- 35
src/containers/ConnectPage.js View File

@ -1,8 +1,10 @@
import React, { PropTypes } from "react";
import { History } from "history";
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { bindActionCreators } from "redux";
import { hashHistory, withRouter } from "react-router";
import { SocketSliceState, SocketState } from "../modules/websocket/slice";
import LoginActions from "../actions/LoginActions";
import SocketActions from "../actions/SocketActions";
import SocketHandlerActions from "../actions/SocketHandlerActions";
@ -16,7 +18,13 @@ import {
import ConnectForm from "../components/ConnectForm";
import LoginStatusPane from "../components/LoginStatusPane";
class ConnectPage extends React.Component {
interface Props {
history: History,
socket: SocketSliceState,
};
// TODO: Gate access on login state too.
class ConnectPage extends React.Component<Props> {
constructor(props) {
super(props);
}
@ -29,10 +37,11 @@ class ConnectPage extends React.Component {
this.getLoginStatusOrRedirect(nextProps);
}
getLoginStatusOrRedirect(props) {
const { actions, login, router, socket } = props;
if (socket.state === STATE_OPEN)
{
getLoginStatusOrRedirect({ history, socket }: Props) {
if (socket.state === SocketState.Open) {
history.push("/app/rooms");
}
/*
switch (login.status) {
case LOGIN_STATUS_UNKNOWN:
actions.login.getStatus();
@ -42,42 +51,19 @@ class ConnectPage extends React.Component {
router.push("/app/rooms");
break;
}
}
*/
}
render() {
const { actions, socket } = this.props;
const { socket } = this.props;
return (
<div id="connect-page">
<ConnectForm socket={socket} actions={actions} />
<ConnectForm socket={socket} />
</div>
);
}
}
ConnectPage.propTypes = {
actions: PropTypes.shape({
login: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired,
socketHandlers: PropTypes.object.isRequired
}).isRequired,
login: PropTypes.object.isRequired,
router: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired
};
const mapStateToProps = ({ login, socket }) => ({ login, socket });
const mapDispatchToProps = (dispatch) => ({
actions: {
login: bindActionCreators(LoginActions, dispatch),
socket: bindActionCreators(SocketActions, dispatch),
socketHandlers: bindActionCreators(SocketHandlerActions, dispatch)
}
});
const mapStateToProps = ({ socket }) => ({ socket });
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(ConnectPage));
export default connect(mapStateToProps)(withRouter(ConnectPage));

+ 12
- 12
src/containers/Footer.js View File

@ -1,34 +1,34 @@
import React, { PropTypes } from "react";
import React from "react";
import { connect } from "react-redux";
import ImmutablePropTypes from "react-immutable-proptypes";
import { SocketSliceState } from "../modules/websocket/slice";
import LoginStatusPane from "../components/LoginStatusPane";
import SocketStatusPane from "../components/SocketStatusPane";
const Footer = ({ login, socket }) => {
interface Props {
//login: any,
socket: SocketSliceState,
};
const Footer = ({ socket }) => {
return (
<footer>
<SocketStatusPane
state={socket.state}
url={socket.url}
/>
{/*
<LoginStatusPane
status={login.status}
username={login.username}
motd={login.motd}
reason={login.reason}
/>
*/}
</footer>
);
};
Footer.propTypes = {
login: ImmutablePropTypes.record.isRequired,
socket: ImmutablePropTypes.record.isRequired
};
const mapStateToProps = ({ socket, login }) => ({ socket, login });
const mapStateToProps = ({ socket }) => ({ socket });
export default connect(
mapStateToProps
)(Footer);
export default connect(mapStateToProps)(Footer);

+ 0
- 35
src/createRoutes.js View File

@ -1,35 +0,0 @@
import React, { PropTypes } from 'react';
import { Route, IndexRoute } from 'react-router';
import ConnectPage from "./containers/ConnectPage";
import RoomsPane from "./containers/RoomsPane";
import UsersPane from "./containers/UsersPane";
import SolsticeApp from "./components/SolsticeApp";
import { STATE_OPEN } from "./constants/socket";
import { LOGIN_STATUS_SUCCESS } from "./constants/login";
const createRoutes = (store) => {
const requireLoggedIn = (nextState, replaceState) => {
let { socket, login } = store.getState();
if (socket.state !== STATE_OPEN ||
login.status !== LOGIN_STATUS_SUCCESS)
{
replaceState({}, "/");
}
};
return (
<Route path="/">
<IndexRoute component={ConnectPage} />
<Route path="app" onEnter={requireLoggedIn} component={SolsticeApp}>
<Route path="rooms(/:roomNameHash)" component={RoomsPane} />
<Route path="users(/:userNameHash)" component={UsersPane} />
</Route>
</Route>
);
};
export default createRoutes;

+ 79
- 0
src/features/counter/Counter.module.css View File

@ -0,0 +1,79 @@
.row {
display: flex;
align-items: center;
justify-content: center;
}
.row > button {
margin-left: 4px;
margin-right: 8px;
}
.row:not(:last-child) {
margin-bottom: 16px;
}
.value {
font-size: 78px;
padding-left: 16px;
padding-right: 16px;
margin-top: 2px;
font-family: 'Courier New', Courier, monospace;
}
.button {
appearance: none;
background: none;
font-size: 32px;
padding-left: 12px;
padding-right: 12px;
outline: none;
border: 2px solid transparent;
color: rgb(112, 76, 182);
padding-bottom: 4px;
cursor: pointer;
background-color: rgba(112, 76, 182, 0.1);
border-radius: 2px;
transition: all 0.15s;
}
.textbox {
font-size: 32px;
padding: 2px;
width: 64px;
text-align: center;
margin-right: 4px;
}
.button:hover,
.button:focus {
border: 2px solid rgba(112, 76, 182, 0.4);
}
.button:active {
background-color: rgba(112, 76, 182, 0.2);
}
.asyncButton {
composes: button;
position: relative;
}
.asyncButton:after {
content: '';
background-color: rgba(112, 76, 182, 0.15);
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
opacity: 0;
transition: width 1s linear, opacity 0.5s ease 1s;
}
.asyncButton:active:after {
width: 0%;
opacity: 1;
transition: 0s;
}

+ 68
- 0
src/features/counter/Counter.tsx View File

@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../app/hooks';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
incrementIfOdd,
selectCount,
} from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
const count = useAppSelector(selectCount);
const dispatch = useAppDispatch();
const [incrementAmount, setIncrementAmount] = useState('2');
const incrementValue = Number(incrementAmount) || 0;
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(incrementValue))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(incrementValue))}
>
Add Async
</button>
<button
className={styles.button}
onClick={() => dispatch(incrementIfOdd(incrementValue))}
>
Add If Odd
</button>
</div>
</div>
);
}

+ 6
- 0
src/features/counter/counterAPI.ts View File

@ -0,0 +1,6 @@
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}

+ 34
- 0
src/features/counter/counterSlice.spec.ts View File

@ -0,0 +1,34 @@
import counterReducer, {
CounterState,
increment,
decrement,
incrementByAmount,
} from './counterSlice';
describe('counter reducer', () => {
const initialState: CounterState = {
value: 3,
status: 'idle',
};
it('should handle initial state', () => {
expect(counterReducer(undefined, { type: 'unknown' })).toEqual({
value: 0,
status: 'idle',
});
});
it('should handle increment', () => {
const actual = counterReducer(initialState, increment());
expect(actual.value).toEqual(4);
});
it('should handle decrement', () => {
const actual = counterReducer(initialState, decrement());
expect(actual.value).toEqual(2);
});
it('should handle incrementByAmount', () => {
const actual = counterReducer(initialState, incrementByAmount(2));
expect(actual.value).toEqual(5);
});
});

+ 82
- 0
src/features/counter/counterSlice.ts View File

@ -0,0 +1,82 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
import { fetchCount } from './counterAPI';
export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}
const initialState: CounterState = {
value: 0,
status: 'idle',
};
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount: number) => {
const response = await fetchCount(amount);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
export const counterSlice = createSlice({
name: 'counter',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state: RootState) => state.counter.value;
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd = (amount: number): AppThunk => (
dispatch,
getState
) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;

+ 0
- 11
src/index.html View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Solstice Web UI</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>

+ 0
- 20
src/index.js View File

@ -1,20 +0,0 @@
import React from 'react';
import {render} from 'react-dom';
import { Provider } from 'react-redux';
import { Router, hashHistory } from "react-router";
import configureStore from './store/configureStore';
import createRoutes from "./createRoutes";
import './styles/styles.scss'; //Yep, that's right. You can import SASS/CSS files too! Webpack will run the associated loader and plug this into the page.
const store = configureStore();
const routes = createRoutes(store);
render(
<Provider store={store}>
<Router history={hashHistory}>
{routes}
</Router>
</Provider>, document.getElementById('app')
);

+ 21
- 0
src/index.tsx View File

@ -0,0 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from "react-router-dom";
import './styles/styles.scss';
import { store } from "./app/store";
import SolsticeApp from "./components/SolsticeApp";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<Router>
<SolsticeApp />
</Router>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

+ 5
- 0
src/modules/websocket/actions.ts View File

@ -0,0 +1,5 @@
export const wsConnect = host => ({ type: 'WS_CONNECT', host });
export const wsConnecting = host => ({ type: 'WS_CONNECTING', host });
export const wsConnected = host => ({ type: 'WS_CONNECTED', host });
export const wsDisconnect = () => ({ type: 'WS_DISCONNECT' });
export const wsDisconnected = () => ({ type: 'WS_DISCONNECTED' });

+ 82
- 0
src/modules/websocket/middleware.ts View File

@ -0,0 +1,82 @@
// A middleware that allows controlling a singleton websocket via redux actions.
//
// Largely copied from the excellent tutorial by Lina Rudashevski:
// https://dev.to/aduranil/how-to-use-websockets-with-redux-a-step-by-step-guide-to-writing-understanding-connecting-socket-middleware-to-your-project-km3
import { Middleware } from "redux";
import {
socketOpen,
socketOpened,
socketClose,
socketClosed,
socketSendMessage,
socketReceiveMessage,
} from "./slice";
import { RootState } from "../app/store";
import { SOCKET_RECEIVE_MESSAGE } from "../../constants/ActionTypes";
// The WebSocket singleton.
let socket: WebSocket | null = null;
const onOpen = dispatch => event => {
console.log('Websocket open', event.target.url);
dispatch(socketOpened(event.target.url));
};
const onClose = dispatch => () => {
dispatch(socketClosed());
};
const onMessage = dispatch => event => {
console.log(`Websocket received message: ${event.data}`);
// TODO: dispatch different actions based on payload type.
const action = { type: SOCKET_RECEIVE_MESSAGE };
try {
const { variant, fields: [data] } = JSON.parse(event.data);
if (typeof variant === "undefined") {
throw new Error('Missing "variant" field in control response');
}
action.payload = { variant, data };
} catch (err) {
action.error = true;
action.payload = err;
}
dispatch(action);
};
// See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
const middleware: Middleware<{}, RootState> = storeApi => next => action => {
if (socketOpen.match(action)) {
if (socket !== null) {
socket.close();
}
// Connect to the remote host.
socket = new WebSocket(action.payload);
// Bind websocket handlers.
socket.onmessage = onMessage(storeApi.dispatch);
socket.onclose = onClose(storeApi.dispatch);
socket.onopen = onOpen(storeApi.dispatch);
} else if (socketClose.match(action)) {
if (socket !== null) {
socket.close();
}
socket = null;
console.log('Websocket closed.');
} else if (socketSendMessage.match(action)) {
console.log('Websocket sending message', action.payload);
socket.send(JSON.stringify(action.payload));
}
return next(action);
};
export default middleware;

+ 47
- 0
src/modules/websocket/slice.ts View File

@ -0,0 +1,47 @@
import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
export enum SocketState {
Opening,
Open,
Closing,
Closed,
};
export interface SocketSliceState {
state: SocketState,
url: string | null,
};
const initialState: SocketSliceState = {
state: SocketState.Closed,
url: null,
};
export const socketSlice = createSlice({
name: 'socket',
initialState,
reducers: {
socketOpen: (state, action: PayloadAction<string>) => {
state.state = SocketState.Opening;
state.url = action.payload;
},
socketOpened: (state, action: PayoadAction<string>) => {
state.state = SocketState.Open;
state.url = action.payload;
},
socketClose: (state) => {
state.state = SocketState.Closing;
},
socketClosed: (state) => {
state.state = SocketState.Closed;
},
},
});
export const { socketOpen, socketOpened, socketClose, socketClosed } =
socketSlice.actions;
export const socketSendMessage = createAction<object>("socketSendMessage");
export default socketSlice.reducer;

+ 1
- 0
src/react-app-env.d.ts View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

+ 1
- 1
src/reducers/socket.js View File

@ -7,7 +7,7 @@ import {
import ControlRequest from "../utils/ControlRequest";
const SocketRecord = Immutable.Record({
export const SocketRecord = Immutable.Record({
state: STATE_CLOSED,
socket: undefined,
url: undefined


+ 5
- 0
src/setupTests.ts View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

+ 0
- 26
src/store/configureStore.dev.js View File

@ -1,26 +0,0 @@
//This file merely configures the store for hot reloading.
//This boilerplate file is likely to be the same for each project that uses Redux.
//With Redux, the actual stores are in /reducers.
import { createStore, compose } from "redux";
import rootReducer from "../reducers";
export default function configureStore(initialState, storeEnhancer) {
if (window.devToolsExtension) {
// Enable Redux devtools if the extension is installed in developer's
// browser.
storeEnhancer = compose(storeEnhancer, window.devToolsExtension());
}
const store = createStore(rootReducer, initialState, storeEnhancer);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextReducer = require('../reducers');
store.replaceReducer(nextReducer);
});
}
return store;
}

+ 0
- 20
src/store/configureStore.js View File

@ -1,20 +0,0 @@
import { applyMiddleware } from "redux";
import thunk from "redux-thunk";
import promise from "redux-promise";
import createLogger from "redux-logger";
let configureStore;
if (process.env.NODE_ENV === 'production') {
configureStore = require('./configureStore.prod').default;
} else {
configureStore = require('./configureStore.dev').default;
}
export default () => {
const logger = createLogger();
const initialState = undefined;
return configureStore(
initialState,
applyMiddleware(thunk, promise, logger)
);
};

+ 0
- 6
src/store/configureStore.prod.js View File

@ -1,6 +0,0 @@
import { createStore } from 'redux';
import rootReducer from '../reducers';
export default function configureStore(initialState, enhancer) {
return createStore(rootReducer, initialState, enhancer);
}

+ 2
- 0
src/utils/OrderedMap.js View File

@ -10,6 +10,8 @@ const MapRecord = Immutable.Record({
lastUpdated: 0
});
// TODO: use a regular map and a reversible name -> hash encoding (e.g. base64).
// This would entirely remove the need for this complicated logic.
class OrderedMap extends MapRecord {
constructor(arg) {
super(arg);


+ 0
- 45
tools/build.js View File

@ -1,45 +0,0 @@
// More info on Webpack's Node API here: https://webpack.github.io/docs/node.js-api.html
// Allowing console calls below since this is a build file.
/*eslint-disable no-console */
import webpack from 'webpack';
import webpackConfigBuilder from '../webpack.config';
import colors from 'colors';
import { argv as args } from 'yargs';
process.env.NODE_ENV = 'production'; // this assures React is built in prod mode and that the Babel dev config doesn't apply.
const webpackConfig = webpackConfigBuilder(process.env.NODE_ENV);
webpack(webpackConfig).run((err, stats) => {
const inSilentMode = args.s; // set to true when -s is passed on the command
if (!inSilentMode) {
console.log('Generating minified bundle for production use via Webpack...'.bold.blue);
}
if (err) { // so a fatal error occurred. Stop here.
console.log(err.bold.red);
return 1;
}
const jsonStats = stats.toJson();
if (jsonStats.hasErrors) {
return jsonStats.errors.map(error => console.log(error.red));
}
if (jsonStats.hasWarnings && !inSilentMode) {
console.log('Webpack generated the following warnings: '.bold.yellow);
jsonStats.warnings.map(warning => console.log(warning.yellow));
}
if (!inSilentMode) {
console.log(`Webpack stats: ${stats}`);
}
// if we got this far, the build succeeded.
console.log('Your app has been compiled in production mode and written to /dist. It\'s ready to roll!'.green.bold);
return 0;
});

+ 0
- 45
tools/buildHtml.js View File

@ -1,45 +0,0 @@
// This script copies src/index.html into /dist/index.html
// and adds TrackJS error tracking code for use in production
// when useTrackJs is set to true below and a trackJsToken is provided.
// This is a good example of using Node and cheerio to do a simple file transformation.
// In this case, the transformation is useful since we only want to track errors in the built production code.
// Allowing console calls below since this is a build file.
/*eslint-disable no-console */
import fs from 'fs';
import colors from 'colors';
import cheerio from 'cheerio';
const useTrackJs = true; // If you choose not to use TrackJS, just set this to false and the build warning will go away.
const trackJsToken = ''; // If you choose to use TrackJS, insert your unique token here. To get a token, go to https://trackjs.com
fs.readFile('src/index.html', 'utf8', (err, markup) => {
if (err) {
return console.log(err);
}
const $ = cheerio.load(markup);
// since a separate spreadsheet is only utilized for the production build, need to dynamically add this here.
$('head').prepend('<link rel="stylesheet" href="styles.css">');
if (useTrackJs) {
if (trackJsToken) {
const trackJsCode = `<!-- BEGIN TRACKJS Note: This should be the first <script> on the page per https://my.trackjs.com/install --><script>window._trackJs = { token: '${trackJsToken}' };</script><script src=https://d2zah9y47r7bi2.cloudfront.net/releases/current/tracker.js></script><!-- END TRACKJS -->`;
$('head').prepend(trackJsCode); // add TrackJS tracking code to the top of <head>
} else {
console.log('To track JavaScript errors, sign up for a free trial at TrackJS.com and enter your token in /tools/build.html on line 10.'.yellow);
}
}
fs.writeFile('dist/index.html', $.html(), 'utf8', function (err) {
if (err) {
return console.log(err);
}
});
console.log('index.html written to /dist'.green);
});

+ 0
- 19
tools/distServer.js View File

@ -1,19 +0,0 @@
// This file configures a web server for testing the production build
// on your local machine.
import browserSync from 'browser-sync';
// Run Browsersync
browserSync({
port: 3000,
ui: {
port: 3001
},
server: {
baseDir: 'dist'
},
files: [
'src/*.html'
]
});

+ 0
- 44
tools/srcServer.js View File

@ -1,44 +0,0 @@
// This file configures the development web server
// which supports hot reloading and synchronized testing.
// Require Browsersync along with webpack and middleware for it
import browserSync from 'browser-sync';
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackConfigBuilder from '../webpack.config';
const webpackConfig = webpackConfigBuilder('development');
const bundler = webpack(webpackConfig);
// Run Browsersync and use middleware for Hot Module Replacement
browserSync({
server: {
baseDir: 'src',
middleware: [
webpackDevMiddleware(bundler, {
// Dev middleware can't access config, so we provide publicPath
publicPath: webpackConfig.output.publicPath,
// pretty colored output
stats: { colors: true },
// Set to false to display a list of each file that is being bundled.
noInfo: true
// for other settings see
// http://webpack.github.io/docs/webpack-dev-middleware.html
}),
// bundler should be the same as above
webpackHotMiddleware(bundler)
]
},
// no need to watch '*.js' here, webpack will take care of it for us,
// including full page reloads if HMR won't work
files: [
'src/*.html'
]
});

+ 26
- 0
tsconfig.json View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

+ 0
- 83
webpack.config.js View File

@ -1,83 +0,0 @@
// For info about this file refer to webpack and webpack-hot-middleware documentation
// Rather than having hard coded webpack.config.js for each environment, this
// file generates a webpack config for the environment passed to the getConfig method.
import webpack from 'webpack';
import path from 'path';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
const developmentEnvironment = 'development' ;
const productionEnvironment = 'production';
const testEnvironment = 'test';
const getPlugins = function (env) {
const GLOBALS = {
'process.env.NODE_ENV': JSON.stringify(env),
__DEV__: env === developmentEnvironment
};
const plugins = [
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin(GLOBALS) //Tells React to build in prod mode. https://facebook.github.io/react/downloads.html
];
switch (env) {
case productionEnvironment:
plugins.push(new ExtractTextPlugin('styles.css'));
plugins.push(new webpack.optimize.DedupePlugin());
plugins.push(new webpack.optimize.UglifyJsPlugin());
break;
case developmentEnvironment:
plugins.push(new webpack.HotModuleReplacementPlugin());
plugins.push(new webpack.NoErrorsPlugin());
break;
}
return plugins;
};
const getEntry = function (env) {
const entry = [];
if (env === developmentEnvironment ) { // only want hot reloading when in dev.
entry.push('webpack-hot-middleware/client');
}
entry.push('./src/index');
return entry;
};
const getLoaders = function (env) {
const loaders = [{ test: /\.js$/, include: path.join(__dirname, 'src'), loaders: ['babel', 'eslint'] }];
if (env === productionEnvironment ) {
// generate separate physical stylesheet for production build using ExtractTextPlugin. This provides separate caching and avoids a flash of unstyled content on load.
loaders.push({test: /(\.css|\.scss)$/, loader: ExtractTextPlugin.extract("css?sourceMap!sass?sourceMap")});
} else {
loaders.push({test: /(\.css|\.scss)$/, loaders: ['style', 'css?sourceMap', 'sass?sourceMap']});
}
return loaders;
};
function getConfig(env) {
return {
debug: true,
devtool: env === productionEnvironment ? 'source-map' : 'cheap-module-eval-source-map', // more info:https://webpack.github.io/docs/build-performance.html#sourcemaps and https://webpack.github.io/docs/configuration.html#devtool
noInfo: true, // set to false to see a list of every file being bundled.
entry: getEntry(env),
target: env === testEnvironment ? 'node' : 'web', // necessary per https://webpack.github.io/docs/testing.html#compile-and-test
output: {
path: __dirname + '/dist', // Note: Physical files are only output by the production build task `npm run build`.
publicPath: '',
filename: 'bundle.js'
},
plugins: getPlugins(env),
module: {
loaders: getLoaders(env)
}
};
}
export default getConfig;

Loading…
Cancel
Save