dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

LDSとdesign-system-reactをBabel&webpackな環境に導入する

メモ。
ちょっと前から Lightning Design System(LDS)の React 実装が公式から出てます。

Lightning Design System for React

で、ようやく入れてみようとドキュメントを頼りにやってみたところ色々手こずったので、手順をまとめておきます。


はじめに

design-system-react の設定に関するドキュメントは、以下のように点在しています。

  1. Getting Started
  2. リポジトリREADME#Usage
  3. リポジトリdocs/webpack.md (Usage with Webpack)

1 が一番参考にしたドキュメント。webpack.config.js の設定例なども載っています。基本はここを見ながらやりました。
ここに書かれてる内容は private repo で管理してるらしい) 2 は 1 と被る部分もあり、ただ Icon の設定に関してはここにしか記載されてません。
また 1, 2 は design-system-react のインストール手順であり、これだけだと LDS はインストールされず、スタイルがあたりません。
LDS を webpack から使えるようにするための情報が 3 に載っています。

以下では、設定手順を大きく「1. design-system-react の設定」と「2. LDS の設定」に分けて記載します。


サンプルコード

Redux & Express でのアプリのひな形リポジトリに導入したときの PR です。

ディレクトリ構成

クライアント側のファイルは public というディレクトリに配置するような構成になっています。

- public
  - assets
    - lds --- LDS はここに展開
    - bundle.js --- ビルド後のファイル
  - components --- React & Redux の
  - containers
  - ...
  - index.js --- エントリーポイント
  - index.html
- package.json
- webpack.config.base.babel.js
- webpack.config.dev.babel.js
- webpack.config.prod.babel.js


1. design-system-react の設定

まずは design-system-react のインストールから設定までの手順。主に先ほどの 1. Getting Started を参考にします。

インストール

npm でインストールできます。

$ npm install @salesforce-ux/design-system @salesforce/design-system-react


webpack の設定

