NetSuite上にReactアプリをデプロイしてみた

## はじめに

NetSuiteではSuiteScriptを使ってカスタマイズが可能ですが、外部ライブラリを使用した開発もやりようによっては可能です。
今回はVite+Reactで作ったアプリをNetSuiteで動作させてみたので、その手順をご紹介します。

## 手順概要と結論

1. Reactアプリを作る
2. webpackで1ファイルにまとめる
3. [2]を読み込むSuiteletを作る
4. [2]と[3]をNetSuiteにデプロイする
5. Suiteletにアクセスする
6. Reactアプリが動作する！

ちなみにですが、Next.jsのようなフレームワークを使用した開発は多分無理です。よりモダンな開発を行いたい場合は、Next.js等のフレームワークで作成したアプリは外部にデプロイし、NetSuiteのAPIを使用してデータをフェッチするのがいいと思います。

## 実装手順

### 0. 前提／環境
1. 開発環境
    * VS Code／Cursor
    * SuiteCloud Extension for Visual Studio Code
    * Node.js 18.20.4
    * npm 10.7.0
2. NetSuite環境と機能の有効化
    * 2024.2
    * クライアントSuiteScript
    * サーバーSuiteScript
    * SuiteCloud開発フレームワーク

### 1. SDFプロジェクトの作成

まずはSDFプロジェクトを作成してください。もしも既存のプロジェクトがある場合は、そのプロジェクトを使用しても大丈夫です。
また、SDFプロジェクト内のディレクトリ構造は任意で定義していただいて大丈夫ですが、この記事では以下のようなディレクトリ構造を想定しています。雰囲気で感じ取ってください。

```bash
.
└── New SuiteScript Project/
    └── src/
        └── FileCabinet/
            └── SuiteScripts/
                ├── 0001/
                │   ├── react_app_sl.js
                │   └── netsuite_react_app/
                │       ├── src/
                │       │   ├── App.jsx
                │       │   ├── App.css
                │       │   ├── main.jsx
                │       │   ├── index.css
                │       │   └── ...
                │       ├── ...
                │       ├── webpack.config.js
                │       └── dist/
                │           └── bundle.js
                ├── 0002/
                │   └── ...
                └── ...
```

ポイントは、プロジェクト内のSuiteScriptsディレクトリに、各カスタマイゼーション用のディレクトリを配置し、その中に今回のReactアプリを配置することです。

### 2. Reactプロジェクトの作成

まずは新規Reactプロジェクトを作成します。今回はviteを使います。

```bash
npm create vite@latest
# プロジェクト名を入力: netsuite-react-app
# フレームワークとしてReactを選択
# バリアントとしてJavaScriptを選択

cd netsuite-react-app
npm install
```
また、svgファイル等は不要なので、`public`ディレクトリと`App.jsx`内のsvgファイルのインポートは削除しておきます（というか、svgファイルをNetSuite上で正常に参照できませんでした）。

```javascript:SuiteScripts\0001\netsuite-react-app\src\App.jsx
import { useState } from 'react'
import './App.css'

function App() {
  const [count, setCount] = useState(0)
  return (
    <>
      <h1>Vite + React on NetSuite</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
      </div>
    </>
  )
}
export default App
```

一旦ここでnpm run devを実行して、Reactアプリが動作することを確認いただいても大丈夫です。

### 3. 必要な依存関係のインストール

webpackでバンドルするために必要なパッケージをインストールします：

```bash
npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-env @babel/preset-react style-loader css-loader
```

### 4. webpack設定ファイルの作成

プロジェクトのルートディレクトリ（今回の場合は`SuiteScripts\0001\netsuite-react-app\`）に`webpack.config.js`を作成します：

```javascript:SuiteScripts\0001\netsuite-react-app\webpack.config.js
import path from 'path';
import webpack from 'webpack';
import { fileURLToPath } from 'url';

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

export default {
    entry: './src/main.jsx',
    plugins: [
        new webpack.ProvidePlugin({
            React: 'react',
            ReactDOM: 'react-dom',
        }),
    ],
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist'),
        libraryTarget: 'var',
        library: 'MyApp',
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react'],
                    },
                },
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.(png|svg|jpg|jpeg|gif)$/i,
                type: 'asset/resource',
            }
        ],
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    },
    externals: {
        'react': 'React',
        'react-dom': 'ReactDOM'
    },
    mode: 'production'
};
```

### 5. package.jsonの更新

package.jsonのビルドスクリプトを編集します：

```json:SuiteScripts\0001\netsuite-react-app\package.json
{
    "scripts": {
        "build": "webpack --config webpack.config.js"
    }
}
```

### 5. Suiteletスクリプトの作成

NetSuiteでReactアプリを表示するためのSuiteletスクリプトを作成します：

```javascript:SuiteScripts\0001\react_app_sl.js
/**
    * @NApiVersion 2.1
    * @NScriptType Suitelet
    * @NModuleScope SameAccount
    *
    */
