dackdive's blog

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

[Salesforce]Winter'19: Apex Replay Debuggerの使い方

昨日の Meetup の内容をブログにもまとめておきます。本当はこっちを事前に公開したかった。

なお Meetup で話したときの LT 資料はこちら。


Apex Replay Debugger とは

Winter'19 リリースノート:Apex Replay Debugger を使用してすべての組織を無料でデバッグ (正式リリース)

Apex Replay Debugger は VSCode拡張機能として使える Apex のデバッグ機能です。
Apex を実行した際に出力されるデバッグログファイルを元に、ログファイル出力時の状況を VSCode 上で再現(Replay)できます。

そのため、Apex をステップ実行したり、任意の行にブレークポイントを置いてその時点での変数の状態を確認することができます。


事前に必要なもの Prerequisites

Apex Replay Debugger for Visual Studio Code を参考に必要なものを事前にインストールしておきます。


セットアップ手順

プロジェクトフォルダを作成する

VSCode のコマンドパレット (Mac の場合 Cmd + Shift + P で開く) から

SFDX: Create Project
# または
SFDX: Create Project with Manifest

を実行してプロジェクトを作成します。

または、従来のディレクトリ構成 (src/ の下に package.xml を置き、Force.com Migration Tool などを使ってデプロイ) でも
sfdx-project.json を置きさえすればこの後の VSCode のコマンドを利用することは可能です。

`--src
     `--classes
     `--pages
     `--...
     `--package.xml
`--sfdx-project.json

(その場合、sfdx-project.jsonpath パラメータは force-app -> src にする必要あり)


起動構成ファイル (launch.json) を作成する

.vscode/launch.json ファイルを作成し、以下をコピペします。

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Apex Replay Debugger",
      "type": "apex-replay",
      "request": "launch",
      "logFile": "${command:AskForLogFileName}",
      "stopOnEntry": true,
      "trace": true
    }
  ]
}

もし、プロジェクトごとに設定するのが面倒な場合、ユーザー設定に "launch": ... という設定を追加することでも可能のようです。

f:id:dackdive:20181031003338p:plain

このあたりは VSCode のドキュメントが参考になります。
https://vscode-doc-jp.github.io/docs/userguide/debugging.html#起動構成


デバッグログレベルを FINEST にする

Replay Debugger を実行するためには、デバッグログレベルを

  • VISUALFORCE: FINER or FINEST
  • APEX_CODE: FINEST

に設定する必要があります。

これは、VSCode 上で

SFDX: Turn On Apex Debug Log for Replay Debugger

というコマンドを実行することで変更できます。


Replay Debugger の使い方


ソースコード上に Checkpoint を置く

処理を中断したりその時点での変数の状態を確認したい行にカーソルを置き、

SFDX: Toggle Checkpoint

を実行します。
行番号の左に赤いマークがつくのがわかります。これを Checkpoint と呼びます。

f:id:dackdive:20181031004307g:plain


Checkpoint を組織に反映させる

設置した Checkpoint は組織に反映させる必要があります。
事前に Scratch Org を作成したり、 sfdx force:auth:web:login などで認証を済ませておいてから

SFDX: Update Checkpoints in Org

を実行します。Output パネルに

Starting SFDX: Update Checkpoints in Org
SFDX: Update Checkpoints in Org, Step 1 of 6: Retrieving org information
SFDX: Update Checkpoints in Org, Step 2 of 6: Retrieving source and line information
SFDX: Update Checkpoints in Org, Step 3 of 6: Setting typeRefs for checkpoints
SFDX: Update Checkpoints in Org, Step 4 of 6: Clearing existing checkpoints
SFDX: Update Checkpoints in Org, Step 5 of 6: Uploading checkpoints
SFDX: Update Checkpoints in Org, Step 6 of 6: Confirming successful checkpoint creation
Ending SFDX: Update Checkpoints in Org

のように出力されれば成功です。

ちなみにこれは、開発者コンソールでエディタを開き、Checkpoint を設置したのと同じことをやっています。

f:id:dackdive:20181031004947p:plain


Apex を実行する

Checkpoint を設置した Apex コードを実行します。
実行にはいくつか方法がありますが、好きな方法で OK です。

  1. Visualforce ページなどで使われている Apex の場合、画面を直接操作する
  2. 該当の Apex のテストコードを実行する
  3. Execute Anonymous Window などで実行する

余談ですが特に VSCode を使った開発の場合、2 に関してはテストクラスを開くとメソッドの上に Run Test というリンクが表示されており、そこからメソッドごとにテストを実行させることができたり

f:id:dackdive:20181031005645p:plain

3 に関しては、適当なスクリプトファイルを作成し、開いた状態で

SFDX: Execute Anonymous Apex with Editor Contents

を実行すれば、開発者コンソールでの Open Execute Anonymous Window とほぼ同等のことを行えて便利です。


デバッグログファイルを取得する

Apex を実行したら、その時のログファイルをローカルにダウンロードします。
これについても VSCode 上で

SFDX: Get Apex Debug Logs...

を実行すると、新しい順にデバッグログの一覧が表示されるので、そこから選択するとダウンロードすることができます。

f:id:dackdive:20181031010027p:plain

