mirror of
https://github.com/SrIzan10/starters.git
synced 2026-05-01 11:05:16 +00:00
Rust WASM Starter (#112)
This commit is contained in:
35
examples/rust-wasm/.gitignore
vendored
Normal file
35
examples/rust-wasm/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
|
||||
# production
|
||||
build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Rust
|
||||
crate/pkg
|
||||
crate/target
|
||||
6
examples/rust-wasm/.prettierrc
Normal file
6
examples/rust-wasm/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
24
examples/rust-wasm/Dockerfile
Normal file
24
examples/rust-wasm/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM rustlang/rust:nightly
|
||||
|
||||
# Install node
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& npm i -g yarn
|
||||
|
||||
# Install wasm-pack
|
||||
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
# Install deps
|
||||
RUN yarn
|
||||
|
||||
# Build
|
||||
RUN yarn build
|
||||
|
||||
# Start
|
||||
CMD [ "yarn", "start" ]
|
||||
38
examples/rust-wasm/README.md
Normal file
38
examples/rust-wasm/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: WASM Rust
|
||||
description: Game of life implemented in Rust and served with NextJS
|
||||
tags:
|
||||
- wasm
|
||||
- rust
|
||||
- next
|
||||
- typescript
|
||||
- tailwind
|
||||
---
|
||||
|
||||
# WebAssembly Rust
|
||||
|
||||
This example is a implements [Conway's Game of
|
||||
Life](https://rustwasm.github.io/book/game-of-life/introduction.html) tutorial
|
||||
in Rust and WebAssembly. The frontend is served as a
|
||||
[NextJS](https://nextjs.org/) app with TypeScript and Tailwind.
|
||||
|
||||
[](https://railway.app/new?template=https%3A%2F%2Fgithub.com%2Frailwayapp%2Fexamples%2Ftree%2Fmaster%2Fexamples%2Frust-wasm)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Rust
|
||||
- WASM
|
||||
- TypeScript
|
||||
- NextJS
|
||||
- Tailwind
|
||||
|
||||
## 💁♀️ How to use
|
||||
|
||||
- Ensure you have the [Rust toolchain](https://www.rust-lang.org/) setup
|
||||
- Install [wasm-pack](https://rustwasm.github.io/wasm-pack/)
|
||||
- Install deps with `yarn`
|
||||
- Run app in development `yarn dev`
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
This starter implements part of the official [Rust WASM tutorial](https://rustwasm.github.io/book/game-of-life/introduction.html).
|
||||
25
examples/rust-wasm/package.json
Normal file
25
examples/rust-wasm/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "wasm-starter",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "yarn build:wasm && concurrently -n crate,www -c magenta,cyan \"yarn dev:www\" \"yarn dev:wasm\" ",
|
||||
"dev:www": "yarn workspace www dev",
|
||||
"dev:wasm": "yarn workspace crate dev",
|
||||
"build": "yarn build:wasm && yarn build:www",
|
||||
"build:www": "yarn workspace www build",
|
||||
"build:wasm": "yarn workspace crate build",
|
||||
"start": "yarn workspace www start",
|
||||
"clean": "wsrun --exclude-missing clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^6.1.0",
|
||||
"wsrun": "^5.2.4",
|
||||
"prettier": "^2.2.1"
|
||||
}
|
||||
}
|
||||
6
examples/rust-wasm/packages/crate/.gitignore
vendored
Normal file
6
examples/rust-wasm/packages/crate/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bin/
|
||||
pkg/
|
||||
wasm-pack.log
|
||||
36
examples/rust-wasm/packages/crate/Cargo.toml
Normal file
36
examples/rust-wasm/packages/crate/Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "crate"
|
||||
version = "0.1.0"
|
||||
authors = ["Jake Runzer <jakerunzer@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3.51"
|
||||
cargo-watch = "7.8.0"
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.1", optional = true }
|
||||
|
||||
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
|
||||
# compared to the default allocator's ~10K. It is slower than the default
|
||||
# allocator, however.
|
||||
#
|
||||
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
|
||||
wee_alloc = { version = "0.4.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.2"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
25
examples/rust-wasm/packages/crate/LICENSE_MIT
Normal file
25
examples/rust-wasm/packages/crate/LICENSE_MIT
Normal file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2018 Jake Runzer <jakerunzer@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
69
examples/rust-wasm/packages/crate/README.md
Normal file
69
examples/rust-wasm/packages/crate/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
<div align="center">
|
||||
|
||||
<h1><code>wasm-pack-template</code></h1>
|
||||
|
||||
<strong>A template for kick starting a Rust and WebAssembly project using <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a>.</strong>
|
||||
|
||||
<p>
|
||||
<a href="https://travis-ci.org/rustwasm/wasm-pack-template"><img src="https://img.shields.io/travis/rustwasm/wasm-pack-template.svg?style=flat-square" alt="Build Status" /></a>
|
||||
</p>
|
||||
|
||||
<h3>
|
||||
<a href="https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html">Tutorial</a>
|
||||
<span> | </span>
|
||||
<a href="https://discordapp.com/channels/442252698964721669/443151097398296587">Chat</a>
|
||||
</h3>
|
||||
|
||||
<sub>Built with 🦀🕸 by <a href="https://rustwasm.github.io/">The Rust and WebAssembly Working Group</a></sub>
|
||||
</div>
|
||||
|
||||
## About
|
||||
|
||||
[**📚 Read this template tutorial! 📚**][template-docs]
|
||||
|
||||
This template is designed for compiling Rust libraries into WebAssembly and
|
||||
publishing the resulting package to NPM.
|
||||
|
||||
Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other
|
||||
templates and usages of `wasm-pack`.
|
||||
|
||||
[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html
|
||||
[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html
|
||||
|
||||
## 🚴 Usage
|
||||
|
||||
### 🐑 Use `cargo generate` to Clone this Template
|
||||
|
||||
[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate)
|
||||
|
||||
```
|
||||
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
|
||||
cd my-project
|
||||
```
|
||||
|
||||
### 🛠️ Build with `wasm-pack build`
|
||||
|
||||
```
|
||||
wasm-pack build
|
||||
```
|
||||
|
||||
### 🔬 Test in Headless Browsers with `wasm-pack test`
|
||||
|
||||
```
|
||||
wasm-pack test --headless --firefox
|
||||
```
|
||||
|
||||
### 🎁 Publish to NPM with `wasm-pack publish`
|
||||
|
||||
```
|
||||
wasm-pack publish
|
||||
```
|
||||
|
||||
## 🔋 Batteries Included
|
||||
|
||||
* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating
|
||||
between WebAssembly and JavaScript.
|
||||
* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook)
|
||||
for logging panic messages to the developer console.
|
||||
* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized
|
||||
for small code size.
|
||||
13
examples/rust-wasm/packages/crate/package.json
Normal file
13
examples/rust-wasm/packages/crate/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "crate",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"author": "Jake Runzer <jakerunzer@gmail.com>",
|
||||
"license": "MIT",
|
||||
"main": "pkg/crate.js",
|
||||
"scripts": {
|
||||
"dev": "cargo watch -i .gitignore -i 'pkg/*' -s 'wasm-pack build'",
|
||||
"build": "wasm-pack build",
|
||||
"clean": "cargo clean && rm -rf pkg"
|
||||
}
|
||||
}
|
||||
168
examples/rust-wasm/packages/crate/src/lib.rs
Normal file
168
examples/rust-wasm/packages/crate/src/lib.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
mod utils;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use js_sys::Math::random;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
|
||||
// allocator.
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn alert(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn value() -> String {
|
||||
"Hello from Rust!".into()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Cell {
|
||||
Dead = 0,
|
||||
Alive = 1,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Universe {
|
||||
width: u32,
|
||||
height: u32,
|
||||
cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
fn toggle(&mut self) {
|
||||
*self = match *self {
|
||||
Cell::Dead => Cell::Alive,
|
||||
Cell::Alive => Cell::Dead,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Universe {
|
||||
pub fn new() -> Universe {
|
||||
let width = 64;
|
||||
let height = 64;
|
||||
|
||||
let cells = (0..width * height).map(|_i| new_cell()).collect();
|
||||
|
||||
Universe {
|
||||
width,
|
||||
height,
|
||||
cells,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_cell(&mut self, row: u32, column: u32) {
|
||||
let idx = self.get_index(row, column);
|
||||
self.cells[idx].toggle()
|
||||
}
|
||||
|
||||
pub fn restart(&mut self) {
|
||||
self.cells = (0..self.width * self.height).map(|_i| new_cell()).collect();
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn cells(&self) -> *const Cell {
|
||||
self.cells.as_ptr()
|
||||
}
|
||||
|
||||
fn get_index(&self, row: u32, column: u32) -> usize {
|
||||
(row * self.width + column) as usize
|
||||
}
|
||||
|
||||
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
|
||||
let mut count = 0;
|
||||
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
|
||||
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
|
||||
if delta_row == 0 && delta_col == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let neighbor_row = (row + delta_row) % self.height;
|
||||
let neighbor_col = (column + delta_col) % self.height;
|
||||
let idx = self.get_index(neighbor_row, neighbor_col);
|
||||
count += self.cells[idx] as u8;
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
let mut next = self.cells.clone();
|
||||
|
||||
for row in 0..self.height {
|
||||
for col in 0..self.width {
|
||||
let idx = self.get_index(row, col);
|
||||
let cell = self.cells[idx];
|
||||
let live_neighbors = self.live_neighbor_count(row, col);
|
||||
|
||||
let next_cell = match (cell, live_neighbors) {
|
||||
// Rule 1: Any live cell with fewer than two live neighbours
|
||||
// dies, as if caused by underpopulation.
|
||||
(Cell::Alive, x) if x < 2 => Cell::Dead,
|
||||
|
||||
// Rule 2: Any live cell with two or three live neighbours
|
||||
// lives on to the next generation.
|
||||
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
|
||||
|
||||
// Rule 3: Any live cell with more than three live
|
||||
// neighbours dies, as if by overpopulation.
|
||||
(Cell::Alive, x) if x > 3 => Cell::Dead,
|
||||
|
||||
// Rule 4: Any dead cell with exactly three live neighbours
|
||||
// becomes a live cell, as if by reproduction.
|
||||
(Cell::Dead, 3) => Cell::Alive,
|
||||
|
||||
// All other cells remain in the same state.
|
||||
(otherwise, _) => otherwise,
|
||||
};
|
||||
|
||||
next[idx] = next_cell;
|
||||
}
|
||||
}
|
||||
|
||||
self.cells = next;
|
||||
}
|
||||
}
|
||||
|
||||
fn new_cell() -> Cell {
|
||||
if random() < 0.5 {
|
||||
Cell::Alive
|
||||
} else {
|
||||
Cell::Dead
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Universe {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for line in self.cells.as_slice().chunks(self.width as usize) {
|
||||
for &cell in line {
|
||||
let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
|
||||
write!(f, "{}", symbol)?;
|
||||
}
|
||||
write!(f, "\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
10
examples/rust-wasm/packages/crate/src/utils.rs
Normal file
10
examples/rust-wasm/packages/crate/src/utils.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub fn set_panic_hook() {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
//
|
||||
// For more details see
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
13
examples/rust-wasm/packages/crate/tests/web.rs
Normal file
13
examples/rust-wasm/packages/crate/tests/web.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
//! Test suite for the Web and headless browsers.
|
||||
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn pass() {
|
||||
assert_eq!(1 + 1, 2);
|
||||
}
|
||||
4
examples/rust-wasm/packages/www/.babelrc.js
Normal file
4
examples/rust-wasm/packages/www/.babelrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
presets: [["next/babel", { "preset-react": { runtime: "automatic" } }]],
|
||||
plugins: ["babel-plugin-macros", ["styled-components", { ssr: true }]],
|
||||
};
|
||||
37
examples/rust-wasm/packages/www/.gitignore
vendored
Normal file
37
examples/rust-wasm/packages/www/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
.blog_index_data
|
||||
.blog_index_data_previews
|
||||
22
examples/rust-wasm/packages/www/LICENSE
Normal file
22
examples/rust-wasm/packages/www/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jake Runzer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
21
examples/rust-wasm/packages/www/README.md
Normal file
21
examples/rust-wasm/packages/www/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# NextJS Starter
|
||||
|
||||
This is my goto starter for new NextJS projects. It has
|
||||
|
||||
- [Typescript](https://www.typescriptlang.org/)
|
||||
- [Tailwind](https://tailwindcss.com/) + [Twin](https://github.com/ben-rogerson/twin.macro) for styling
|
||||
- Light/dark modes
|
||||
- [Fathom](https://usefathom.com/) for analytics
|
||||
- Simple page/layout structure
|
||||
|
||||
## Development
|
||||
|
||||
This is a [Next.js](https://nextjs.org/) site
|
||||
|
||||
```bash
|
||||
# Install deps
|
||||
yarn
|
||||
|
||||
# Start development server
|
||||
yarn dev
|
||||
```
|
||||
2
examples/rust-wasm/packages/www/next-env.d.ts
vendored
Normal file
2
examples/rust-wasm/packages/www/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
11
examples/rust-wasm/packages/www/next.config.js
Normal file
11
examples/rust-wasm/packages/www/next.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
// Unset client-side javascript that only works server-side
|
||||
// https://github.com/vercel/next.js/issues/7755#issuecomment-508633125
|
||||
config.node = { fs: "empty", module: "empty" };
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
42
examples/rust-wasm/packages/www/package.json
Normal file
42
examples/rust-wasm/packages/www/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "www",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start --port ${PORT-3000}",
|
||||
"tsc": "tsc -p .",
|
||||
"clean": "rm -rf .next"
|
||||
},
|
||||
"dependencies": {
|
||||
"crate": "0.0.1",
|
||||
"next": "10.0.9",
|
||||
"next-seo": "^4.23.0",
|
||||
"next-themes": "^0.0.14",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-feather": "^2.0.9",
|
||||
"styled-components": "^5.2.1",
|
||||
"theme-custom-properties": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/styled-components": "^5.1.9",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"postcss": "^8.2.8",
|
||||
"react-is": "^17.0.2",
|
||||
"tailwindcss": "^2.0.4",
|
||||
"ts-node": "^9.1.1",
|
||||
"twin.macro": "^2.3.1",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"babelMacros": {
|
||||
"twin": {
|
||||
"preset": "styled-components"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
examples/rust-wasm/packages/www/postcss.config.js
Normal file
3
examples/rust-wasm/packages/www/postcss.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: ["tailwindcss", "autoprefixer"],
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
examples/rust-wasm/packages/www/public/apple-touch-icon.png
Normal file
BIN
examples/rust-wasm/packages/www/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
examples/rust-wasm/packages/www/public/favicon-16x16.png
Normal file
BIN
examples/rust-wasm/packages/www/public/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 432 B |
BIN
examples/rust-wasm/packages/www/public/favicon-32x32.png
Normal file
BIN
examples/rust-wasm/packages/www/public/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 840 B |
BIN
examples/rust-wasm/packages/www/public/favicon.ico
Normal file
BIN
examples/rust-wasm/packages/www/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
19
examples/rust-wasm/packages/www/public/site.webmanifest
Normal file
19
examples/rust-wasm/packages/www/public/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Jake",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#e41b61",
|
||||
"background_color": "#f8f9fa",
|
||||
"display": "standalone"
|
||||
}
|
||||
177
examples/rust-wasm/packages/www/src/components/Life.tsx
Normal file
177
examples/rust-wasm/packages/www/src/components/Life.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { Cell, Universe } from "crate";
|
||||
import { memory } from "crate/pkg/crate_bg.wasm";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Play, Pause, RefreshCw } from "react-feather";
|
||||
import tw from "twin.macro";
|
||||
import { Link } from "./Link";
|
||||
|
||||
const CELL_SIZE = 10; // px
|
||||
const GRID_COLOR = "#CCCCCC";
|
||||
const DEAD_COLOR = "#FFFFFF";
|
||||
const ALIVE_COLOR = "#000000";
|
||||
|
||||
const Life: React.FC = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const universeRef = useRef(Universe.new());
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
const drawGrid = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
) => {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = GRID_COLOR;
|
||||
|
||||
// Vertical lines.
|
||||
for (let i = 0; i <= width; i++) {
|
||||
ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
|
||||
ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
|
||||
}
|
||||
|
||||
// Horizontal lines.
|
||||
for (let j = 0; j <= height; j++) {
|
||||
ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
|
||||
ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const getIndex = (row: number, column: number, width: number) => {
|
||||
return row * width + column;
|
||||
};
|
||||
|
||||
const drawCells = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
universe: Universe,
|
||||
width: number,
|
||||
height: number,
|
||||
) => {
|
||||
const cellsPtr = universe.cells();
|
||||
const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
const idx = getIndex(row, col, width);
|
||||
|
||||
ctx.fillStyle = cells[idx] === Cell.Dead ? DEAD_COLOR : ALIVE_COLOR;
|
||||
|
||||
ctx.fillRect(
|
||||
col * (CELL_SIZE + 1) + 1,
|
||||
row * (CELL_SIZE + 1) + 1,
|
||||
CELL_SIZE,
|
||||
CELL_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (canvas == null || ctx == null) return;
|
||||
|
||||
const universe = universeRef.current;
|
||||
const width = universe.width();
|
||||
const height = universe.height();
|
||||
|
||||
canvas.height = (CELL_SIZE + 1) * height + 1;
|
||||
canvas.width = (CELL_SIZE + 1) * width + 1;
|
||||
|
||||
let animationId: number | null = null;
|
||||
|
||||
const renderLoop = () => {
|
||||
if (!isPaused) {
|
||||
universe.tick();
|
||||
}
|
||||
|
||||
drawGrid(ctx, width, height);
|
||||
drawCells(ctx, universe, width, height);
|
||||
|
||||
animationId = requestAnimationFrame(renderLoop);
|
||||
};
|
||||
|
||||
renderLoop();
|
||||
|
||||
return () => {
|
||||
if (animationId != null) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
};
|
||||
}, [isPaused]);
|
||||
|
||||
const onClickCell = (x: number, y: number) => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (canvas == null || ctx == null) return;
|
||||
|
||||
const universe = universeRef.current;
|
||||
const width = universe.width();
|
||||
const height = universe.height();
|
||||
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
|
||||
const scaleX = canvas.width / boundingRect.width;
|
||||
const scaleY = canvas.height / boundingRect.height;
|
||||
|
||||
const canvasLeft = (x - boundingRect.left) * scaleX;
|
||||
const canvasTop = (y - boundingRect.top) * scaleY;
|
||||
|
||||
const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
|
||||
const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);
|
||||
|
||||
universe.toggle_cell(row, col);
|
||||
|
||||
drawGrid(ctx, width, height);
|
||||
drawCells(ctx, universe, width, height);
|
||||
};
|
||||
|
||||
return (
|
||||
<div tw="leading-none">
|
||||
<div tw="mb-4">
|
||||
<div tw="space-y-4 mb-4">
|
||||
<h1 tw="font-bold text-4xl">Game of Life</h1>
|
||||
<p>
|
||||
Implemented in Rust based off the{" "}
|
||||
<Link
|
||||
href="https://rustwasm.github.io/book/game-of-life/introduction.html"
|
||||
tw="text-pink-600 underline hover:text-pink-500"
|
||||
>
|
||||
Wasm Game of Life tutorial
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div tw="flex space-x-4 mt-4">
|
||||
<button
|
||||
onClick={() => setIsPaused(!isPaused)}
|
||||
css={[tw`focus:outline-none focus:ring ring-pink-500 rounded-sm`]}
|
||||
>
|
||||
{isPaused ? <Play /> : <Pause />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
universeRef?.current.restart();
|
||||
}}
|
||||
css={[tw`focus:outline-none focus:ring ring-pink-500 rounded-sm`]}
|
||||
>
|
||||
<RefreshCw />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onClick={e => onClickCell(e.clientX, e.clientY)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Life;
|
||||
38
examples/rust-wasm/packages/www/src/components/Link.tsx
Normal file
38
examples/rust-wasm/packages/www/src/components/Link.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import NLink from "next/link";
|
||||
import React, { useMemo } from "react";
|
||||
import "twin.macro";
|
||||
|
||||
export interface Props {
|
||||
href: string;
|
||||
external?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const isExternalLink = (href: string) =>
|
||||
href == null || href.startsWith("http://") || href.startsWith("https://");
|
||||
|
||||
const useIsExternalLink = (href: string) =>
|
||||
useMemo(() => isExternalLink(href), [href]);
|
||||
|
||||
export const Link: React.FC<Props> = ({
|
||||
href,
|
||||
external,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const isExternal = (useIsExternalLink(href) || external) ?? false;
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NLink href={href} passHref>
|
||||
<a {...props}>{children}</a>
|
||||
</NLink>
|
||||
);
|
||||
};
|
||||
54
examples/rust-wasm/packages/www/src/components/SEO.tsx
Normal file
54
examples/rust-wasm/packages/www/src/components/SEO.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from "react";
|
||||
import { DefaultSeo, NextSeo, NextSeoProps } from "next-seo";
|
||||
import Head from "next/head";
|
||||
import { DefaultSeoProps } from "next-seo";
|
||||
|
||||
export interface Props extends NextSeoProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const title = "WASM Starter";
|
||||
export const url = "";
|
||||
const description = "Game of Life implemented in Rust";
|
||||
const image = "";
|
||||
|
||||
const config: DefaultSeoProps = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
url,
|
||||
site_name: title,
|
||||
images: [{ url: image }],
|
||||
},
|
||||
};
|
||||
|
||||
export const SEO: React.FC<Props> = ({ image, ...props }) => {
|
||||
const title = props.title ?? config.title;
|
||||
const description = props.description || config.description;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DefaultSeo {...config} />
|
||||
|
||||
<NextSeo
|
||||
{...props}
|
||||
{...(image == null
|
||||
? {}
|
||||
: {
|
||||
openGraph: {
|
||||
images: [{ url: image }],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
|
||||
<meta name="description" content={description} />
|
||||
</Head>
|
||||
</>
|
||||
);
|
||||
};
|
||||
26
examples/rust-wasm/packages/www/src/components/Text.tsx
Normal file
26
examples/rust-wasm/packages/www/src/components/Text.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Link } from "./Link";
|
||||
import tw from "twin.macro";
|
||||
|
||||
export const Header = tw.h1`
|
||||
text-4xl font-extrabold mb-8
|
||||
`;
|
||||
|
||||
export const SubHeader = tw.h2`
|
||||
text-2xl font-bold mb-4 mt-8
|
||||
`;
|
||||
|
||||
export const Paragraph = tw.p`
|
||||
mb-12 max-w-sm text-article
|
||||
`;
|
||||
|
||||
export const PillLink = tw(Link)`
|
||||
block text-fg rounded-sm px-1 bg-accentDim hover:bg-accent hover:text-bg dark:hover:text-fg
|
||||
`;
|
||||
|
||||
export const HighlightLink = tw(Link)`
|
||||
font-bold hover:text-accent
|
||||
`;
|
||||
|
||||
export const UnderlineLink = tw(HighlightLink)`
|
||||
underline
|
||||
`;
|
||||
21
examples/rust-wasm/packages/www/src/layouts/Page.tsx
Normal file
21
examples/rust-wasm/packages/www/src/layouts/Page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { Props as SEOProps, SEO } from "../components/SEO";
|
||||
import "twin.macro";
|
||||
|
||||
export interface Props {
|
||||
seo?: SEOProps;
|
||||
}
|
||||
|
||||
export const Page: React.FC<Props> = props => {
|
||||
return (
|
||||
<>
|
||||
<SEO {...props.seo} />
|
||||
|
||||
<div tw="flex flex-col w-full px-6 mx-auto">
|
||||
<main tw="flex-grow py-20 md:py-20 w-full mx-auto">
|
||||
{props.children}
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
16
examples/rust-wasm/packages/www/src/pages/_app.tsx
Normal file
16
examples/rust-wasm/packages/www/src/pages/_app.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AppProps } from "next/app";
|
||||
import React from "react";
|
||||
import { GlobalStyles } from "../styles/GlobalStyles";
|
||||
import { TwinGlobalStyles } from "../styles/TwinGlobalStyles";
|
||||
|
||||
const MyApp = ({ Component, pageProps }: AppProps) => {
|
||||
return (
|
||||
<>
|
||||
<TwinGlobalStyles />
|
||||
<GlobalStyles />
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyApp;
|
||||
28
examples/rust-wasm/packages/www/src/pages/_document.tsx
Normal file
28
examples/rust-wasm/packages/www/src/pages/_document.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import Document from "next/document";
|
||||
import { ServerStyleSheet } from "styled-components";
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
static async getInitialProps(ctx) {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
try {
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
|
||||
});
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
{sheet.getStyleElement()}
|
||||
</>
|
||||
),
|
||||
};
|
||||
} finally {
|
||||
sheet.seal();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
examples/rust-wasm/packages/www/src/pages/index.tsx
Normal file
17
examples/rust-wasm/packages/www/src/pages/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextPage } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import React from "react";
|
||||
import "twin.macro";
|
||||
import { Page } from "../layouts/Page";
|
||||
|
||||
const DynamicLife = dynamic(() => import("../components/Life"));
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<Page>
|
||||
<DynamicLife />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
10
examples/rust-wasm/packages/www/src/styles/GlobalStyles.ts
Normal file
10
examples/rust-wasm/packages/www/src/styles/GlobalStyles.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import tw from "twin.macro";
|
||||
|
||||
export const GlobalStyles = createGlobalStyle`
|
||||
body {
|
||||
${tw`antialiased`}
|
||||
${tw`text-black bg-gray-50 leading-relaxed`}
|
||||
${tw`font-sans`}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import { GlobalStyles } from "twin.macro";
|
||||
|
||||
export const TwinGlobalStyles: React.FC = () => {
|
||||
return <GlobalStyles />;
|
||||
};
|
||||
15
examples/rust-wasm/packages/www/tailwind.config.js
Normal file
15
examples/rust-wasm/packages/www/tailwind.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const colors = require("tailwindcss/colors");
|
||||
|
||||
module.exports = {
|
||||
purge: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||
darkMode: "class", // 'media' or 'class'
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`,
|
||||
},
|
||||
colors: colors,
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
};
|
||||
21
examples/rust-wasm/packages/www/tsconfig.json
Normal file
21
examples/rust-wasm/packages/www/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"downlevelIteration": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
38
examples/rust-wasm/packages/www/typings/twin.d.ts
vendored
Normal file
38
examples/rust-wasm/packages/www/typings/twin.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
// twin.d.ts
|
||||
import "twin.macro";
|
||||
import styledImport, { CSSProp, css as cssImport } from "styled-components";
|
||||
import {} from "styled-components/cssprop";
|
||||
|
||||
declare module "twin.macro" {
|
||||
// The styled and css imports
|
||||
const styled: typeof styledImport;
|
||||
const css: typeof cssImport;
|
||||
}
|
||||
|
||||
declare module "react" {
|
||||
// The css prop
|
||||
interface HTMLAttributes<T> extends DOMAttributes<T> {
|
||||
css?: CSSProp;
|
||||
tw?: string;
|
||||
}
|
||||
|
||||
// The inline svg css prop
|
||||
interface SVGProps<T> extends SVGProps<SVGSVGElement> {
|
||||
css?: CSSProp;
|
||||
}
|
||||
|
||||
// <style jsx> and <style jsx global> support for styled-jsx
|
||||
interface StyleHTMLAttributes<T> extends HTMLAttributes<T> {
|
||||
jsx?: boolean;
|
||||
global?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// The 'as' prop on styled components
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicAttributes<T> extends DOMAttributes<T> {
|
||||
as?: string | React.ComponentType;
|
||||
}
|
||||
}
|
||||
}
|
||||
2523
examples/rust-wasm/packages/www/yarn.lock
Normal file
2523
examples/rust-wasm/packages/www/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
3109
examples/rust-wasm/yarn.lock
Normal file
3109
examples/rust-wasm/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user