├── src ├── lib.rs ├── routes │ ├── 404.js │ └── index.js ├── ducks │ ├── root.js │ ├── app.js │ ├── index.js │ └── helper.js ├── main.rs ├── index.js └── components │ └── App.js ├── dist ├── robots.txt ├── index.html └── react-static-routes.js ├── public └── robots.txt ├── Cargo.toml ├── .editorconfig ├── README.md ├── .babelrc ├── install.sh ├── .eslintrc.json ├── package.json ├── static.config.js └── .gitignore /src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /src/routes/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NotFound = () =>
404 - Not Found
4 | 5 | export default NotFound 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Phoomparin Mano"] 3 | name = "react-rust" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | stdweb = "0.3.0" 8 | 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebAssembly Modules in React Apps via Rust 2 | 3 | We're going to use Rust, Cargo Web and Stdweb to provide WebAssembly Modules, 4 | then use them in react-static. 5 | 6 | ### Usage 7 | 8 | ```sh 9 | # Install Rust and Cargo Web 10 | ./install.sh 11 | 12 | # Install Node Modules 13 | yarn --dev 14 | 15 | # Start it up! 16 | yarn start 17 | ``` 18 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-static/.babelrc", 3 | "env": { 4 | "production": { 5 | "plugins": [ 6 | ["emotion", {"sourceMap": false, "hoist": true, "autoLabel": true}] 7 | ] 8 | }, 9 | "development": { 10 | "plugins": [ 11 | ["emotion", {"sourceMap": true, "hoist": true, "autoLabel": true}] 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | React 💕 Rust
-------------------------------------------------------------------------------- /src/ducks/root.js: -------------------------------------------------------------------------------- 1 | import {all} from 'redux-saga/effects' 2 | import storage from 'redux-persist/lib/storage' 3 | import {persistCombineReducers} from 'redux-persist' 4 | 5 | import app, {appWatcherSaga} from './app' 6 | 7 | const config = {key: 'root', storage} 8 | 9 | export const reducers = persistCombineReducers(config, {app}) 10 | 11 | export function* rootSaga() { 12 | yield all([appWatcherSaga()]) 13 | } 14 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | # Install Rust 2 | curl https://sh.rustup.rs -sSf | sh 3 | rustup update 4 | 5 | # Install Nightly Rust 6 | rustup default nightly 7 | rustup update nightly 8 | 9 | # Adds the WebAssembly Target 10 | rustup target add wasm32-unknown-unknown --toolchain nightly 11 | 12 | # Install wasm-gc 13 | cargo install --force --git https://github.com/alexcrichton/wasm-gc 14 | 15 | # Install cargo-web via stable Rust 16 | # NOTE: You need stable Rust to install cargo-web on macOS, for now. 17 | cargo +stable install cargo-web 18 | 19 | # Test to see if it actually works 20 | cargo web build --message-format=json --target wasm32-unknown-unknown --release 21 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate stdweb; 3 | 4 | use stdweb::web::*; 5 | 6 | pub static SECRET_KEY: &str = "SUPER_DUPER_SECRET_KEY"; 7 | 8 | fn secret() -> String { 9 | let window = window(); 10 | let storage = window.local_storage(); 11 | 12 | storage.insert(SECRET_KEY, "I'm a Hog."); 13 | 14 | String::from("Hot Bog!") 15 | } 16 | 17 | fn main() { 18 | stdweb::initialize(); 19 | 20 | js! { 21 | console.log("Hello from Rust!"); 22 | 23 | fetch("https://jsonplaceholder.typicode.com").then(console.log); 24 | 25 | Module.exports.secret = @{secret}; 26 | Module.exports.SECRET_KEY = @{SECRET_KEY}; 27 | } 28 | 29 | stdweb::event_loop() 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import {AppContainer} from 'react-hot-loader' 4 | 5 | import App from './components/App' 6 | 7 | if (typeof document !== 'undefined') { 8 | const renderMethod = module.hot ? ReactDOM.render : ReactDOM.hydrate 9 | 10 | const render = Comp => { 11 | renderMethod( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ) 17 | } 18 | 19 | render(App) 20 | 21 | if (module.hot) { 22 | module.hot.accept('./components/App', () => { 23 | render(require('./components/App').default) 24 | }) 25 | } 26 | } 27 | 28 | export default App 29 | 30 | -------------------------------------------------------------------------------- /src/ducks/app.js: -------------------------------------------------------------------------------- 1 | import {takeEvery, call, put} from 'redux-saga/effects' 2 | 3 | import {createReducer, Creator} from './helper' 4 | import wasm from '../main.rs' 5 | 6 | export const GET_SECRET = 'GET_SECRET' 7 | export const STORE_SECRET = 'STORE_SECRET' 8 | 9 | export const getSecret = Creator(GET_SECRET) 10 | export const storeSecret = Creator(STORE_SECRET) 11 | 12 | export function* getSecretSaga() { 13 | const lib = yield wasm 14 | 15 | const name = lib.secret() 16 | const phrase = localStorage.getItem(lib.SECRET_KEY) 17 | 18 | yield put(storeSecret({name, phrase})) 19 | } 20 | 21 | export function* appWatcherSaga() { 22 | yield takeEvery(GET_SECRET, getSecretSaga) 23 | } 24 | 25 | const initial = { 26 | secret: {}, 27 | } 28 | 29 | export default createReducer(initial, state => ({ 30 | [STORE_SECRET]: secret => ({ 31 | secret, 32 | }), 33 | })) 34 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Provider} from 'react-redux' 3 | import {Router} from 'react-static' 4 | import {lifecycle} from 'recompose' 5 | import {injectGlobal} from 'react-emotion' 6 | 7 | import Routes from 'react-static-routes' 8 | 9 | import createStore from '../ducks' 10 | 11 | const store = createStore() 12 | 13 | const App = () => ( 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | 21 | const enhance = lifecycle({ 22 | componentWillMount() { 23 | injectGlobal` 24 | body { 25 | margin: 0; 26 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 27 | color: #555; 28 | background: rgb(251, 252, 255); 29 | } 30 | 31 | h1, h2 { 32 | font-weight: 300; 33 | } 34 | ` 35 | }, 36 | }) 37 | 38 | export default enhance(App) 39 | -------------------------------------------------------------------------------- /src/ducks/index.js: -------------------------------------------------------------------------------- 1 | import {compose, createStore, applyMiddleware} from 'redux' 2 | import createSagaMiddleware from 'redux-saga' 3 | import {persistStore} from 'redux-persist' 4 | 5 | import {reducers, rootSaga} from './root' 6 | 7 | /* eslint no-undef: 0 */ 8 | 9 | export default () => { 10 | const saga = createSagaMiddleware() 11 | const middleware = [saga] 12 | let composeEnhancers = compose 13 | 14 | if (typeof window !== 'undefined') { 15 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 16 | } 17 | 18 | const store = createStore( 19 | reducers, 20 | composeEnhancers(applyMiddleware(...middleware)), 21 | ) 22 | 23 | // NOTE: Uncomment this to persist to Redux Store 24 | // persistStore(store) 25 | 26 | if (module.hot) { 27 | module.hot.accept(() => { 28 | const nextReducers = require('./root').reducers 29 | store.replaceReducer(nextReducers) 30 | }) 31 | } 32 | 33 | saga.run(rootSaga) 34 | 35 | return store 36 | } 37 | -------------------------------------------------------------------------------- /src/ducks/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a reducer from an initial state and a handler function. 3 | * @param {object} initialState 4 | * @param {object} handlers - handler function which returns an object 5 | * @example state => ({ SET_NAME: name => ({...state, name}) }) 6 | */ 7 | export function createReducer(initialState, handlers) { 8 | return (state = initialState, action) => 9 | handlers(state)[action.type] 10 | ? handlers(state)[action.type](action.payload) 11 | : state 12 | } 13 | 14 | /** 15 | * Creates an action creator. 16 | * Will also put each arguments into the payload, if any. 17 | * @param {string} type - action type 18 | * @param {...string} [argNames] - action argument names 19 | * @return {function} Returns the Action Creator Function 20 | */ 21 | export function Creator(type, ...argNames) { 22 | if (argNames.length > 0) { 23 | return (...args) => { 24 | const payload = {} 25 | argNames.forEach((arg, index) => { 26 | payload[argNames[index]] = args[index] 27 | }) 28 | return {type, payload} 29 | } 30 | } 31 | return payload => (payload ? {type, payload} : {type}) 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "plugin:react/recommended", 6 | "prettier", 7 | "prettier/react", 8 | "prettier/standard" 9 | ], 10 | "plugins": ["react", "prettier", "standard"], 11 | "parserOptions": { 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "env": { 18 | "es6": true, 19 | "node": true, 20 | "browser": true 21 | }, 22 | "rules": { 23 | "prettier/prettier": [ 24 | "error", 25 | { 26 | "printWidth": 80, 27 | "tabWidth": 2, 28 | "singleQuote": true, 29 | "trailingComma": "all", 30 | "bracketSpacing": false, 31 | "semi": false, 32 | "useTabs": false, 33 | "parser": "babylon", 34 | "jsxBracketSameLine": true 35 | } 36 | ], 37 | "eqeqeq": ["error", "always"], 38 | "space-before-function-paren": 0, 39 | "generator-star-spacing": 0, 40 | "react/prop-types": 0, 41 | "react/display-name": 0, 42 | "import/no-unresolved": [1, {"commonjs": true, "amd": true}], 43 | "import/named": 2, 44 | "import/namespace": 2, 45 | "import/default": 2, 46 | "import/export": 2 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-static-example-basic", 3 | "version": "1.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "react-static start", 8 | "build": "react-static build", 9 | "serve": "serve dist -p 3000" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.17.1", 13 | "emotion": "^8.0.12", 14 | "polished": "^1.9.0", 15 | "react": "^16.2.0", 16 | "react-dom": "^16.0.0", 17 | "react-emotion": "^8.0.12", 18 | "react-redux": "^5.0.6", 19 | "react-router": "^4.2.0", 20 | "react-static": "^4.9.0-beta.5", 21 | "recompose": "^0.26.0", 22 | "redux": "^3.7.2", 23 | "redux-persist": "^5.5.0", 24 | "redux-saga": "^0.16.0", 25 | "reselect": "^3.0.1" 26 | }, 27 | "devDependencies": { 28 | "babel-eslint": "^8.2.1", 29 | "emotion-server": "^8.0.12", 30 | "eslint": "^4.16.0", 31 | "eslint-config-prettier": "^2.9.0", 32 | "eslint-config-react-tools": "1.x.x", 33 | "eslint-config-standard": "^11.0.0-beta.0", 34 | "eslint-config-standard-react": "^5.0.0", 35 | "eslint-plugin-import": "^2.8.0", 36 | "eslint-plugin-node": "^5.2.1", 37 | "eslint-plugin-prettier": "^2.5.0", 38 | "eslint-plugin-promise": "^3.6.0", 39 | "eslint-plugin-react": "^7.5.1", 40 | "eslint-plugin-standard": "^3.0.1", 41 | "prettier": "^1.10.2", 42 | "rust-native-wasm-loader": "^0.2.7", 43 | "serve": "^6.1.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, {css} from 'react-emotion' 3 | import {connect} from 'react-redux' 4 | 5 | import {getSecret} from '../ducks/app' 6 | 7 | const Container = styled.div` 8 | padding: 2em; 9 | margin: 2em; 10 | 11 | text-align: center; 12 | box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 25px; 13 | background: white; 14 | ` 15 | 16 | // prettier-ignore 17 | const Header = styled.h1` 18 | margin: 0; 19 | margin-bottom: 0.5em; 20 | 21 | ${props => props.big && css` 22 | font-size: 2.2em; 23 | font-weight: 400; 24 | `}; 25 | ` 26 | 27 | const Button = styled.button` 28 | background: #2d2d30; 29 | border-radius: 4px; 30 | border: none; 31 | box-shadow: rgba(51, 51, 51, 0.6) 0px 4px 20px -2px; 32 | color: white; 33 | cursor: pointer; 34 | font-size: 1.08em; 35 | outline: none; 36 | padding: 0.4em 0.8em; 37 | transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); 38 | 39 | &:hover { 40 | color: #555; 41 | background: white; 42 | } 43 | ` 44 | 45 | const Landing = ({secret, getSecret}) => ( 46 | 47 |
Knock, Knock. {secret.phrase}
48 |
{secret.name}
49 | 50 |
51 | ) 52 | 53 | const mapStateToProps = state => ({ 54 | secret: state.app.secret, 55 | }) 56 | 57 | const enhance = connect(mapStateToProps, {getSecret}) 58 | 59 | export default enhance(Landing) 60 | -------------------------------------------------------------------------------- /static.config.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {extractCritical} from 'emotion-server' 3 | 4 | const Document = ({Html, Head, Body, children, renderMeta}) => ( 5 | 6 | 7 | 8 | 9 |