ログは .sfdx/tools/debug/logs/ の下に保存されます。


デバッグログファイルから Replay Debugger を起動する

該当のログファイルを開いた状態で、コマンドパレットまたは右クリックして表示されるメニューから

SFDX: Launch Apex Replay Debugger with Current File

を実行します。
ログパネルが開き、またログファイルの1行目に黄色いカーソルが表示され、デバッグ用のメニューが表示されます。

f:id:dackdive:20181031010756p:plain


デバッグ:Apex をステップ実行したり、変数の中身を覗いてみる

この状態で |▶ ボタン (Continue: F5) を押すと、先ほどの Checkpoint まで処理が進み、該当行でストップしていることがわかります。
左側の VARIABLES ペインを見ると、この時点での変数に格納されている値を確認することができます。

f:id:dackdive:20181031011213p:plain

また、行番号の左のスペースをクリックすることで、 Checkpoint を設置した行以外の行にも Breakpoint を設置することができます。
次にデバッガを実行したときにはこの Breakpoint でも処理が中断されるようになります。
(Checkpoint と Breakpoint の違いは後述)


注意事項

今のところ以下の制約があるようです。

  • Checkpoint は最大5個まで。また有効期限は30分
  • 一度に実行できるのは1個のログファイルまで
    • そのため、非同期処理などのデバッグは注意が必要

詳細は Apex Replay Debugger の「Considerations」の項 を一読されることをおすすめします。


その他

Checkpoint と Breakpoint

SFDX: Toggle Checkpoint を使い、また組織にもデプロイした Checkpoint と、VSCode 上だけで定義した Breakpoint は、できること(得られる情報)が異なります。
「Set Breakpoints and Checkpoints」「Considerations」 あたりを読む限り、以下のような違いがあるようです。

For more information than line breakpoints provide, add checkpoints. You can set up to five checkpoints to get heap dumps when lines of code run. All local variables, static variables, and trigger context variables have better information at checkpoints. Trigger context variables don’t exist in logs and are available only at checkpoint locations.

トリガーのコンテキスト変数をはじめとして、Checkpoint の方が持っている情報が多いみたいです。

  • Long string variable values are truncated at breakpoints. At checkpoints, heap-dump-augmented variables have full strings.
  • When viewing a standard or custom object at a breakpoint, you can drill down only to the object’s immediate child variables (one level deep). At checkpoints, heap-dump-augmented variables have full drill-down to child standard objects, not only to immediate children.

Checkpoint の方が長い文字列も省略されなかったり、ネストしたオブジェクトの情報なども完全に保持しているらしい。


Apex Debugger との違い

名前がややこしいですが、VSCode拡張機能として Apex Debugger というのもあります。

ただ、こちらは

This extension enables VS Code to use the real-time Apex Debugger with your scratch orgs and to use ISV Customer Debugger with your subscribers’ sandbox orgs.

というところから対象組織は Scratch Org (および ISV 向けの機能については Sandbox Org) に限定されたり、

  • One Apex Debugger session is included with Performance Edition and Unlimited Edition orgs.
  • To purchase Apex Debugger sessions for Enterprise Edition orgs, or to purchase more sessions for orgs that already have allocated sessions, contact Salesforce.

から Performance Edition, Unlimited Edition 以外では有償のようです。

[Salesforce]UserRecordAccessでレコードの参照・編集権限をチェックする

こんなことやりたい

Apex で SOQL を実行してレコードを取得するとき、実行ユーザが編集権限のあるレコードだけ返したい。
このとき、レコードに対し参照権限だけあると SOQL では取得できてしまうため適宜フィルターする必要がある。


先にまとめ

UserRecordAccess オブジェクトというのがあるので、それを使いましょう。
UserRecordAccess | SOAP API 開発者ガイド | Salesforce Developers

UserRecordAccess には Has*Access ( *EditDelete など) という項目があるので、チェックしたい権限に応じた項目を使います。

また API バージョン 30.0 以降であれば UserRecordAccess は各 SObject のリレーション項目として取得できます。

例:SELECT Id, Name, Email, UserRecordAccess.HasEditAccess FROM Contact


方法

ほぼほぼこちらのテラスカイさんの記事にある通りです。感謝。
が、少々アップデートがあるみたいなので順を追って記載します。

ブログ記事にあるように、あるユーザが特定のレコードに対して参照・編集・削除などの権限を有しているかを判定するためのオブジェクトとして UserRecordAccess というオブジェクトがあります。

UserRecordAccess | SOAP API 開発者ガイド | Salesforce Developers

上のオブジェクト定義を見るとわかりますが、 UserRecordAccess には以下の項目があります。

  • 各種の権限があるかどうかのフラグ項目
    • HasReadAccess : 参照
    • HasEditAccess : 編集
    • HasDeleteAccess : 削除
    • ほか
  • UserId
  • RecordId

UserIdRecordId を WHERE 句に指定することで、「あるユーザが特定のレコードに対して権限を有しているか」を判断できるわけです。

なので、冒頭に記載したように「編集権限のあるレコードだけ返す」を実現するためには、こういったメソッドを用意すればいいでしょう。