Getting Started のページに webpack.config.js の設定例があるので、それを参考に設定していきます。

 // webpack.config.babel.js (抜粋)
 export default {
   context: path.resolve(path.join(__dirname, 'public')),
   entry: [
     './index',
   ],
+  resolve: {
+    extensions: ['.js', '.jsx'],
+  },
   output: {
     filename: 'bundle.js',
     path: path.resolve(path.join(__dirname, 'public', 'assets')),
     publicPath: '/assets/',
   },
   module: {
     rules: [
       {
-        test: /\.js$/,
-        exclude: /node_modules/,
+        test: /\.jsx?$/,
+        include: [
+          path.resolve(path.join(__dirname, 'public')),
+          path.resolve(path.join(__dirname, 'node_modules/@salesforce/design-system-react')),
+        ],
         use: 'babel-loader',
       },
       {
         ...

(ファイル全体は こちら を参照)

ポイントとしては

  • design-system-react のファイルは .jsx なので、 .jsx もビルドできるように resolve.extensions および babel-loader を適用する正規表現を修正
  • 元々 node_modules 以下はすべて babe-loader の対象外としていたが、design-system-react は対象に含めるよう修正

です。

また、この記事を書いた時点ではドキュメントの方では

path.join(__dirname, 'node_modules/design-system-react')

というように @salesforce が入ってなかったんですが、入れるのが正しいです。
Issue で報告したのでそのうち直るはず。


Babel の設定

.babelrc を修正します。
これには @salesforce/babel-preset-design-system-react という preset を使えとドキュメントにはありますが、中身

https://github.com/salesforce/design-system-react/blob/master/preset/index.js

を見てみると

  • babel-preset-env
  • babel-preset-react
  • babel-plugin-transform-object-rest-spread
  • babel-plugin-transform-class-properties
  • babel-plugin-transform-export-extensions

を指定しているだけなので、既に設定されている .babelrc の内容に合わせて必要な preset/plugin を個別に追加するという対応でもいいのかもしれません。
(実際自分のリポジトリでも babel-plugin-transform-export-extensions 以外は使っていた)

今回は素直にこの preset を使うようにします。

$ npm install -D @salesforce/babel-preset-design-system-react
// .babelrc
{
  "presets": ["@salesforce/babel-preset-design-system-react"]
}


使う

このような形で import して使います。

// components/App.js
import React, { Component } from 'react';
import Button from '@salesforce/design-system-react/components/button';

export default class App extends Component {
  render() {
    return (
      <Button
        iconName="download"
        iconPosition="left"
        label="Neutral Icon"
      />
    );
  }
}


2. LDS の設定

さて、今度は LDS を読み込むための設定をします。
これは docs/webpack.md がある程度参考になります。


インストール
$ npm install @salesforce-ux/design-system style-loader css-loader file-loader

必要になる loader も一緒にインストールしときます。


webpack の設定
 // webpack.config.babel.js
 ...
 export default {
   ...
   module: {
     rules: [
       ...
+      {
+        test: /\.min\.css$/,
+        use: ['style-loader', 'css-loader'],
+      },
+      {
+        test: /\.(eot|svg|ttf|woff|woff2)$/,
+        loader: 'file-loader',
+        options: {
+          name: '[name].[ext]',
+          outputPath: 'lds/',
+        },
+      },
       ...


postinstall 時の処理

次に、npm install 後に LDS を静的リソース配信用のディレクトリ(ここでは public/assets/lds/)にコピーしておきます。
ドキュメントでは postinstall.js スクリプトを作ってシンボリックリンクを貼る処理をやっているようですが、単にコピーするだけなら package.json 内に記述しても十分だと思います。

 // package.json
 {
   "scripts": {
+    "postinstall": "rm -rf public/assets/lds && cp -r node_modules/@salesforce-ux/design-system/assets public/assets/lds",
   ...
 }


読み込み

アプリのエントリーポイントで

 // public/index.js
 import { render } from 'react-dom';
 import { AppContainer } from 'react-hot-loader';
 
+import '@salesforce-ux/design-system/assets/styles/salesforce-lightning-design-system.min.css';
+
 import configureStore from './store/configureStore';
 import App from './containers/App';
 ...

とすれば OK です。


3. Icon パスの設定

最後にもう一点だけ。Icon を使う場合、アプリのルートで <IconSettings> コンポーネントを使い、パスを正しく設定しておく必要があります。

README#icon-usageSLDS React | Icon Settings あたりに記載があります。

 // public/index.js
 import { AppContainer } from 'react-hot-loader';
 
 import '@salesforce-ux/design-system/assets/styles/salesforce-lightning-design-system.min.css';
+import IconSettings from '@salesforce/design-system-react/components/icon-settings';
 
 import configureStore from './store/configureStore';
 import App from './containers/App';
 
 import './styles/index.scss';
 
 const store = configureStore();
 
 const rootEl = document.getElementById('root');
+const LDS_ROOT = 'assets/lds';
 
 render(
   <AppContainer>
     <Provider store={store}>
-      <App />
+      <IconSettings
+        standardSprite={`${LDS_ROOT}/icons/standard-sprite/svg/symbols.svg`}
+        utilitySprite={`${LDS_ROOT}/icons/utility-sprite/svg/symbols.svg`}
+        actionSprite={`${LDS_ROOT}/icons/action-sprite/svg/symbols.svg`}
+        doctypeSprite={`${LDS_ROOT}/icons/doctype-sprite/svg/symbols.svg`}
+        customSprite={`${LDS_ROOT}/icons/custom-sprite/svg/symbols.svg`}
+      >
+        <App />
+      </IconSettings>
     </Provider>
   </AppContainer>,
   rootEl,
 );

ここに関してはよくわかってないことが 2 点あって、

1) <IconSettings> には iconPath を指定する方法と、xxxSprite というアイコンの種類ごとのパスを指定する方法とあるんですが、webpack を使ってると後者を推奨してるようです。

Individual sprites If you are using webpack it is advised to use the sprite properties {actionSprite, standardSprite...} to specify the individual sprite paths so that webpack can easily re-write the paths.

この PR で追加されたようで、そこでも webpack のためにということが書かれていますが詳しいことはよくわからず。

2) xxxSprite の指定のしかたとして、Example にあるように

import actionSprite from '@salesforce-ux/design-system/assets/icons/action-sprite/svg/symbols.svg';

のようにすればいいのかなと思ったんですが、なぜかうまくいかず。。。
結局 postinstall でコピーした方のパスを指定してうまくいったのでそうしています。


結果

f:id:dackdive:20180206041536p:plain

<Button> だけですが、無事に LDS のスタイルがあたった状態でコンポーネントが描画できていることが確認できました。アイコンも表示されています。


おわりに

書いてみるとなんてことないんですが、 @salesforce がちょいちょい抜けてたりとドキュメントの整備が追いついてないみたいで結構ハマりました。。。
とりあえず使えるところまでできてよかった。


トラブルシューティング

遭遇したエラー達。

ERROR in ./node_modules/@salesforce/design-system-react/components/button/index.jsx
Module parse failed: Unexpected token (228:3)
You may need an appropriate loader to handle this file type.
ERROR in ./public/components/App.js
Module not found: Error: Can't resolve '@salesforce/design-system-react/components/button' in '/Users/yamazaki/workspace/react/redux-express-template/public/components'

→ webpack の設定不備。 .jsx がビルド対象になっていない、 node_modules/@salesforce/design-system-react が include されていない、などなど。

/Users/yamazaki/workspace/react/redux-express-template/node_modules/webpack/lib/webpack.js:19
                throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
        ^
WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
 - configuration.resolve.extensions[0] should not be empty.
   -> A non-empty string
    at webpack (/Users/yamazaki/workspace/react/redux-express-template/node_modules/webpack/lib/webpack.js:19:9)
    at Object.<anonymous> (/Users/yamazaki/workspace/react/redux-express-template/app.js:13:20)
    at Module._compile (module.js:643:30)
    at babelWatchLoader (/Users/yamazaki/workspace/react/redux-express-template/node_modules/babel-watch/runner.js:50:13)
    at Object.require.extensions.(anonymous function) [as .js] (/Users/yamazaki/workspace/react/redux-express-template/node_modules/babel-watch/runner.js:61:7)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Function.Module.runMain (module.js:684:10)
    at process.on (/Users/yamazaki/workspace/react/redux-express-template/node_modules/babel-watch/runner.js:108:21)

→ webpack の resolve.exntensions に空文字 ['', '.js', '.jsx'] は webpack 2 系以降 NG になったぽい。使うなら'*'