コードベースのデザインツール「UXPin Merge」をNext.jsで試してみた

2022.02.25

こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。

最近コードベースのデザインツール「UXPin Merge」を触る機会をいただいたので、実際に試したことをまとめたいと思います。

UXPin Mergeとは?

UXPin Mergeはコードベースのデザインツールです。

一般的なデザインツールでは描画したものはベクター、ラスターのイメージデータとして扱われますが、UXPinで描画したものはコード化が行われ実際に動作するコンポーネントとしてレンダリングされます。

また、UXPin Mergeでは作成したReactコンポーネントをインポートしてデザインに利用することができます。

今回試してみたこと

UXPin Mergeでは「React」に対応していますが、今回は「Next.js」のプロジェクトでコンポーネントを作成し、作成したものをUXPinに取り込んで利用できるか試してみます。

コンポーネントとしては「簡単なボタンのコンポーネント」を作成して、試してみたいと思います。

ドキュメントは以下のドキュメントを参考にしてすすめてみます。

前提

今回実施する環境は、NodeとYarnが設定済みの環境となっています。

% node -v
v16.13.2

% yarn -v
1.22.17

プロジェクトのセットアップ

まずはNext.jsのプロジェクトを作成します。今回はhello-uxpin-merge-sampleという名前にしました。

$ yarn create v1.22.17
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Installed "create-next-app@12.1.0" with binaries:
      - create-next-app
? What is your project named? › hello-uxpin-merge-sample
(...snip...)

プロジェクトを作成したら、UXPin MergeのCLIをインストールします。

$ cd hello-uxpin-merge-sample
$ yarn add -D @uxpin/merge-cli

インストール後に以下のコマンドを実行してUXPin Merge関連の初期化をしておきます。

$ npx uxpin-merge init

You are using @uxpin/merge-cli version: 2.7.10

✅ Successfully created /misc/hello-uxpin-merge-sample/uxpin.config.js
✅ Successfully created /misc/hello-uxpin-merge-sample/uxpin.webpack.config.js
✅ Successfully created /misc/hello-uxpin-merge-sample/src/components/UXPinWrapper/UXPinWrapper.js
✅ Successfully created example component in /misc/hello-uxpin-merge-sample/src/components/Button

初期化をすることで、サンプルのButtonコンポーネントButton.jsと設定ファイルuxpin.config.jsuxpin-webpack.config.jsなどが自動生成されます。

なお、今回はこのサンプルのButton.jsコンポーネントは利用しません。

TypeScript関連のセットアップ

今回は、TypeScriptのNext.jsプロジェクトなのでTypeScript関連のセットアップを行います。

UXPin MergeではWebpackを利用しているので、Loaderの追加インストールや設定ファイルの変更などを行っていきます。

ts-loader

ts-loaderをインストールします。注意点として、merge-cliでは「Webpack 4」を利用しているようなので、バージョンの指定でWebpack 4をサポートしている~8.2.0を指定するようにします。

yarn add -D ts-loader@~8.2.0

なお、バージョン指定をせずに最新バージョンを利用すると以下のようなエラーが発生します。

loaderContext.getOptions is not a function

uxpin.webpack.config.js

Webpackの設定ファイルuxpin.webpack.config.jsは初期状態ではTypeScript向けにはなっていないので、いくつか設定していきます。

uxpin.webpack.config.js

const path = require("path");
const webpack = require("webpack");

module.exports = {
  output: {
    path: path.resolve(__dirname, "build"),
    filename: "bundle.js",
    publicPath: "/"
  },
  resolve: {
    modules: [__dirname, "node_modules"],
    extensions: ["*", ".js", ".jsx", ".ts", ".tsx"],
  },
  devtool: "source-map",
  module: {
    rules: [
      {
        test: /\.ts$|\.tsx$/,
        use: [
          {
            loader: require.resolve("babel-loader", {
              paths: ["./node_modules/@uxpin/merge-cli"],
            }),
            options: {
              presets: [
                require.resolve("@babel/preset-react", {
                  paths: ["./node_modules/@uxpin/merge-cli"],
                }),
              ],
            },
          },
          {
            loader: "ts-loader",
          },
        ],
        exclude: /node_modules/,
      },
      {
        test: /\.(s*)css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2
            }
          },
        ]
      },
      {
        loader: require.resolve('babel-loader', { paths: ['./node_modules/@uxpin/merge-cli'] }),
        test: /\.js?$/,
        exclude: /node_modules/,
        options: {
          presets: [
            require.resolve('@babel/preset-env', { paths: ['./node_modules/@uxpin/merge-cli'] }),
            require.resolve('@babel/preset-react', { paths: ['./node_modules/@uxpin/merge-cli'] })
          ],
        }
      },
    ]
  }
}