public List<Contact> getEditableContacts() {
    Map<Id, Contact> contactMap = new Map<Id, Contact>([
        SELECT Id, Name, Email FROM Contact
    ]);
                                                         
    List<UserRecordAccess> accesses = [
        SELECT
            RecordId, HasEditAccess
        FROM
            UserRecordAccess
        WHERE
            UserId = :UserInfo.getUserId()
        AND
            RecordId IN :contactMap.keySet()
    ];
                                                         
    List<Contact> result = new List<Contact>();
    for (UserRecordAccess access : accesses) {
        if (access.HasEditAccess) {
            result.add(contactMap.get(access.RecordId));
        }
    }
    return result;
}


制限事項

というわけで便利なオブジェクトですが、制限事項がいくつかあります。
↑のテラスカイさんのブログ記事でも述べられていますが、個人的に気になったものを挙げると

  • RecordId は単一または複数のレコードを WHERE 句に指定できるが、 UserId は単一のみ
    • UserId IN *** とすると Error: Can filter on only UserId = [single ID], either RecordId = [single ID] or RecordId IN [list of IDs], and Has*Access = true というエラーが出る
  • 一度に取得できるレコードは 200 件までで、SOQL の結果が 200 件を超えると QueryException が発生する

特に後者の制限が厳しいですね。。。


より良い方法

制限事項を回避できる何か良い方法はないのかと SOAP API 開発者ガイド を最後まで読むと、

API バージョン 30.0 以降では、UserRecordAccess はレコードの外部キーになります。このオブジェクトをルックアップまたは外部キーとして使用する場合は、UserId または RecordId 項目を検索条件に使用したり、指定したりすることはできません。前記のサンプルクエリは、次のように実行できます。
SELECT Id, Name, UserRecordAccess.HasReadAccess, UserRecordAccess.HasTransferAccess, UserRecordAccess.MaxAccessLevel FROM Account

お! UserRecordAccessAPI バージョン 30.0 以降だと任意の SObject のリレーション項目として一緒に取得できるようです。
なので、先ほどのサンプルは以下のように書き直すことができます。

/** Since API version 30.0 **/
public List<Contact> getEditableContacts() {
    List<Contact> contacts = [
        SELECT
            Id, Name, Email,
            UserRecordAccess.HasEditAccess
        FROM
            Contact
    ];
    List<Contact> result = new List<Contact>();
    for (Contact contact : contacts) {
        System.debug('Contact [' + contact.Name + '] is editable?: ' + contact.UserRecordAccess.HasEditAccess);
        if (contact.UserRecordAccess.HasEditAccess) {
            result.add(contact);
        }
    }
    return result;
}

これにより、制限事項のうち「一度に取得できるレコードは 200 件まで」は回避できました。よかった。

ただし、 UserRecordAccess.Has*Access は WHERE 句に直接は指定できず、以下のエラーになります。

Error: You cannot filter on UserRecordAccess when used in a relationship


注意事項

Task, Event など一部の標準オブジェクトについては、UserRecordAccess を項目として取得する方法は使えないようです。

UserRecordAccess オブジェクトへのクエリが一度に 200 件までという制限があることなど、内部的にパフォーマンスに影響ありそうなことをやってそうな香りがしますね。
実際に使うときはパフォーマンス面での影響を少し意識しなければ。


サンプルコード

https://github.com/zaki-yama/salesforce-samples/tree/master/user-record-access

[Salesforce]ワークフローの時間ベースのアクションについて

今さらながら時間ベースのワークフローを使うことがあったのでメモ。

時間ベースのワークフローとは

f:id:dackdive:20180817101627p:plain

ここのこと。

時間ベースのワークフローで何ができる?

時間ベースじゃない方のワークフローアクションだと、ルール条件に一致した場合レコードの作成または編集直後にアクションが実行される。
これに対し、時間ベースのアクションは文字通り、アクションの実行を将来のある時点に予約しておくことができる。

具体的なユースケースとして、たとえば
「金額が1000万円以上の大型商談について、完了予定日の7日前になった時点でクローズされていなければ、商談所有者にリマインドメールを送信する」
といったことができる。


将来のある時点、は何を基準に決定される?

レコードの作成日やルールが適用された日だけでなく、対象のオブジェクトの日付項目の値を基準にアクション実行のタイミングを制御することもできる。

タイミングの制御は基準となる項目の日付から数えて◯日前(後)、といった指定方法になる。

f:id:dackdive:20180817102652p:plain

↑では例として、商談オブジェクトのレコードに対し、「完了予定日」の7日前に実行されるアクションを設定している。


アクションが予約されたことをどうやって確認するの?

LEXであれば 環境>監視>時間ベースのワークフロー から、現在セットされている時間ベースのアクションを確認できる。

f:id:dackdive:20180817103443p:plain


アクションは一度予約されたら必ず実行される?

NO。
一度ルール条件に合致してアクションがセットされても、その後レコードが更新されてルール条件を満たさなくなれば、アクションは実行されない。

冒頭の例でいうと、

  • レコード作成時は金額1000万以上だったが、その後1000万未満になった
  • 完了予定日の7日前より以前に商談がロストした/クローズした

場合はリマインドメールは送信されない。

参考:FAQ - 時間ベースのワークフロー

