Open14

Next.js+Rust+WebAssembly+Marpでスライドを自動生成するwebアプリを作る

so-heyso-hey

まずは

npx create-next-app@latest project-name --use-npm

を実行する.

PS C:\Users\my-name\Desktop> npx create-next-app@latest project-name --use-npm
√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like to use `src/` directory? ... Yes
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to customize the default import alias (@/*)? ... No
Creating a new Next.js app in C:\Users\my-name\Desktop\project-name.
so-heyso-hey

project-nameに移動して,早速

npm run build

でビルドして,

npm run start

でアプリを起動.-> 問題なし

so-heyso-hey

ここで,Rust+WebAssemblyを導入.

cargo new directory-name --lib
cd directory-name
wasm-pack build --target web

を実行.(ここですでにwasm-packはインストールしているので,それ関連のエラーはなし)
しかし,

Error: crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your Cargo.toml file:

[lib]
crate-type = ["cdylib", "rlib"]
Caused by: crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your Cargo.toml file:

[lib]
crate-type = ["cdylib", "rlib"]

というエラーが出た.

so-heyso-hey

これはCargo.toml

[lib]
crate-type = ["cdylib", "rlib"]

を書けば解決.再びwasm-pack build --target webを実行すると

[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
   Compiling backend v0.1.0 (C:\Users\my-name\Desktop\project-name\directory-name)
    Finished release [optimized] target(s) in 0.14s
Error: Ensure that you have "wasm-bindgen" as a dependency in your Cargo.toml file: 
[dependencies]
wasm-bindgen = "0.2"
Caused by: Ensure that you have "wasm-bindgen" as a dependency in your Cargo.toml file:
[dependencies]
wasm-bindgen = "0.2"

またエラー.

so-heyso-hey

これもCargo.toml

[dependencies]
wasm-bindgen = "0.2"

を追加するか,cargo add wasm-bindgenを実行することで解決.
またまたwasm-pack build --target webを実行.

so-heyso-hey

どこで何が起こるのかわからないので,こまめにnpm run buildしておく.
今の時点では問題なし.今後も頻繁に確認する.

so-heyso-hey

next.jsのプロジェクトにWebAssemblyモジュールを統合する.これにより,フロントエンドからRustの関数を呼び出すことができる.
まず,必要なパッケージをインストールする.

npm install @wasm-tool/wasm-pack-plugin webpack

これは問題なくいける.

so-heyso-hey

次に,webpackの設定を追加する.next.config.mjsを以下の内容に書き換える.

next.config.mjs
import path from "path";
import WasmPackPlugin from "@wasm-tool/wasm-pack-plugin";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const nextConfig = {
  webpack: (config, {}) => {
    config.plugins.push(
      new WasmPackPlugin({
        crateDirectory: path.resolve(__dirname, "./directory-name"),
        outDir: path.resolve(__dirname, "./public/pkg"),
        outName: "rust_wasm",
      })
    );
    config.experiments = {
      syncWebAssembly: true,
      layers: true,
    };
    return config;
  },
};

export default nextConfig;

注意してほしいのが,これは .mjs ファイルであることだ.(ここによると,v14.1あたりからデフォルトで.mjsになっているらしい.)

so-heyso-hey

この状態でnpm run buildを実行すると,project-name/public/pkgにWebAssmblyモジュールが出力されるので,最初にビルドして出力したproject-name/directory-name/pkgは消しちゃう.

so-heyso-hey

これでたぶん大体のセットアップは完了したはず.

so-heyso-hey

project-name/directory-name/src/lib.rsを以下の内容に書き換える.

lib.rs
use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(a: &str);
}

macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

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

#[wasm_bindgen]
pub fn add(a: isize, b: isize) -> isize {
    console_log!("called add function");
    a + b
}

さらにproject-name/src/app/page.tsxを以下の内容に書き換える.

page.tsx
"use client";
import { useEffect, useState } from "react";
import initSync, { greet, add } from "../../public/pkg";

export default function Home() {
  const [greeting, setGreeting] = useState("now loading...");
  const [num, setNum] = useState(0);

  const greetAndAdd = () => {
    const greeting = greet("World");
    setGreeting(greeting);
    setNum((num) => add(num, 1));
  };

  useEffect(() => {
    initSync();
  }, []);

  return (
    <center>
      <h1>{greeting}</h1>
      <h1>{num}</h1>
      <button onClick={greetAndAdd}>greet and add button</button>
    </center>
  );
}
so-heyso-hey

これでnpm run buildからnpm run startを実行すると,画面の左上に

Hello, World!
0
greet and add button

となっていて,ボタンを押すと数字が増える.

so-heyso-hey

ついでに,project-name/package.json

package.json
{
    ...
    "dependencies": {
        ...
        "file-name": "file:public/pkg"
    }
    ...
}

と追加する.その後npm install file-nameを実行すると,"file-name"をモジュール名として使用することができる.
npm install file-name

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

のエラーが出たら,素直にnpm audit fixして対処する.