Salesforce B2C Commerce で利用できる sgmf-scripts について

※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。

今回の記事ではカートリッジの開発やCI/CDに利用できるツールである、[sgmf-scripts](https://www.npmjs.com/package/sgmf-scripts)を紹介いたします。
[Info Center](https://documentation.b2c.commercecloud.salesforce.com/DOC3/index.jsp)で「sgmf-scripts」で検索しても使い方は結構出て来ますので、参考にしてみてください。


## sgmf-scripts って何ができるの？

* カートリッジの作成
* カートリッジのアップロード
* ユニットテスト
* インテグレーションテスト
* JS、SCSSなどの静的ファイルのコンパイル
* JS、CSS ファイルのリントチェック
* リアルタイムでファイルをビルド＆アップロード



## sgmf-scripts を使ってみる

node-sassのバージョンの関係で、Node 14じゃないと動かなさそうです。
今回は `v14.17.6` を利用しました。


### 〜〜まず準備（カートリッジの作成）〜〜

1. 任意の場所に作業用フォルダを作成する
1. `npm install sgmf-scripts --save-dev`でインストール
1. `npx sgmf-scripts --createCartridge [任意のカートリッジ名]`　で環境を作成
1. `dw.json` 内に hostname, username, password を入力
    * 入力する内容は[以前紹介したQiita](https://qiita.com/syunyama/items/21c71ac9c49b332e126d#6--%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%82%AD%E3%83%BC%E3%81%AE%E8%A8%AD%E5%AE%9A)をご参照ください。


**SFRAの依存関係を利用してビルドする場合**

*  `cartridges` フォルダ内に `app_storefront_base` カートリッジを設置。
*  `package.json`内に下記の内容を追加して`base` というエイリアスでモジュールを読み込めるようにする。 

```json:package.json
{
====OMIT===
    "paths": {
        "base": "./cartridges/app_storefront_base"
    },
====OMIT===
}
```

* `npm install bootstrap --save-dev` でインストール
* 下記の内容をルートにある `webpack.config.js` と置き換える

```js:webpack.config.js
var path = require('path');
var webpack = require('sgmf-scripts').webpack;
var ExtractTextPlugin = require('sgmf-scripts')['extract-text-webpack-plugin'];
var sgmfScripts = require('sgmf-scripts');

var bootstrapPackages = {
  Alert: 'exports-loader?Alert!bootstrap/js/src/alert',
  // Button: 'exports-loader?Button!bootstrap/js/src/button',
  Carousel: 'exports-loader?Carousel!bootstrap/js/src/carousel',
  Collapse: 'exports-loader?Collapse!bootstrap/js/src/collapse',
  // Dropdown: 'exports-loader?Dropdown!bootstrap/js/src/dropdown',
  Modal: 'exports-loader?Modal!bootstrap/js/src/modal',
  // Popover: 'exports-loader?Popover!bootstrap/js/src/popover',
  Scrollspy: 'exports-loader?Scrollspy!bootstrap/js/src/scrollspy',
  Tab: 'exports-loader?Tab!bootstrap/js/src/tab',
  // Tooltip: 'exports-loader?Tooltip!bootstrap/js/src/tooltip',
  Util: 'exports-loader?Util!bootstrap/js/src/util'
};

module.exports = [
  {
    mode: 'production',
    name: 'js',
    entry: sgmfScripts.createJsPath(),
    output: {
      path: path.resolve('./cartridges/app_custom_sfra/cartridge/static'),
      filename: '[name].js'
    },
    module: {
      rules: [
        {
          test: /bootstrap(.)*\.js$/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/env'],
              plugins: ['@babel/plugin-proposal-object-rest-spread'],
              cacheDirectory: true
            }
          }
        }
      ]
    },
    plugins: [new webpack.ProvidePlugin(bootstrapPackages)]
  },
  {
    mode: 'none',
    name: 'scss',
    entry: sgmfScripts.createScssPath(),
    output: {
      path: path.resolve('./cartridges/app_custom_sfra/cartridge/static'),
      filename: '[name].css'
    },
    module: {
      rules: [
        {
          test: /\.scss$/,
          use: ExtractTextPlugin.extract({
            use: [
              {
                loader: 'css-loader',
                options: {
                  url: false,
                  minimize: true
                }
              },
              {
                loader: 'postcss-loader',
                options: {
                  // eslint-disable-next-line global-require
                  plugins: [require('autoprefixer')()]
                }
              },
              {
                loader: 'sass-loader',
                options: {
                  includePaths: [
                    path.resolve('node_modules'),
                    path.resolve('node_modules/flag-icon-css/sass')
                  ]
                }
              }
            ]
          })
        }
      ]
    },
    plugins: [new ExtractTextPlugin({ filename: '[name].css' })]
  }
];
```




### 〜〜カートリッジのアップロード〜〜

```sh
## 特定のカートリッジのアップロード
npm run uploadCartridge [カートリッジ名]
## 例： npm run uploadCartridge app_custom_sfra

## 特定のファイルのアップロード
npx sgmf-scripts --upload [ファイルパス(ワイルドカードも使える)]
## 例：npx sgmf-scripts --upload cartridges/app_custom_sfra/cartridge/controllers/*
```

※  `npm run upload [ファイルパス]`を利用する場合は、`package.json` 内の `"upload": "sgmf-scripts --upload --",` をから「--」を削除して  `"upload": "sgmf-scripts --upload",` に変更してください  。



### 〜〜ユニットテスト〜〜

* 任意のフォルダを作成（例： `test/unit`フォルダをフォルダのルートに作成）し、下記コマンドを実行。

```sh
## テスト実行
npx sgmf-scripts --test [ファイルパス]
## 例： sgmf-scripts --test test/unit/**/*.js

## テストカバレッジの取得(coverageフォルダに結果出力)
npx sgmf-scripts --cover [テストフォルダ]
## 例： sgmf-scripts --cover 'test/unit'
```


**[サンプルコード]テストされるコード**

```js:cartridges/app_custom_sfra/cartridge/scripts/test.js
'use strict';

var server = require('server');

function test(a) {
  return server.test(a);
}

module.exports = {
  test: test
};
```

**[サンプルコード]テストするコード**

```js:test/unit/app_custom_sfra/scripts/test.js
'use strict';

var assert = require('chai').assert;
var proxyquire = require('proxyquire').noCallThru().noPreserveCache();

describe('Test a + 1', function () {
  var test = proxyquire(
    '../../../../cartridges/app_custom_sfra/cartridge/scripts/test',
    {
      server: {
        test: function (a) {
          return a + 1;
        }
      }
    }
  );

  it("should append an object's key/value pairs to a provided url", function () {
    var result = test.test(1);
    assert.equal(result, 2);
  });
});
```




### 〜〜インテグレーションテスト〜〜

* 任意のフォルダを作成（例：`test/integration`フォルダをフォルダのルートに作成）し、下記コマンドを実行。

```sh
## インテグレーションテスト
npx sgmf-scripts --integration [テストファイルパス] --baseUrl [テストする環境のベースURL]
## 例： npx sgmf-scripts --integration test/integration/**/*.js --baseUrl https://zzjs-021.sandbox.us02.dx.commercecloud.salesforce.com/on/demandware.store/Sites-RefArch-Site/en_US
```


**[サンプルコード]テストされるコントローラー**

```js:cartridges/app_custom_sfra/cartridge/controllers/Test.js
'use strict';

var server = require('server');

server.get('Json', function (req, res, next) {
  res.json({ test: 'test' });
  return next();
});

module.exports = server.exports();
```

**[サンプルコード]テストするコード**

下記のサンプルコードを利用する前は下記のコマンドで `request-promise` をインストールしてください。

```sh
npm install request-promise --save-dev
```

```js:test/integration/sample/test.js
var assert = require('chai').assert;
var request = require('request-promise');
var optionator = require('optionator')(require('sgmf-scripts/lib/utils/options'));
var options = optionator.parse(process.argv);

describe('Test.js', function () {
  this.timeout(5000);

  it('Test test prop', function (done) {
    var myRequest = {
      url: '',
      method: 'GET',
      rejectUnauthorized: false,
      headers: {
        'X-Requested-With': 'XMLHttpRequest'
      }
    };
    myRequest.url = options.baseUrl + '/Test-Json';
    request(myRequest, function (error, response) {
      var bodyAsJson = JSON.parse(response.body);
      assert.equal(response.statusCode, 200, 'Expected statusCode to be 200.');
      assert.equal(bodyAsJson.test, 'test', 'Expected test prop equal "test".');
      done();
    });
  });
});
```



### 〜〜JS、 SCSSなどの静的ファイルのコンパイル〜〜

```sh
npx sgmf-scripts --compile js
npx sgmf-scripts --compile css
```


**[サンプルコード]JS**

```js:cartridges/app_custom_sfra/cartridge/client/default/js/test.js
'use strict';

// var processInclude = require('base/util');
$(document).ready(function () {
  // processInclude(require('./storeLocator/storeLocator'));
});
```


**[サンプルコード]SCSS**

```scss:cartridges/app_custom_sfra/cartridge/client/default/scss/test.scss
@import "base/homePage";

.test {
  color: red;
}
```



### 〜〜JS、CSS ファイルのリントチェック〜〜

下記のコマンドでリントをチェックする。

```sh
npx sgmf-scripts --lint css
npx sgmf-scripts --lint js
```


**※リントするにあたって何点か注意点があります。**

#### 1. テストコードの内のmocha のメソッドで `"describe" in not defined` のようなエラーが出る。静的ファイル内で `"$" in not defined` のようなエラーが出る。

`.eslintrc.json` 内に下記の行を追加してください。

```json:.eslintrc.json
"env": {
    "mocha": true,
    "jquery" : true
 }
```

#### 2. `app_storefront_base` カートリッジ内などの必要ないコードまでリントされてしまう。

下記のファイルをフォルダのルートに設置して、特定のcssとjsを対象から外す

**特定のJSを対象から外すサンプルコード**
```properties:.eslintignore
cartridges/app_storefront_base/**/*
cartridges/**/cartridge/static/default/js/**/*
coverage/
doc/
bin/
```

**特定のSCSSを対象から外すサンプルコード**
```properties:.stylelintignore
cartridges/app_storefront_base/**/*
cartridges/**/cartridge/static/default/css/**/*
```




### 〜〜リアルタイムでファイルをビルド＆アップロード〜〜

下記コマンドを実行すると、実行中に編集したファイルがリアルダイムにアップロードされる。（静的ファイルはビルドされる）

```sh
## リアルタイムでファイルをビルド＆アップロード
npx sgmf-scripts --watch
```