キューの待機中のアクションは、必ず起動されますか?
いいえ。時間ベースのアクションは、プロセスまたはワークフロールールのルール条件が “false” と評価される限り、ワークフローキューに留まります。ルールが評価されたときに、レコードがルール条件に一致しなくなると、Salesforce はそのレコードについてキューにある時間ベースのアクションを削除します。


評価条件が「作成されたとき」か「作成されたとき、およびその後基準を満たすように編集されたとき」で挙動にどういう違いがあるの?

これは時間ベースじゃないアクションと同じ。
「作成されたとき」の場合はレコード作成時しか評価しないので、たとえば冒頭の例でいうと

  • 一度ロストした商談を再度 Prospecting に戻した(その時点ではまだ完了予定日の7日前より以前だった)

という操作をした場合も、リマインドメールの対象になるわけではない。

参考:FAQ - 時間ベースのワークフロー

レコードの待機中のアクションをキューに戻すことはできますか?
はい。レコードが更新され、評価条件をレコードが [作成されたとき、およびその後基準を満たすように編集されたとき] に設定した場合は、自動的にキューに戻されます (レコードは、入力された条件を以前に満たしていない必要があります)。

※これは余談だが、この直後にある例は日本語記事だとなぜか間違っている(1個前のと同じ例になっている)


プロセスビルダーでも同じことできないの?

できますね。「スケジュール済みアクション」という名前みたい。

f:id:dackdive:20180817105810p:plain

注意点として、プロセスの開始条件を「レコードを作成または編集したとき」にしている場合、スケジュール済みアクションを使えるようにするには

「レコードに指定の変更が行われた場合にのみアクションを実行しますか?」

にチェックを入れる必要がある。


リファレンス

Pipenvで仮想環境をプロジェクトディレクトリの下に作る(PIPENV_VENV_IN_PROJECT)

ちょいメモ。

久しぶりに Python を書くにあたって環境構築する際、 2018年のPythonプロジェクトのはじめかた - Qiita を見て Pipenv を使ってみた。

普通に pipenv shell で仮想環境を作成すると

 ~/workspace/Python/pipenv-sandbox $ pipenv shell
Creating a virtualenv for this project…
Using /Users/yamazaki/.pyenv/versions/3.6.5/bin/python3.6m (3.6.5) to create virtualenv…
⠋Running virtualenv with interpreter /Users/yamazaki/.pyenv/versions/3.6.5/bin/python3.6m
Using base prefix '/Users/yamazaki/.pyenv/versions/3.6.5'
New python executable in /Users/yamazaki/.local/share/virtualenvs/pipenv-sandbox-9fl74ZVx/bin/python3.6m
Also creating executable in /Users/yamazaki/.local/share/virtualenvs/pipenv-sandbox-9fl74ZVx/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location: /Users/yamazaki/.local/share/virtualenvs/pipenv-sandbox-9fl74ZVx
Spawning environment shell (/bin/zsh). Use 'exit' to leave.
. /Users/yamazaki/.local/share/virtualenvs/pipenv-sandbox-9fl74ZVx/bin/activate
 ~/workspace/Python/pipenv-sandbox $ . /Users/yamazaki/.local/share/virtualenvs/pipenv-sandbox-9fl74ZVx/bin/activate
(pipenv-sandbox-9fl74ZVx)  ~/workspace/Python/pipenv-sandbox $

というように、プロジェクトディレクトリとは別のグローバルな場所( ~/.local/share/virtualenvs/ )に仮想環境が作られる。

これを、

$ virtualenv venv

したときと同じように、各プロジェクトディレクトリの下に作成したい。

Configuration With Environment Variables を読むと

  • PIPENV_VENV_IN_PROJECT — If set, use .venv in your project directory instead of the global virtualenv manager pew.

とあり、 .zshrc

export PIPENV_VENV_IN_PROJECT=true

を追加したところ、プロジェクトディレクトリの下に仮想環境が作成されるようになった。

 ~/workspace/Python/pipenv-sandbox $ pipenv shell
Creating a virtualenv for this project…
Using /Users/yamazaki/.pyenv/versions/3.6.5/bin/python3.6m (3.6.5) to create virtualenv…
⠋Running virtualenv with interpreter /Users/yamazaki/.pyenv/versions/3.6.5/bin/python3.6m
Using base prefix '/Users/yamazaki/.pyenv/versions/3.6.5'
New python executable in /Users/yamazaki/workspace/Python/pipenv-sandbox/.venv/bin/python3.6m
Also creating executable in /Users/yamazaki/workspace/Python/pipenv-sandbox/.venv/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location: /Users/yamazaki/workspace/Python/pipenv-sandbox/.venv
Spawning environment shell (/bin/zsh). Use 'exit' to leave.
. /Users/yamazaki/workspace/Python/pipenv-sandbox/.venv/bin/activate
 ~/workspace/Python/pipenv-sandbox $ . /Users/yamazaki/workspace/Python/pipenv-sandbox/.venv/bin/activate
(pipenv-sandbox) ~/workspace/Python/pipenv-sandbox $ 

# .venv ディレクトリが作られている
(pipenv-sandbox) ~/workspace/Python/pipenv-sandbox $ ls -al
total 8
drwxr-xr-x  4 yamazaki  staff  136  5 16 02:33 ./
drwxr-xr-x  8 yamazaki  staff  272  5 16 02:08 ../
drwxr-xr-x  5 yamazaki  staff  170  5 16 02:33 .venv/
-rw-r--r--  1 yamazaki  staff  138  5 16 02:08 Pipfile

