├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitpod.yml ├── LICENSE ├── README.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── components │ └── WASMExample.tsx ├── context │ └── WASM.tsx ├── hooks │ └── useMountEffectOnce.ts ├── pages │ ├── _app.tsx │ └── index.tsx └── styles │ ├── Home.module.css │ └── globals.css ├── tsconfig.json └── wasm ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── rs-test.sh └── src └── lib.rs /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/javascript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 4 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-16-bullseye 5 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/javascript-node 3 | { 4 | "name": "nextjs-wasm", 5 | "build": { 6 | "dockerfile": "Dockerfile" 7 | }, 8 | 9 | // Add the IDs of extensions you want installed when the container is created. 10 | "extensions": [ 11 | "rust-lang.rust-analyzer", 12 | "bungcip.better-toml", 13 | "serayuzgur.crates", 14 | "EditorConfig.EditorConfig" 15 | ], 16 | 17 | // Use 'postCreateCommand' to run commands after the container is created. 18 | "postCreateCommand": "cargo install wasm-pack", 19 | 20 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 21 | "remoteUser": "node", 22 | "features": { 23 | "rust": "latest" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.{js,jsx,ts,tsx,json}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | version: 2 6 | updates: 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | 12 | - package-ecosystem: "cargo" 13 | directory: "/wasm/" 14 | schedule: 15 | interval: "monthly" 16 | 17 | - package-ecosystem: "github-actions" 18 | directory: "/" # default location of `.github/workflows` 19 | schedule: 20 | interval: "monthly" 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | webassembly: 11 | name: WebAssembly 12 | runs-on: ubuntu-latest 13 | env: 14 | CARGO_TERM_COLOR: always 15 | RUST_TEST_THREADS: 32 16 | defaults: 17 | run: 18 | working-directory: wasm 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/cache@v3 22 | with: 23 | path: | 24 | ~/.cargo/bin/ 25 | ~/.cargo/registry/index/ 26 | ~/.cargo/registry/cache/ 27 | ~/.cargo/git/db/ 28 | target/ 29 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 30 | - name: Tests 31 | run: ./rs-test.sh 32 | 33 | nextjs: 34 | name: NextJS 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | - uses: actions/cache@v3 39 | with: 40 | path: | 41 | ~/.cargo/bin/ 42 | ~/.cargo/registry/index/ 43 | ~/.cargo/registry/cache/ 44 | ~/.cargo/git/db/ 45 | ./wasm/target/ 46 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 47 | - uses: actions/setup-node@v3 48 | with: 49 | node-version: 16.x 50 | cache: 'npm' 51 | cache-dependency-path: ./package-lock.json 52 | - name: Install dependencies 53 | run: npm ci 54 | - name: Build WebAssembly 55 | run: npm run build:wasm 56 | - name: Build NextJS application 57 | run: npm run build 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: gitpod/workspace-full 2 | 3 | tasks: 4 | - init: | 5 | cargo install wasm-pack 6 | npm run build:wasm 7 | npm install 8 | npm run dev 9 | vscode: 10 | extensions: 11 | - matklad.rust-analyzer 12 | - bungcip.better-toml 13 | - serayuzgur.crates 14 | - EditorConfig.EditorConfig 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 George Satellite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js with WebAssembly 2 | 3 | [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 4 | 5 | [Next.js](https://nextjs.org/)-based web application template with [WebAssembly](https://webassembly.org/) module written in [Rust](https://www.rust-lang.org/) programming language. 6 | 7 | ## Try in [Gitpod](https://www.gitpod.io/) 8 | 9 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/satelllte/nextjs-wasm) 10 | 11 | ## Try in [GitHub Codespaces](https://github.com/features/codespaces) 12 | 13 | [![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/satelllte/nextjs-wasm) 14 | 15 | ## Prerequisites 16 | 17 | - [NodeJS](https://nodejs.org/) | recommended version: `>= 16` 18 | - [Rust & Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) | recommended version: `>= 1.61` 19 | 20 | ## Getting Started 21 | 22 | Install dependencies: 23 | 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | Compile WebAssembly: 29 | 30 | ```bash 31 | npm run build:wasm 32 | ``` 33 | 34 | Run the development server: 35 | 36 | ```bash 37 | npm run dev 38 | ``` 39 | 40 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 41 | 42 | ## Build for Production 43 | 44 | ```bash 45 | npm run build:wasm # if wasn't built yet 46 | npm run build 47 | ``` 48 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-wasm", 3 | "version": "1.4.0", 4 | "scripts": { 5 | "dev": "next dev", 6 | "build:wasm": "wasm-pack build wasm --release --target web", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "13.0.5", 13 | "react": "18.2.0", 14 | "react-dom": "18.2.0", 15 | "wasm": "file:wasm/pkg" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "18.11.9", 19 | "@types/react": "18.0.25", 20 | "@types/react-dom": "18.0.9", 21 | "eslint": "8.28.0", 22 | "eslint-config-next": "13.0.5", 23 | "typescript": "4.9.3", 24 | "wasm-pack": "0.10.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satelllte/nextjs-wasm/2a62968bb10a7299f59d35499e5b61048bc00548/public/favicon.ico -------------------------------------------------------------------------------- /src/components/WASMExample.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { WASMContext } from "../context/WASM" 3 | 4 | export const WASMExample = () => { 5 | const ctx = useContext(WASMContext) 6 | 7 | if (!ctx.wasm) { 8 | return <>... 9 | } 10 | 11 | return <>Computed from WASM: 4+3={ctx.wasm.add(4,3)} 12 | } 13 | -------------------------------------------------------------------------------- /src/context/WASM.tsx: -------------------------------------------------------------------------------- 1 | import { useState, createContext } from 'react' 2 | import type { ReactNode } from 'react' 3 | import { useMountEffectOnce } from '../hooks/useMountEffectOnce' 4 | 5 | const initial: IWASMContext = {} 6 | 7 | export const WASMContext = createContext(initial) 8 | 9 | export const WASMContextProvider: React.FC = ({ 10 | children 11 | }) => { 12 | const [state, setState] = useState(initial) 13 | 14 | // This has to run only once: https://github.com/rustwasm/wasm-bindgen/issues/3153 15 | // Though, in development React renders twice when Strict Mode is enabled: https://reactjs.org/docs/strict-mode.html 16 | // That's why it must be limited to a single mount run 17 | useMountEffectOnce(() => { 18 | (async() => { 19 | const wasm = await import("wasm"); 20 | await wasm.default(); 21 | setState({ wasm }); 22 | })() 23 | }) 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | ) 30 | } 31 | 32 | interface IWASMContext { 33 | wasm?: typeof import('wasm') 34 | } 35 | 36 | interface WASMContextProviderProps { 37 | children: ReactNode 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/useMountEffectOnce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | export const useMountEffectOnce = (fn: () => void) => { 4 | const wasExecutedRef = useRef(false) 5 | useEffect(() => { 6 | if (!wasExecutedRef.current) { 7 | fn() 8 | } 9 | wasExecutedRef.current = true 10 | }, [fn]) 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { WASMContextProvider } from '../context/WASM' 4 | 5 | const App = ({ Component, pageProps }: AppProps) => { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default App 14 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import styles from '../styles/Home.module.css' 4 | import { WASMExample } from '../components/WASMExample' 5 | 6 | const Home: NextPage = () => { 7 | return ( 8 |
9 | 10 | Next.JS with WebAssembly 11 | 12 | 13 | 14 | 15 |
16 |

17 | Welcome to Next.js with WebAssembly! 18 |

19 | 20 |
21 | 22 |
23 |
24 |
25 | ) 26 | } 27 | 28 | export default Home 29 | -------------------------------------------------------------------------------- /src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .title a { 16 | color: #0070f3; 17 | text-decoration: none; 18 | } 19 | 20 | .title a:hover, 21 | .title a:focus, 22 | .title a:active { 23 | text-decoration: underline; 24 | } 25 | 26 | .title { 27 | margin: 0; 28 | line-height: 1.15; 29 | font-size: 4rem; 30 | text-align: center; 31 | } 32 | 33 | .wasm { 34 | margin: 4rem 0; 35 | line-height: 1.5; 36 | font-size: 1.5rem; 37 | text-align: center; 38 | } 39 | 40 | @media (max-width: 600px) { 41 | .title { 42 | font-size: 3rem; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /wasm/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /pkg -------------------------------------------------------------------------------- /wasm/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bumpalo" 7 | version = "3.9.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" 10 | 11 | [[package]] 12 | name = "cfg-if" 13 | version = "1.0.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 16 | 17 | [[package]] 18 | name = "log" 19 | version = "0.4.17" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 22 | dependencies = [ 23 | "cfg-if", 24 | ] 25 | 26 | [[package]] 27 | name = "once_cell" 28 | version = "1.13.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 31 | 32 | [[package]] 33 | name = "proc-macro2" 34 | version = "1.0.39" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" 37 | dependencies = [ 38 | "unicode-ident", 39 | ] 40 | 41 | [[package]] 42 | name = "quote" 43 | version = "1.0.18" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 46 | dependencies = [ 47 | "proc-macro2", 48 | ] 49 | 50 | [[package]] 51 | name = "syn" 52 | version = "1.0.95" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" 55 | dependencies = [ 56 | "proc-macro2", 57 | "quote", 58 | "unicode-ident", 59 | ] 60 | 61 | [[package]] 62 | name = "unicode-ident" 63 | version = "1.0.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" 66 | 67 | [[package]] 68 | name = "wasm" 69 | version = "1.4.0" 70 | dependencies = [ 71 | "wasm-bindgen", 72 | ] 73 | 74 | [[package]] 75 | name = "wasm-bindgen" 76 | version = "0.2.83" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 79 | dependencies = [ 80 | "cfg-if", 81 | "wasm-bindgen-macro", 82 | ] 83 | 84 | [[package]] 85 | name = "wasm-bindgen-backend" 86 | version = "0.2.83" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 89 | dependencies = [ 90 | "bumpalo", 91 | "log", 92 | "once_cell", 93 | "proc-macro2", 94 | "quote", 95 | "syn", 96 | "wasm-bindgen-shared", 97 | ] 98 | 99 | [[package]] 100 | name = "wasm-bindgen-macro" 101 | version = "0.2.83" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 104 | dependencies = [ 105 | "quote", 106 | "wasm-bindgen-macro-support", 107 | ] 108 | 109 | [[package]] 110 | name = "wasm-bindgen-macro-support" 111 | version = "0.2.83" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 114 | dependencies = [ 115 | "proc-macro2", 116 | "quote", 117 | "syn", 118 | "wasm-bindgen-backend", 119 | "wasm-bindgen-shared", 120 | ] 121 | 122 | [[package]] 123 | name = "wasm-bindgen-shared" 124 | version = "0.2.83" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 127 | -------------------------------------------------------------------------------- /wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm" 3 | version = "1.4.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | "wasm-bindgen" = "0.2.83" 13 | -------------------------------------------------------------------------------- /wasm/rs-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cargo test -------------------------------------------------------------------------------- /wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | #[wasm_bindgen] 4 | pub fn add(x: i32, y: i32) -> i32 { 5 | x + y 6 | } 7 | 8 | #[cfg(test)] 9 | mod tests { 10 | use super::add; 11 | 12 | #[test] 13 | fn _add() { 14 | assert_eq!(add(2, 2), 4); 15 | } 16 | } 17 | --------------------------------------------------------------------------------