Misc scribbles

Using Rust/WASM in a monorepo with create-react-app

2022-08-22

Behold, the buzzwords:

The main goal here: To use Rust + WASM in a react app, inside a monorepo.

TLDR: visit the final product! https://github.com/cmdcolin/rust_react_monorepo_template. It is also deployed live here https://cmdcolin.github.io/rust_react_monorepo_template

#Steps to create this type of integration from scratch

#Create repo

mkdir template
cd template
git init

#Create root package.json

Then put this in the monorepo's root package.json

{
  "private": true,
  "workspaces": ["hello-wasm", "app"]
}

This sets our repo up as a "monorepo" with two "workspaces". one will be the wasm code, in hello-wasm, one will be an instance of create-react-app

#Add a create-react-app instance inside the monorepo

npx create-react-app --template typescript app

This will make an app subfolder inside our monorepo

#Download the hello world rust wasm-bindgen example and put it in a folder named hello-wasm

Download https://github.com/rustwasm/wasm-bindgen/tree/main/examples/hello_world to the hello-wasm folder

This link can help https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Frustwasm%2Fwasm-bindgen%2Ftree%2Fmain%2Fexamples%2Fhello_world

#Add some extra fields to the package.json in the hello-wasm folder

{
  "name": "hello-wasm",
  "version": "1.0.0",
  "files": ["pkg"],
  "main": "pkg/index.js"
  ... rest
}

#Modify the hello-wasm example to return a value instead of making an alert

I changed the rust code to return a String value instead of making an alert box

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello {}", name)
}

#Build the hello-wasm pkg

Go into the hello-wasm folder and run yarn build. This creates a directory named pkg which has .wasm files and .js files. Now, the hello-wasm folder is effectively a node package. We could publish this to NPM (see footnote 1)

#Add the hello-wasm package to the app dependencies

Add "hello-wasm":"^1.0.0" to the dependencies array in app/package.json. This will refer to our local monorepo's rust wasm package!

#Create craco config for create-react-app

As of writing, with webpack v5/create-react-app v5, you have to customize the create-react-app to add extra webpack flags.

So, yarn add @craco/craco in the app folder, then create this craco.config.js

module.exports = {
  webpack: {
    configure: config => {
      const wasmExtensionRegExp = /\.wasm$/
      config.resolve.extensions.push('.wasm')
      config.experiments = {
        syncWebAssembly: true,
      }
 
      config.module.rules.forEach(rule => {
        ;(rule.oneOf || []).forEach(oneOf => {
          if (oneOf.type === 'asset/resource') {
            oneOf.exclude.push(wasmExtensionRegExp)
          }
        })
      })
 
      return config
    },
  },
}

Note: this thread helped me to create the craco config https://github.com/Emurgo/cardano-serialization-lib/issues/295

Also see footnote 2 for more info

#Final step: Use async import() to import the hello-wasm greeting

We use a useEffect hook to import the code asynchronously, and can call our rust function, greet, from javascript!

function App() {
  const [greeting, setGreeting] = useState<string>()
  useEffect(() => {
    ;(async () => {
      try {
        const wasm = await import('hello-wasm')
        const greeting = wasm.greet('Colin')
        setGreeting(greeting)
      } catch (e) {
        console.error(e)
      }
    })()
  }, [])
 
  return (
    <div>
      <h1>rust monorepo wasm demo</h1>
      <h2>Greeting from wasm: {!greeting ? 'Loading...' : greeting}</h2>
    </div>
  )
}

In order to greet an arbitrary person, I modified this slightly in the live demo. See https://github.com/cmdcolin/rust_react_monorepo_template/blob/master/app/src/App.tsx

#Run the app!

Go into the app folder, and then run yarn start

#Result!

A screenshot of the app, showing the string "Hello Colin" which is generated via rust and wasm

#Conclusion

My main aim was to demonstrate creating a "simple" monorepo setup showing how you can integrate Rust+WASM and React. Feel free to ask me any questions and go check out the repo!

https://github.com/cmdcolin/rust_react_monorepo_template

#Other resources

This article is quite helpful also, but uses a file:/ reference in their package.json while my approach uses a monorepo, it is fundamentally quite similar though! https://tkat0.github.io/posts/how-to-create-a-react-app-with-rust-and-wasm

#Footnote 1: The hello-wasm folder IS a npm package with wasm files

The hello-wasm folder can be published to NPM by itself. When consumers of the package import the module, they would receive pkg/index.js from the main field in package.json, and then pkg/index.js in turn imports the index.wasm file. Then it is up to the consumers bundler to package that correctly.

#Footnote 2: Bundlers and wasm

As of writing, I am using webpack v5 (part of create-react-app v5), which has "native support" for wasm. Still, it is hidden behind a flag called "experiments" (see first google result for webpack wasm here https://webpack.js.org/configuration/experiments/) so I use @craco/craco to modify the webpack config of create-react-app v5 to add this.

Note also: The first time I wrote this, I used webpack v4, which used a slightly different workflow (used a special webpack loader called wasm-loader)

You can also likely use similar techniques described in this article to incorporate into next.js since it also uses webpack. If you have info on how other bundlers use wasm, feel free to leave a comment.

#Footnote 3: Why do I have to use async imports?

Fundamentally, the .wasm file has to be fetched asynchronously before it can be run (it is not in my experience e.g. embedded as binary data inside a js file) which means it would be difficult to use the wasm code as a synchronous import.

There are hints that this may be possible but it would rely on the bundler embedding the wasm code in the js itself, or maybe top-level-await. If anyone has more info, feel free to leave a comment!

#Footnote 4: Build setup

The hello-wasm package does not automatically recompile when we are running e.g. yarn start in the app folder. Therefore, changes to the rust requires you to manually run yarn build in the hello-wasm folder. Just something to be aware of

#Footnote 5: My first experience with trying to make this work was rocky!

I first created an example of rust+wasm+react almost two years ago when creating a fractal viewer https://github.com/cmdcolin/logistic_chaos_map and it has some development notes on the stumbling blocks I faced https://github.com/cmdcolin/logistic_chaos_map/blob/master/NOTES.md

#Footnote 6: I thought you said typescript too

Yep! The hello-wasm example generates typescript .d.ts files! Check out the hello-wasm/pkg/ folder after you build it! This was none of my doing, just a built-in feature. PS: I highly recommend inspecting the pkg folder that is produced in the hello-wasm build to help understand the details. I also recommend reading the https://rustwasm.github.io/wasm-bindgen/ docs and if you are getting started with rust, read the Rust Book along with doing rustlings https://github.com/rust-lang/rustlings

#Footnote 7: Another resource

This article was posted on reddit and is also a great resource especially about sync vs async webpack loading schemes for wasm

https://canvasapp.com/blog/building-modern-web-apps-with-rust-wasm-and-webpack/