.venv というディレクトリ名は変更できないのかな。
個人的にはこちらの方が好みだけど、両者にメリットデメリットあるのかは不明。

ちなみに

仮想環境が作られた場所を確認するには --venv オプションを使う。

(pipenv-sandbox-9fl74ZVx)  ~/workspace/Python/pipenv-sandbox $ pipenv --venv
/Users/yamazaki/.local/share/virtualenvs/pipenv-sandbox-9fl74ZVx

React 16.3.0で追加されたStrictModeコンポーネントについて

2018-04-01のJS: TypeScript 2.8、React 16.3.0、TensorFlow.js - JSer.info を読んで。

React 16.3.0 から StrictMode コンポーネントというものが追加されたらしい。
公式ドキュメントを読んでみます。


StrictMode とは

StrictMode はアプリの潜在的な問題を検出するために追加されたコンポーネントコンポーネントだが Fragment などと同じく UI として画面に表示されるものはない。
<StrictMode>...</StrictMode> で囲まれた子孫コンポーネントに対し、いくつかのチェックを行う。

また development モードでのみ動作し、 production build 時には影響を与えない。


ScrictMode がチェックしてくれること

今のところ以下。今後のリリースで機能は追加予定とのこと(Additional functionality will be added with future releases of React.)

  1. 安全でないライフサイクルメソッドの使用(Identifying components with unsafe lifecycles)
  2. レガシーな string ref の使用(Warning about legacy string ref API usage)
  3. 予期せぬ副作用の検出(Detecting unexpected side effects)
    • (検出するために、特定のライフサイクルメソッドを二度実行する)


1. 安全でないライフサイクルメソッドの使用

背景として、v16.3.0 以降は非同期レンダリングなどのサポートのために一部のライフサイクルメソッド

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

が今後削除予定となった。
参考:Update on Async Rendering - React Blog (こっちはまだ読んでない)

<StrictMode> の子孫コンポーネントでこれらのライフサイクルメソッドを使用しているものがあれば、ブラウザのコンソールで warning が出力される。

(サンプル)

import React, { Component, StrictMode } from 'react';

class UnsafeComponent extends Component {
  componentWillMount() {
    console.log('componentWillMount');
  }

  componentWillReceiveProps(props) {
    console.log('componentWillReceiveProps');
  }

  render() {
    return <div>Unsafe Component</div>;
  }
}

export default function App() {
  return (
    <StrictMode>
      <UnsafeComponent />
    </StrictMode>
  );
}

(結果) f:id:dackdive:20180404015423p:plain

Warning: Unsafe lifecycle methods were found within a strict-mode tree:
    in App
    in AppContainer

componentWillMount: Please update the following components to use componentDidMount instead: UnsafeComponent

componentWillReceiveProps: Please update the following components to use static getDerivedStateFromProps instead: UnsafeComponent

Learn more about this warning here:
https://fb.me/react-strict-mode-warnings

該当のコンポーネント名と使っているライフサイクルメソッドが表示されている。


2. レガシーな string ref の使用

ref を使ってコンポーネントを参照するための方法はこれまで2通りあって

  1. ref="input" のように文字列で指定する
  2. ref={(element) => this.input = element} のように callback 関数で指定する

このうち 1 の文字列で指定する方にはいくつか問題があったらしく、ドキュメントでも 2 の方法を推奨していた。
<StrictMode> の子孫コンポーネントで 1 の string ref を使っている箇所があると、こちらも同様にブラウザのコンソールでwarning が出る。

(サンプル)

import React, { Component, StrictMode } from 'react';

class LegacyRef extends Component {
  handleClick = () => {
    const name = this.refs.name.value;
    console.log('LegacyRef', name);
  };

  render() {
    return (
      <div>
        <input ref="name" />
        <button onClick={this.handleClick}>click me</button>
      </div>
    );
  }
}

class NewRef extends Component {
  constructor(props) {
    super(props);

    this.nameRef = React.createRef();
  }

  handleClick = () => {
    const name = this.nameRef.current.value;
    console.log('NewRef', name);
  };

  render() {
    return (
      <div>
        <input ref={this.nameRef} />
        <button onClick={this.handleClick}>click me</button>
      </div>
    );
  }
}

export default function App() {
  return (
    <StrictMode>
      <LegacyRef />
      <NewRef />
    </StrictMode>
  );
}

(結果) f:id:dackdive:20180404021000p:plain

Warning: A string ref, "name", has been found within a strict mode tree. String refs are a source of potential bugs and should be avoided. We recommend using createRef() instead.

    in div (created by LegacyRef)
    in LegacyRef (created by App)
    in App
    in AppContainer

Learn more about using refs safely here:
https://fb.me/react-strict-mode-string-ref


余談: createRef() を使った新しい ref の方法

上のサンプルで、 NewRef コンポーネントがやっているのは v16.3.0 から追加された新しい ref の実装方法で、 createRef() という関数を使う。
従来の string ref のような書き方で、かつ string ref のときの問題点は解決されている。らしい。