resolveの拡張子に.ts.tsxを加えているのと、module.rulesに各loaderの設定を加えています。

ts-loaderは先程追加インストールしたものを利用しており、それ以外はmerge-cliに含まれているので、そちらを利用しています。注意点としてloaderは下から順番に評価されるのでts-loaderの設定を下にしています。

Loaders can be chained. Each loader in the chain applies transformations to the processed resource. A chain is executed in reverse order.

tsconfig.json

Next.jsのデフォルトではnoEmittrueとなっているのですが、こちらはUXPin Merge用にfalseに設定します。

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": false,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

起動用の設定

あとは package.json を修正して、uxpin関連のコマンドが実行できるようにしておきます。

package.json

{
  "name": "hello-uxpin-merge-sample",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "uxpin": "uxpin-merge --disable-tunneling",
    "push": "uxpin-merge push --token \"SampleToken12345678901234567890123456789\" --branch main"
  },
  "dependencies": {
    "next": "12.1.0",
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  "devDependencies": {
    "@types/node": "17.0.21",
    "@types/react": "17.0.39",
    "@uxpin/merge-cli": "^2.7.10",
    "eslint": "8.9.0",
    "eslint-config-next": "12.1.0",
    "ts-loader": "~8.2.0",
    "typescript": "4.5.5"
  }
}

コンポーネントの動作確認実行用にuxpin、サーバーへのコードデプロイ用にpushを追加で定義しました。

uxpin-merge

こちらのコマンドで--disable-tunnelingオプションを付加して起動すると、UXPinのExperimental Modeが起動して自作コンポーネントの確認ができます。

なお、確認時にはUXPinにログインする必要があるので、事前にUXPinのサイトからログインしておくと確認がスムーズです。

uxpin-merge push

このコマンドは自作コンポーネントをサーバー側のライブラリにPushするコマンドです。2つオプションを指定しています。

--token

このトークンにはUXPin側で設定したライブラリに設定したトークンを指定します。

まずはMerge用のライブラリを以下のページの「Add new library」から作成していきます。

UXPin | Dashboard

「Import React Components」を選択して「Next」

「Library name」に適宜名前をつけて、権限設定を選択したら「Create library」で作成します。

作成したらライブラリを開きます。

開いた画面にトークン情報が表示されるのでこちらをトークンとして利用します。

--branch

こちらには自分のgitブランチ名を指定します。指定しない場合にはmasterが利用されるようです。

私は今回ローカル環境で特に意識していませんでしたが、gitのブランチをgit statusコマンドで確認するとmainブランチだったので明示的に指定をしています。

コンポーネントを作成する

では、肝心のコンポーネントを作成していきます。

今回はuxpin-merge initでサンプル作成されたコンポーネントと同様の階層に自作コンポーネントSampleButtonを作っていきます。

ファイル構成は以下のようになります。

$ cd src/components/
$ tree
.
├── Button
│   ├── Button.js
│   └── presets
│       └── 0-default.jsx
├── SampleButton
│   ├── SampleButton.tsx
│   └── presets
│       └── 0-default.jsx
└── UXPinWrapper
    └── UXPinWrapper.js

まずはコンポーネント自身のコードです。こちらはただのシンプルなボタンになっています。

src/components/SampleButton/SampleButton.tsx

import React from "react";

type Props = {
  disabled: boolean;
  label: string;
};

const SampleButton = ({ ...props }: Props) => {
  return (
    <div>
      <button type="button" disabled={props.disabled}>
        {props.label}
      </button>
    </div>
  );
};

