├── .babelrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── Cargo.toml
├── README.md
├── dist
├── index.html
├── react-static-routes.js
└── robots.txt
├── install.sh
├── package.json
├── public
└── robots.txt
├── src
├── components
│ └── App.js
├── ducks
│ ├── app.js
│ ├── helper.js
│ ├── index.js
│ └── root.js
├── index.js
├── lib.rs
├── main.rs
└── routes
│ ├── 404.js
│ └── index.js
├── static.config.js
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/node,rust,macos
3 |
4 | ### macOS ###
5 | *.DS_Store
6 | .AppleDouble
7 | .LSOverride
8 |
9 | # Icon must end with two \r
10 | Icon
11 |
12 | # Thumbnails
13 | ._*
14 |
15 | # Files that might appear in the root of a volume
16 | .DocumentRevisions-V100
17 | .fseventsd
18 | .Spotlight-V100
19 | .TemporaryItems
20 | .Trashes
21 | .VolumeIcon.icns
22 | .com.apple.timemachine.donotpresent
23 |
24 | # Directories potentially created on remote AFP share
25 | .AppleDB
26 | .AppleDesktop
27 | Network Trash Folder
28 | Temporary Items
29 | .apdisk
30 |
31 | ### Node ###
32 | # Logs
33 | logs
34 | *.log
35 | npm-debug.log*
36 | yarn-debug.log*
37 | yarn-error.log*
38 |
39 | # Runtime data
40 | pids
41 | *.pid
42 | *.seed
43 | *.pid.lock
44 |
45 | # Directory for instrumented libs generated by jscoverage/JSCover
46 | lib-cov
47 |
48 | # Coverage directory used by tools like istanbul
49 | coverage
50 |
51 | # nyc test coverage
52 | .nyc_output
53 |
54 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
55 | .grunt
56 |
57 | # Bower dependency directory (https://bower.io/)
58 | bower_components
59 |
60 | # node-waf configuration
61 | .lock-wscript
62 |
63 | # Compiled binary addons (http://nodejs.org/api/addons.html)
64 | build/Release
65 |
66 | # Dependency directories
67 | node_modules/
68 | jspm_packages/
69 |
70 | # Typescript v1 declaration files
71 | typings/
72 |
73 | # Optional npm cache directory
74 | .npm
75 |
76 | # Optional eslint cache
77 | .eslintcache
78 |
79 | # Optional REPL history
80 | .node_repl_history
81 |
82 | # Output of 'npm pack'
83 | *.tgz
84 |
85 | # Yarn Integrity file
86 | .yarn-integrity
87 |
88 | # dotenv environment variables file
89 | .env
90 |
91 |
92 | ### Rust ###
93 | # Generated by Cargo
94 | # will have compiled files and executables
95 | /target/
96 |
97 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
98 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
99 | Cargo.lock
100 |
101 | # These are backup files generated by rustfmt
102 | **/*.rs.bk
103 |
104 |
105 | # End of https://www.gitignore.io/api/node,rust,macos
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
React 💕 Rust
--------------------------------------------------------------------------------
/dist/react-static-routes.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component } from 'react'
3 | import { Route } from 'react-router-dom'
4 | import universal, { setHasBabelPlugin } from 'react-universal-component'
5 |
6 | setHasBabelPlugin()
7 |
8 | const universalOptions = {
9 | loading: () => null,
10 | error: () => null,
11 | }
12 |
13 | const t_0 = universal(import('../src/routes/index'), universalOptions)
14 | const t_1 = universal(import('../src/routes/404'), universalOptions)
15 |
16 | // Template Map
17 | const templateMap = {
18 | t_0,
19 | t_1
20 | }
21 |
22 | // Template Tree
23 | const templateTree = {c:{"404":{t:"t_1"},"/":{t:"t_0"}}}
24 |
25 | // Get template for given path
26 | const getComponentForPath = path => {
27 | const parts = path === '/' ? ['/'] : path.split('/').filter(d => d)
28 | let cursor = templateTree
29 | try {
30 | parts.forEach(part => {
31 | cursor = cursor.c[part]
32 | })
33 | return templateMap[cursor.t]
34 | } catch (e) {
35 | return false
36 | }
37 | }
38 |
39 | if (typeof document !== 'undefined') {
40 | window.reactStaticGetComponentForPath = getComponentForPath
41 | }
42 |
43 | export default class Routes extends Component {
44 | render () {
45 | const { component: Comp, render, children } = this.props
46 | const renderProps = {
47 | templateMap,
48 | templateTree,
49 | getComponentForPath
50 | }
51 | if (Comp) {
52 | return (
53 |
56 | )
57 | }
58 | if (render || children) {
59 | return (render || children)(renderProps)
60 | }
61 |
62 | // This is the default auto-routing renderer
63 | return (
64 | {
65 | let Comp = getComponentForPath(props.location.pathname)
66 | if (!Comp) {
67 | Comp = getComponentForPath('404')
68 | }
69 | return Comp &&
70 | }} />
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/dist/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/lib.rs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heypoom/react-rust/d53f13de1d49af6d508ca82f71131b912906b948/src/lib.rs
--------------------------------------------------------------------------------
/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/routes/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const NotFound = () => 404 - Not Found
4 |
5 | export default NotFound
6 |
--------------------------------------------------------------------------------
/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 |
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 |
10 | React 💕 Rust
11 |
12 |
16 |
17 | {children}
18 |
19 | )
20 |
21 | // This uses `rust-native-wasm-loader` to load Rust code!
22 | const wasmLoader = {
23 | test: /\.rs$/,
24 | use: [
25 | {
26 | loader: 'babel-loader',
27 | options: {
28 | compact: true,
29 | },
30 | },
31 | {
32 | loader: 'rust-native-wasm-loader',
33 | options: {
34 | gc: true,
35 | release: true,
36 | cargoWeb: true,
37 | name: 'static/wasm/[name].[hash:8].wasm',
38 | },
39 | },
40 | ],
41 | }
42 |
43 | const nodeConfig = {
44 | dgram: 'empty',
45 | fs: 'empty',
46 | net: 'empty',
47 | tls: 'empty',
48 | child_process: 'empty',
49 | }
50 |
51 | function webpack(config, args) {
52 | config.module.rules[0].oneOf.unshift(wasmLoader)
53 | config.node = nodeConfig
54 |
55 | return config
56 | }
57 |
58 | export default {
59 | webpack,
60 | getSiteProps: () => ({
61 | title: 'WebAssembly with Rust',
62 | }),
63 | getRoutes: async () => [
64 | {
65 | path: '/',
66 | component: 'src/routes/index',
67 | },
68 | {
69 | is404: true,
70 | component: 'src/routes/404',
71 | },
72 | ],
73 | renderToHtml: (render, Comp, meta) => {
74 | const html = render()
75 | meta.css = extractCritical(html).css
76 | return html
77 | },
78 | Document,
79 | }
80 |
--------------------------------------------------------------------------------