参考:React v16.3.0: New lifecycles and context API - React BlogcreateRef API の項

なお createRef() の導入後も 2 の callback を使った方法はサポートされるので、置き換える必要はない。


3. 予期せぬ副作用の検出

背景として、React の動作時には大きく2つのフェーズがある。

  • render フェーズ:DOM に適用する必要のある変更を決定する。 render メソッドが呼ばれ、結果を直前の render の結果と比較する
  • commit フェーズ:React がすべての変更を DOM に適用する。 componentDidMountcomponentDidUpdate などのライフサイクルメソッドもこのフェーズで呼ばれる

一般に commit フェーズは速いが render は遅い。そのため、非同期レンダリングによってレンダリング処理を複数の小さな処理に分割し、ブラウザをブロックしないように停止と再開をしながらレンダリングを行う。
これにより、commit 前に render フェーズのライフサイクルメソッドが複数回実行される可能性が生じる。
(このあたりはよくわかっていない)

ので、これらのライフサイクルメソッドに副作用がないことが重要となる。
これらのライフサイクルメソッドとは具体的には以下。

  • Class コンポーネントconstructor メソッド
  • render
  • setState の第一引数に関数を渡したときの関数(updater と呼ぶらしい)
  • getDerivedStateFromProps (v16.3.0 から componentWillReceiveProps の代替として追加)

ただ、これらのメソッドに副作用がないことを自動的に検出することは難しいため、StrictMode ではこれらのメソッドを2回ずつ実行する。

(サンプル)

class SideEffect extends Component {
  constructor(props) {
    super(props);

    this.state = { count: 0 };
    console.log('SideEffect constructor');
  }

  increment = () => {
    this.setState((prevState, props) => {
      console.log('SideEffect updater', prevState);
      return {
        count: prevState.count + 1,
      };
    });
  };

  render() {
    console.log('SideEffect render');
    return <div onClick={this.increment}>{this.state.count}</div>;
  }
}

export default function App() {
  return (
    <StrictMode>
      <SideEffect />
    </StrictMode>
  );
}

(結果)

f:id:dackdive:20180404023839p:plain


コード

一応上げておく。

https://github.com/zaki-yama/react-strict-mode-example

Node.js製CLIフレームワークoclifを試す

はじめに

Heroku が oclif という CLI フレームワークオープンソースとして公開したという記事を読みました。

Heroku CLISalesforce DX のベースにもなっているらしい。
どんなもんか触ってみます。

(oclif は (The) Open CLI Framework の略のようです。読み方がわからない。。。)


oclif の特徴

手を動かす前に、どういった特徴があるのか公式ドキュメントに目を通してみます。
Features · oclif: The Open CLI Framework

  • Super Speed
    • コマンド実行時のオーバーヘッド(?)がほとんどなく、また依存パッケージもほとんどない
    • 実行されるコマンドだけ require されるので、たくさんのコマンドからなる巨大 CLI でも単一コマンドの CLI と速度が変わらない
  • CLI Generator
    • コマンド一発で scaffold が生成できる generator がある
  • Testing Helpers
    • テストが書きやすい。 stdout/stderr を簡単にモックできる
    • generator がテストの scaffold も自動生成する
  • Auto-documentation
    • --help オプションで表示するヘルプテキストが自動生成される
    • ↑は CLI が publish されるときに README にも自動的に記載される(※最後で軽く触れる)
  • (未確認)Plugins
  • (未確認)Hooks
    • コマンド実行時などのライフサイクルイベントや独自にカスタムイベントを定義し、そこにフックする処理を書ける
  • TypeScript
    • TypeScript と JavaScript 両方をサポート
    • oclif 自体も TypeScript で書かれている
  • Coming soon: man pages, Autocomplete

テストしやすく、ドキュメントが自動生成されるのはいいですね。


Single-command と Multi-command

oclif で作成できる CLI には大きく分けて 2 種類あります。
Single-command とは lscurl のように、コマンド自体は1つで引数やオプションを取るものです。
Multi-command とは githeroku のように、後にサブコマンドが続くものです。


試してみる

今回は、自分が過去に Node.js で作った parse-salesforce-object という CLI に oclif を導入してみます。
Salesforce 開発で使うメタデータファイル(XML)をパースしてよしなに表示してくれるという、ごくごく一部の方にしか需要がないやつです)
この CLI は先ほどの分類で言うと Single-command です。


generator で CLI のひな形(scaffold)作成

Quickstart を参考に、generator を使って必要なファイルを生成します。

$ npx oclif single [コマンド名]

を実行すると途中で色々聞かれるので適宜入力します。