define(["N/ui/serverWidget", 'N/file', 'N/url'],
    /**
     * 
     * @param {serverWidget} serverWidget 
     * @param {file} file 
     * @param {url} url 
     */
    (serverWidget,file,url) => {
        const onRequest = async ({ request, response }) => {
        const action = request.parameters.action;
        switch (request.method) {
            case "GET":
                switch (action) {
                    default:
                    const suiteletUI = await createUI(request, response);
                    response.writePage(suiteletUI);
                }
                break;
            case "POST":
                break;
            default:
                throw {
                    name: "INVALID_REQUEST_METHOD",
                    message: 'Request method request.method is invalid.',
                };
        }
        };
    
        const createUI = async ({ parameters }) => {
        const form = serverWidget.createForm({ title: " " });
    
        const field = form.addField({
            id: "custpage_hidden",
            label: "not shown - hidden",
            type: "INLINEHTML",
        });
        // Load the bundle.js file from the File Cabinet
        const bundleFile = file.load({
            id: 'SuiteScripts/0001/netsuite-react-app/dist/bundle.js'  
        });

        
        // Get the URL for the bundle.js file
        const bundleUrl = bundleFile.url
        
    
        field.defaultValue = `
            <!DOCTYPE html>
            <html lang="en">
            <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
            <title>React Widget Example</title>
            </head>
            <body>
            
            <div id="root"></div>
    
            <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
            <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
            <script defer src="${bundleUrl}"></script>
            <script>
              // フォームのデフォルトの送信を防ぐ
              document.addEventListener('DOMContentLoaded', function() {
                const forms = document.getElementsByTagName('form');
                for (let form of forms) {
                  form.addEventListener('submit', function(e) {
                    e.preventDefault();
                    return false;
                  });
                  form.onsubmit = function() { return false; };
                }
              });
            </script>
            </body>
            </html>`;
    
        return form;
        };
    
        return { onRequest };
    });
```

### 6. デプロイ手順

1. Reactアプリをビルド：
    ```bash
    npm run build
    ```

2. 生成された`dist/bundle.js`をNetSuiteのファイルキャビネットにアップロード
    * VS Code上でアップロードしたいファイルを選択しつつ、コマンドパレットを開き（ctrl+shift+p）、`SuiteCloud: Upload File` を行うことで、SuiteScripts配下のディレクトリ構造のままファイルがアップロードされます
3. Suiteletスクリプトをアップロードし、スクリプトレコードとデプロイメントレコードを作成

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289720/70c97457-2d09-19d8-9cd8-7287522d510e.png)

4. デプロイメントのURLにアクセスしてReactアプリの動作を確認

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289720/f7476924-e509-05d4-b779-bfead44a1fc2.png)


### 7. できた

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289720/310bed36-ef56-05d1-1097-b948c315220c.png)

無事にReactアプリをNetSuite上にデプロイすることができました。
このままだと`index.css`のスタイルが悪さして気持ち悪いので、`index.css`内の`body`スタイルを以下のようにコメントアウトか削除しちゃいましょう。
```css:SuiteScripts\0001\netsuite-react-app\src\index.css
body {
  margin: 0;
  /* display: flex;
  place-items: center; */
  min-width: 320px;
  min-height: 100vh;
}
```
編集した後は、再度`npm run build`を行い、NetSuiteに`bundle.js`を再アップロードすることをお忘れなく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289720/04befba8-631f-619c-844a-37a4af6e17bc.png)

## こんなことができる！

このアプローチを使うことで、以下のようなことが実現可能です：

1. モダンなUI/UX
   * Reactの豊富なエコシステムを活用
   * インタラクティブなユーザーインターフェース
   * 再利用可能なコンポーネント
2. NetSuiteとの連携
   * Suiteletからのデータフェッチ
   * NetSuiteのレコード操作
   * 既存機能との統合

これらの要素は、以下のような業務アプリケーションの開発で活用できます：

* データ可視化ダッシュボード
* カスタムトランザクション入力フォーム
* インタラクティブなレポーティングツール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289720/39c16125-9add-cf41-5156-4fad2f0c18ac.png)


## 備考

Next.jsのような、フレームワークを使用したフルスタックアプリの開発は多分できないです。データフェッチの部分はSuiteletやRestletから渡してあげる必要があります。
Tailwind CSS等のCSSフレームワークは使用できます。つまるところ、一つのファイルにバンドルでき、データフェッチの部分をNetSuiteに頼れば、あとは何でもござれといった感じです。
ただ、WebpackでバンドルしてNetSuiteにアップロードして初めてデータ連携の部分を確認できるので、ローカル環境で開発中はデータ連携の部分はテストしづらいです。スタブデータを作成する必要があります。

## まとめ

今回はReactをNetSuiteで活用する方法をご紹介しました。
このアプローチを使うことで、従来のERPシステムの概念を超えた、現代的なWebアプリケーションの開発が可能になります。
特に、複雑なUIやインタラクティブな要素が必要なカスタマイズで真価を発揮するかもです。