export default SampleButton;

次にちょっと特殊な0-default.jsxです。これは、UXPin側でコンポーネントを表示させる場合のデフォルト値を設定するためのものです。フォルダ名とファイル名はこの名前が規定のようなのでそのまま指定しています。

src/components/SampleButton/presets/0-default.jsx

import * as React from "react";
import SampleButton from "../SampleButton";

export default (
  <SampleButton
    uxpId="SampleButton"
    disabled={false}
    label="I'm Sample"
  ></SampleButton>
);

コンポーネントが作成できたら、Next.js側でも動作を確認してみます。

ちょっと見るだけなので、pages/index.tsxに埋め込んで確認してみました。

pages/index.tsx

import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import SampleButton from '../src/components/SampleButton/SampleButton'
import styles from '../styles/Home.module.css'

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        
        <SampleButton disabled={false} label={'in Next.js'}></SampleButton>

(...snip...)

動かしてみましょう。

$ yarn dev

問題なさそうですね。ボタンのラベルも指定したもので表示されています。

UXPin MergeのExperimental Modeで確認する

さて、コンポーネントが作成できたのでUXPin MergeのExperimental Modeで確認したいと思います。

まずはコンポーネントを設定ファイルに登録する必要があるので、uxpin.config.jsを以下のように編集します。

uxpin.config.js

module.exports = {
  components: {
    categories: [
      {
        name: 'General',
        include: [
          'src/components/Button/Button.js',
          'src/components/SampleButton/SampleButton.tsx',
        ],
      },
    ],
    wrapper: 'src/components/UXPinWrapper/UXPinWrapper.js',
    webpackConfig: 'uxpin.webpack.config.js',
  },
  name: 'Example Design System'
};

コンポーネントの定義に、作成したSampleButtonコンポーネントのパスを指定して追加しました。

指定が済んだら以下のコマンドで起動させて、表示されたURLにアクセスします。

$ yarn uxpin
yarn run v1.22.17
$ uxpin-merge --disable-tunneling

You are using @uxpin/merge-cli version: 2.7.10


┌─────────┐
│         │
│  UXPin  │ {V}erge - Experimental Mode
│         │
└─────────┘

👩‍🔬 Open the following URL in your browser to enter the experimental mode:
https://app.uxpin.com/experiment/foobar?port=8877&name=Example Design System

表示されました!デフォルトのサンプルボタンButtonと、自作コンポーネントのSampleButtonが表示されています。

ラベルにもpresets/0-default.jsxに設定したデフォルト値I'm Sampleが入っていますね。

サーバーにPushする

最後に作成したライブラリをサーバーにPushしてみましょう。

$ git add .
$ git commit -m "[add]first commit"
$ yarn push
yarn run v1.22.17
$ uxpin-merge push --token "SampleToken12345678901234567890123456789" --branch main

You are using @uxpin/merge-cli version: 2.7.10


✅ Library bundle uploaded successfully!
✅ Library metadata uploaded successfully!
🛈  Projects using this Design System have been updated to branch [main]
✨  Done in 12.79s.

pushが完了したら、ブラウザでUXPin側にログインして先程トークンを取得した際に作成した自作ライブラリを開いてみましょう。

ちゃんとライブラリに登録されましたね。

あとは、デザイン画面上でも同様に参照可能なので、適宜画面に配置してデザインを行うことができます。

まとめ

以上、コードベースでのデザイン作成「UXPin Merge」を試してみました。

このツールの素敵なところは、ともかくローカル環境で作成したコンポーネントがそのままデザイン画面で利用できるところにあると思っています。また、Gitでコード管理が出来るのも、UXPin側へCLIコマンドでPushして連携できるのも素敵ですね。

今回は試していませんが、Circle CIやGitHub Actionsをうまく使ってCIを回すこともできるそうです。

Connecting code to UXPin (CI & push) | Merge

また以下には日本語のドキュメントも用意されていますので、こちらも参考になるかと思います。

どなたかのお役に立てば幸いです。それでは!

参考

以下は、今回特に参考にさせていただいたサイトです。