$ npx oclif single parse-salesforce-object
npx: 293個のパッケージを17.261秒でインストールしました。

     _-----_     ╭──────────────────────────╮
    |       |    │      Time to build a     │
    |--(o)--|    │  single-command CLI with │
   `---------´   │   oclif! Version: 1.7.9
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? npm package name parse-salesforce-object
? command bin name the CLI will export parse-salesforce-object
? description
? author Shingo Yamazaki @zaki-yama
? version 0.0.5
? license MIT
? node version supported >=8.0.0
? github owner of repository (https://github.com/OWNER/repo) zaki-yama
? github name of repository (https://github.com/owner/REPO) parse-salesforce-object
? optional components to include
❯◉ yarn (npm alternative)
 ◉ mocha (testing framework)
 ◉ typescript (static typing for javascript)
 ◉ tslint (static analysis tool for typescript)
 ◯ semantic-release (automated version management)

最後のオプションで yarn, mocha, TypeScript を使うかどうかは好みです。

インストールが完了すると、 [コマンド名]ディレクトリが作成され、その下に必要なファイルが揃っています。
npm install (or yarn )も実行されているため、必要なパッケージもインストール済みです。

$ tree -I node_modules parse-salesforce-object
parse-salesforce-object
├── README.md
├── appveyor.yml
├── bin
│   ├── run
│   └── run.cmd
├── package.json
├── src
│   └── index.ts
├── test
│   ├── helpers
│   │   └── init.js
│   ├── index.test.ts
│   ├── mocha.opts
│   └── tsconfig.json
├── tsconfig.json
├── tslint.json
└── yarn.lock

bin/run を実行するとコマンドを実行できます。

$ cd parse-salesforce-object
$ ./bin/run
hello world from /Users/yamazaki/workspace/nodejs/parse-salesforce-object/src/index.ts!


コマンドファイルの構成

src/index.ts がコマンド本体です。中身を見てみます。
((1) ~ (3) は便宜的にこちらで番号を振りました)

import {Command, flags} from '@oclif/command'

class ParseSalesforceObject extends Command {
  // (3)
  static description = 'describe the command here'

  // (3)
  static examples = [
    `$ parse-salesforce-object
hello world from ./src/parse-salesforce-object.ts!
`,
  ]

  // (2)
  static flags = {
    // add --version flag to show CLI version
    version: flags.version({char: 'v'}),
    // add --help flag to show CLI version
    help: flags.help({char: 'h'}),

    // flag with a value (-n, --name=VALUE)
    name: flags.string({char: 'n', description: 'name to print'}),
    force: flags.boolean({char: 'f'}),
  }

  // (2)
  static args = [{name: 'file'}]

  // (1)
  async run() {
    const {args, flags} = this.parse(ParseSalesforceObject)

    const name = flags.name || 'world'
    this.log(`hello ${name} from ${__filename}!`)
    if (args.file && flags.force) {
      this.log(`you input --force and --file: ${args.file}`)
    }
  }
}

export = ParseSalesforceObject
  • (1) コマンドの実処理は run() メソッドに記述します
  • (2) 引数やオプション(flags)はこのように static 変数として定義します。この後ここをカスタマイズしてみます
  • (3) description, examples も同様に static 変数として定義すると、ヘルプテキストに反映されます

最後の (3) について、実際にコマンドを --help オプションつきで実行すると

$ ./bin/run --help
describe the command here

USAGE
  $ parse-salesforce-object [FILE]

OPTIONS
  -f, --force
  -h, --help       show CLI help
  -n, --name=name  name to print
  -v, --version    show CLI version

EXAMPLE
  $ parse-salesforce-object
  hello world from ./src/parse-salesforce-object.ts!

のように、description および examples に記述した文字列がヘルプの先頭と EXAMPLE セクションに記載されているのがわかります。


引数を処理する

さて、ここからひな形をベースに元の CLI としての機能を実装していきます。
まずは引数の処理から。
参考:Command Arguments · oclif: The Open CLI Framework

元の CLI では

const argv = require('minimist')(process.argv.slice(2));
const filePath = argv._[0];

...

if (!filePath) {
  console.log(chalk.red('ERROR: You must specify a path to .object file.'));
  process.exit(1);
}

fs.readFile(filePath, (err, data) => {
  ...
});

のように、とあるファイルへのパスを必須の引数として受け取るようになっていました。
またその処理のために minimist というライブラリを使っていました。

oclif だと以下のように書けます。

class ParseSalesforceObject extends Command {

  ...

  static args = [{
    name: 'path',
    description: 'path to .object file',
    required: true,
  }]

  async run() {
    const {args, flags} = this.parse(ParseSalesforceObject)

    fs.readFile(args.path, (err, data) => {
      ...
    })
  }

引数には { name: 'foo' } という形で名前を付けておくことができ、run() メソッド内で(パース後に) args.foo でアクセスできます。

その他のオプションは https://oclif.io/docs/args.html を参照するといいです。
必須かどうかもオプションで指定できるようになったので判定処理が不要になりました。

# 引数なしで実行するとエラーになる
$ ./bin/run
 ›   Error: Missing 1 required arg:
 ›   path  path to .object file
 ›   See more help with --help


フラグ(オプション)を処理する

続いて、いくつかのフラグを受け取れるようにします。
フラグとは -f foo--file=foo のようなものを指します。
参考:Command Flags · oclif: The Open CLI Framework

フラグは version と help 以外は

static flags = {
  force: flags.boolean({char: 'f'}),
  file: flags.string(),
}

のように、

  • 引数を受け取るもの: flags.string()
  • 引数を受け取らず、boolean として使うもの: flags.boolean()

の 2 種類あります。

また両者に共通して、{char: 'f'} のように char オプションを指定すると短縮形も扱えるようになります。

その他のオプションは https://oclif.io/docs/flags.html を参照します。

元の CLI には、-f xxx または --format=xxx オプションで出力フォーマットを指定でき、その選択肢は markdown, csv, soql のいずれかとなっていたので

format: flags.string({
  char: 'f',
  description: 'output format',
  options: ['markdown', 'csv', 'soql'],
  default: 'markdown',
}),

のように optionsdefault を利用しました。便利。

# -f で許可されているフォーマット以外を指定するとエラー
$ ./bin/run objects/Expense__c.object -f foo 
 ›   Error: Expected --format=foo to be one of: markdown, csv, soql
 ›   See more help with --help


ヘルプを出力してみる

引数やオプションを一通り定義した後で、 --help によりヘルプを表示してみます。

$ ./bin/run --help
USAGE
  $ parse-salesforce-object PATH

ARGUMENTS
  PATH  path to .object file

OPTIONS
  -f, --format=markdown|csv|soql  [default: markdown] output format
  -h, --help                      show CLI help
  -n, --namespace=namespace       namespace prefix (for SOQL format)
  -v, --version                   show CLI version

EXAMPLE
  $ parse-salesforce-object src/objects/Expense__c.object
  | label       | fullName      | type     | required |
  | ----------- | ------------- | -------- | -------- |
  | Amount      | Amount__c     | Number   | false    |
  | Client      | Client__c     | Text     | false    |
  | Date        | Date__c       | DateTime | false    |
  | Reimbursed? | Reimbursed__c | Checkbox | null     |

ARGUMENTS および OPTIONS のセクションのところが、定義した引数・フラグの内容から自動生成されました。


GitHub

今回 oclif での置き換えを試した CLIリポジトリはここにあります。

PR は https://github.com/zaki-yama/parse-salesforce-object/pull/6


まとめと TODO

今回は oclif の導入手順と基本構成についてなんとなくわかったという程度ですが、個人的には

  • npx oclif single/multi foo で scaffold から始められるのは楽
  • 引数やオプションからヘルプ自動生成は便利

といった点が、フレームワークというだけあって良いなと思いました。
また将来的に Autocomplete もサポートしてくれるのは期待したい。

テストを書くところや Plugins、Hooks については試せてないので、今後の TODO ということで。


おまけ:Auto-documentation について

Features#Auto-documentation には

This information is also automatically placed in the README whenever the npm package of the CLI is published. See the multi-command CLI example

と記載がありますが、自動生成したヘルプを README に埋め込む方法はドキュメントに記載がありませんでした。
リンクされてるリポジトリを見ると oclif-dev という CLI を使ってる っぽいので、これかな...?


あわせて読みたい

Salesforce のエンジニアブログにも記事があった。

Vim+ALEでファイル保存時にPrettierを実行する

メモ。
Prettier という JavaScript のフォーマッターをファイル保存時に自動的に実行する、というのを Vim でやりたい。
特に自分は ESLint や Flow のチェックに ALE というプラグインを使っているため
(参考:VimでESLintとFlowを使うためにNeomakeからALEに乗り換える - Qiita
Prettier も同じように ALE で設定できないのか調べた。

すると、ちゃんと Prettier の公式ドキュメントに ALE での設定方法が載ってた。

Prettier 単体で使う場合は上記を読むのが一番早いが、自分は ESLint と併用するために prettier-eslint-cli)を使っているのでその前提で手順を記載する。


設定手順

Vim

ALE の設定として以下を追加する。

let g:ale_fixers = {}
let g:ale_fixers['javascript'] = ['prettier-eslint']

" ファイル保存時に実行
let g:ale_fix_on_save = 1

" ローカルの設定ファイルを考慮する
let g:ale_javascript_prettier_use_local_config = 1

プラグイン管理に dein.vim を使っており、toml で管理している場合は以下のようになる

# rc/dein.toml

[[plugins]]
repo = 'w0rp/ale'
hook_add = '''
let g:ale_statusline_format = ['E%d', 'W%d', 'OK']

nmap <silent> <C-w>j <Plug>(ale_next_wrap)
nmap <silent> <C-w>k <Plug>(ale_previous_wrap)

let g:ale_fixers = {}
let g:ale_fixers['javascript'] = ['prettier-eslint']

" ファイル保存時に実行
let g:ale_fix_on_save = 1

" ローカルの設定ファイルを考慮する
let g:ale_javascript_prettier_use_local_config = 1
'''

let g:ale_fixers['javascript'] = ['prettier-eslint'] の部分、prettier 単体では ['prettier'] でいいが ESLint と併用している場合は prettier-eslint-cli を使うため上記の設定になる。


プロジェクト側

prettier-eslint-cli をインストールしておくだけ。

$ yarn add -D prettier-eslint-cli


様子

f:id:dackdive:20180330013445g:plain

あんまり早くない。。。
非同期で行われるためそこまで気にならないかもしれないけど。
VSCode とかでもこんなもんなんだろうか。


GitHub

以上を反映した Redux アプリ用テンプレートリポジトリがこちらです。
Prettier 導入の Issue は https://github.com/zaki-yama/redux-express-template/issues/16


リファレンス

冒頭の Prettier のドキュメントのほか、ALE の README でも fixer については記載があった。
https://github.com/w0rp/ale#2ii-fixing