Sketch Assistants で DesignOps をアシスト

この記事は [Goodpatch Advent Calendar 2020](https://qiita.com/advent-calendar/2020/goodpatch) の15日目です。6日目の記事 [Abstract API を使った CI の構成を考える](https://blog.kakeragames.com/2020/12/06/ci-with-abstract-api.html) に関連し、[Sketch Assistants](https://developer.sketch.com/assistants/) を DesignOps に着目して紹介します。

なお、この記事の内容は [Sketch Version 70.2](https://www.sketch.com/updates/#version-70.2) で動作確認しています。

# DesignOps
はじめに「DesignOps」の定義を確認しましょう。[DesignOpsの基礎 – U-Site](https://u-site.jp/alertbox/design-operations-101) では、次のように定義されています。

> **定義：DesignOpsとは、スケールしたときのデザインの価値と影響を増大させるために、人材とプロセス、プロダクトを組織化して、最適化することである。**
> 
> DesignOpsは、以下のような課題に対する取り組みの総称である：
>    - デザインチームの成長と進化
>    - 適切なスキルをもつ人材の発掘と採用
>    - 効率的なワークフローの作成
>    - デザインアウトプットの品質と影響の改善

「DesignOps」には、私たちが普段慣れ親しんでいる「DevOps [^1] 」的なアプローチが背景にあります。上記の「効率的なワークフローの作成」という課題については、DevOps のプラクティスの「継続的インテグレーション（CI）」を適用できそうです。

UI デザインツールとして広く利用されている [Sketch](https://www.sketch.com/) には [Assistants](https://developer.sketch.com/assistants/) という仕組み（機能）が提供されています。Assistants を使うことで、いわゆる Linter として Sketch ファイルを静的解析できます。後述する [sketch-assistant-cli](https://github.com/sketch-hq/sketch-assistants/tree/main/packages/cli) も提供されており、CI による静的解析の自動化も実現可能だと考えています。

# Sketch Assistants
Sketch Assistants は、[Sketch — Introducing Assistants — a helping hand for your designs](https://www.sketch.com/blog/2020/08/11/introducing-assistants/) で紹介された Sketch の機能の一つで、次の特徴があります。

- ドキュメントの問題点を発見し、デザインシステムとの整合性を保ち、共同作業のためのファイルを準備するのに役立つ
- Sketch Mac app の汎用拡張機能である「プラグイン」とは異なり、Sketch内でも、どのプラットフォームでもコマンドラインでも動作する
- 既存の Assistants を修正するか、TypeScript で独自のルール実装を書くことで、独自の Assistants を作成できる

Sketch Assistants には、[Naming Conventions](https://www.sketch.com/extensions/assistants/@sketch-hq/sketch-naming-conventions-assistant/) や [Tidy](https://www.sketch.com/extensions/assistants/@sketch-hq/sketch-tidy-assistant/) などの Assistant が用意されています。これらを Sketch に導入することで「空の Group 」「非表示の Layer 」など、人力では見つけにくい箇所を、機械的に発見できるようになります。既存の Assistants は [Sketch — Assistants (extensions)](https://www.sketch.com/extensions/assistants/) で探せます。

## Sketch Assistant を使う
Assistants の使い方を詳しく知りたい場合は、[Sketch — Assistants (docs)](https://www.sketch.com/docs/assistants/) を参照してください。

<img width="488" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/d97b1f9f-f23c-ee6d-8ec3-f9c8b8eb0bb4.png" >

Toolbar の［Assistants］をクリックすると、次のウインドウが表示されます。

<img width="513" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/b0df8b16-76c4-899c-ef6b-763b191d7420.png" >

［Find Assistants］をクリックすると、https://www.sketch.com/extensions/assistants/ が表示されます。

![Screenshot 2020-12-14 22.37.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/de5de4b1-776a-2ba2-b29a-916b6550b103.png)

今回は [Sketch — Tidy](https://www.sketch.com/extensions/assistants/@sketch-hq/sketch-tidy-assistant/)をインストールします。インストールするには［Add to Sketch］をクリックします。

<img width="1128" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/d3bbf2de-bfee-4d09-2109-5055f2c5b75e.png" >

[Tidy](https://www.sketch.com/extensions/assistants/@sketch-hq/sketch-tidy-assistant/)がインストールされると、Tidyの実行結果が Assistants ウインドウに表示されます。警告された項目をクリックすると、該当箇所が表示されます。

<img width="673" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/f83d059a-2221-d54b-28f9-0713be788acf.png" >


## Sketch Assistant を作る
公式ドキュメント [Sketch Developer — Getting started](https://developer.sketch.com/assistants/getting-started) です。

自作 Assistant のパターンとしては大きつ二つあります。

1. Sketch が提供している [Core Rules](https://github.com/sketch-hq/sketch-assistants/tree/main/assistants/core) を参照し、ルールのオプションを指定して、ルールの条件を調整する（普段使用している Linter のルールを適用する感覚と同じです）
2. 自作のロジックを実装する

## 開発の流れ
開発環境として Node.js version 12+ が必要になります。GitHub に [Sketch Assistantテンプレートのリポジトリ](https://github.com/sketch-hq/sketch-assistant-template) が用意されていますので、このテンプレートを使って開発を始めます。

1. `npm install` でパッケージをインストール
2. ルールとそのテストを実装
3. テストで参照する Sketch ファイルを作成
4. `npm run test` でテストを実行
5. `npm run package-tarball` でビルドして、パッケージ化する（tgzファイルが生成される）
6. tgz ファイルを Sketch Assistants にインストールして動作確認

### 注意点
- ルール名は Assistant のエコシステムで一意の識別子となるため、ルール名の前には親の Assistant名を `/` で区切って付けます
    - 例: `my-sketch-assistant/text-disallow`
- できるだけ設定可能なルールを実装しましょう
    - 例えば「lorem ipsum」文字列を探すルールの場合、「lorem ipsum」をハードコードするのではなく、オプションとして受け取るようにしましょう。一般化することで、設定が可能になり、公開された後のAssistant は Sketchユーザーにとってより広く有用なものになります。
    - オプションは `utils.getOption` を使用して参照できます
- デフォルトでは、ルールがアクティブではありません
    - Assistant の config オブジェクトで明示的に有効にする必要があります

# 自作のロジックを実装するパターン
テキストレイヤーの文字列に「lorem-ipsum」を含む場合に警告するルールを実装してみます。なお、このルールのプロジェクトは [mnkd/first-sketch-assistant](https://github.com/mnkd/first-sketch-assistant)で公開しています。

最初に [Sketch Assistant Template](https://github.com/sketch-hq/sketch-assistant-template)を基にしてrepoを用意して、実装を進めます。以下は、`index.ts` の抜粋です。

- `utils.getOption()` で option `pattern` を取得しています
- `for (const text of utils.objects.text) {}` で、ドキュメント内のすべてのテキストレイヤーに対して、評価しています
    - `text` の他 `page` `artboard` `group` `color` などの情報を取得できます
- 評価した結果 `pattern` を含む場合、`utils.report()`でレポートします

```ts:index.ts
import { AssistantPackage, RuleDefinition } from '@sketch-hq/sketch-assistant-types'

const textDisallow: RuleDefinition = {
  rule: async (context) => {
    const { utils } = context

    const pattern = utils.getOption('pattern')
    if (typeof pattern !== 'string') throw Error()
  
    for (const layer of utils.objects.text) {
      const value = layer.attributedString.string
      if (value.includes(pattern)) {
        utils.report(`Layer "${layer.name}" contains "${pattern}"`, layer)
      }
    }
  },
  name: 'first-sketch-assistant/text-disallow',
  title: (config) => `Text should not contain "${config.pattern}"`,
  description: 'Reports a violation when text layers contain a configurable text pattern',
}

const assistant: AssistantPackage = async () => {
  return {
    name: 'first-sketch-assistant',
    rules: [textDisallow],
    config: {
      rules: {
        'first-sketch-assistant/text-disallow': { 
          active: true,
          pattern: 'lorem-ipsum',
        },
      },
    },
  }
}
```

### パッケージ化
作成したルールは `npm run package-tarball` でビルドしてパッケージ化します。パッケージ化に成功すると、tgzファイルが生成されます。

tgzファイルは［Assistants］ウインドウの［Manage Assistants…］からインストールできます。

<img width="588" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/94e3dff4-492b-d7b5-bda0-24a873defdc0.png" >

［Manage Assistants...］をクリックすると、次の画面になりますので、さらに［Add from Archive…］をクリックしてください。クリックするとファイルを選択するウインドウが表示されますので、tgzファイルを選んでください。
<img width="475" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/c15d292e-bfa4-d52e-b7d8-687187b02b95.png" >

tgzファイルを選択すると、Assistant が実行され、次のように表示されます。
<img width="513" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49300/ba1f67f4-d856-8ff4-61cf-cb41344d1aa7.png" >


## テスト駆動の Assistant 開発
[Sketch Developer — Run and test Assistants](https://developer.sketch.com/assistants/run-and-test) で「Test-driven Assistant development」と記載されているとおり、テストしやすく整備されています。また、[Core Rules](https://github.com/sketch-hq/sketch-assistants/tree/main/assistants/core) の各ルールのコードからテストの実装方法を確認できます。

最初に、各テストで参照する Sketch ファイルを用意します。ベストプラクティスとしては、テスト単位で Sketch ファイルを用意しておくとよいでしょう。

例えば、`first-sketch-assistant/text-font-name` をテストしたい場合は、次のようにテストコードを記述します。

```ts:src/__tests__/index.test.ts
import { resolve } from 'path'
import { testAssistant } from '@sketch-hq/sketch-assistant-utils'

import Assistant from '..'

test('text-font-name', async () => {
  const { violations, ruleErrors } = await testAssistant(
    resolve(__dirname, './font-name.sketch'),
    Assistant,
  )

  const textFontNameViolations = violations.filter(
    (v) => v.ruleName === 'first-sketch-assistant/text-font-name'
  )

  expect(textFontNameViolations).toHaveLength(1)
  expect(ruleErrors).toHaveLength(0)
})
```

次にテストを実行して、期待どおりの結果が得られるかチェックします。

```
$ npm run test

> first-sketch-assistant@1.0.0 test /path/to/first-sketch-assistant
> jest --no-cache

 PASS  src/__tests__/index.test.ts
  ✓ text-font-name (22 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.037 s
Ran all test suites.
```

# Core Rules を活用するパターン
Core Rules は [sketch-assistants/assistants/core](https://github.com/sketch-hq/sketch-assistants/tree/main/assistants/core) にリストアップされています。この中から、適用したいルールを選択します。

ルールを実装するには、はじめに `sketch-hq/sketch-core-assistant` を追加します。

```console
$ npm install -S @sketch-hq/sketch-core-assistant
```

`$ npm run package-tarball` した際に、次のようなエラーが発生する場合は、`package.json` の `dependencies` の `@sketch-hq/sketch-assistant-utils` や `@sketch-hq/sketch-assistant-types` `@sketch-hq/sketch-core-assistant` のバージョンを揃えるなど、対応してみてください。

```
  node_modules/@sketch-hq/sketch-file-format-ts/dist/cjs/v3-types.d.ts:477:5
    477     changeIdentifier: number;
```


例えば、次のルールを適用したい場合は、下記 `index.ts` のように記述します。

- [layers-no-hidden]()
    - レイヤーリストで視覚的に非表示になっているレイヤーは違反とみなされます
- [groups-no-redundant](https://github.com/sketch-hq/sketch-assistants/tree/main/assistants/core/src/rules/groups-no-redundant)
- [layers-no-loose](https://github.com/sketch-hq/sketch-assistants/tree/main/assistants/core/src/rules/layers-no-loose)
    - アートボードの外側に存在するレイヤーは違反とみなされます
- [name-pattern-artboards](https://github.com/sketch-hq/sketch-assistants/tree/main/assistants/core/src/rules/name-pattern-artboards)
    - Artboard 名に「Copy」を含む場合に指摘する
    - `forbidden` option に正規表現で指摘できる


```ts:index.ts
import CoreAssistant from '@sketch-hq/sketch-core-assistant'
import { AssistantPackage } from '@sketch-hq/sketch-assistant-types'

const assistant: AssistantPackage = [
  CoreAssistant,
  async () => {
    return {
      name: 'my-rules',
      rules: [],
      config: {
        rules: {
          '@sketch-hq/sketch-core-assistant/layers-no-hidden': {
            active: true,
          },
          '@sketch-hq/sketch-core-assistant/groups-no-redundant': {
            active: true,
          },
          '@sketch-hq/sketch-core-assistant/layers-no-loose': {
            active: true,
          },
          '@sketch-hq/sketch-core-assistant/name-pattern-artboards': {
            active: true,
            "allowed": [],
            "forbidden": ['Copy'],
          },
        },
      },
    }
  },
]

export default assistant
```



# Sketch Assistant を CLI から使う
Sketch は [sketch-assistant-cli](https://github.com/sketch-hq/sketch-assistants/tree/main/packages/cli) も提供しています。CLI を使うことで Mac以外の環境でも様々な Assistant を使ったワークフローが可能になり、CI の実現も容易になります。

例えば、先ほどの「Core Rules を活用するパターン」と、同等のルールを定義してみます。

```json:assistant.json
{
    "name": "my-rules",
    "dependencies": {
        "@sketch-hq/sketch-core-assistant": "latest"
    },
    "assistant": {
        "extends": [
            "@sketch-hq/sketch-core-assistant"
        ],
        "config": {
            "rules": {
                "@sketch-hq/sketch-core-assistant/layers-no-hidden": {
                    "active": true
                },
                "@sketch-hq/sketch-core-assistant/groups-no-redundant": {
                    "active": true
                },
                "@sketch-hq/sketch-core-assistant/layers-no-loose": {
                    "active": true
                },
                "@sketch-hq/sketch-core-assistant/name-pattern-artboards": {
                    "active": true,          
                    "allowed": [],
                    "forbidden": ["Copy"]
                }
            }
        }
    }
}
```


`--assistant` に、上記の JSON を指定して `sketch-assistants` を実行すると、次のような実行結果が得られます。

```console
$ sketch-assistants --assistant=assistant.json iOS_02.sketch
✔ iOS_02.sketch: Ready

File: /path/to/iOS_02.sketch

   custom/my-rules

    Grade: fail
    Violations: 10
    Rule errors: 0

    Groups should not be redundant
      Detail: This group is redundant
      Severity: error
      Rule: @sketch-hq/sketch-core-assistant/groups-no-redundant
      Object: Name : Top | Pointer : /document/pages/0/layers/0/layers/1 | Id : 85F36E12-9801-40A5-842A-B97B7AFE2CE2 | Class : 85F36E12-9801-40A5-842A-B97B7AFE2CE2

    Groups should not be redundant
      Detail: This group is redundant
      Severity: error
      Rule: @sketch-hq/sketch-core-assistant/groups-no-redundant
      Object: Name : lv1/M | Pointer : /document/pages/0/layers/0/layers/4/layers/1/layers/0/layers/1 | Id : 754EA6E0-5B96-4BF3-B5AA-B2485BB24312 | Class : 754EA6E0-5B96-4BF3-B5AA-B2485BB24312
      
  ..snip..
```


`--json` を指定することで、JSONとして実行結果を得られます。

```console
$ sketch-assistants --assistant=assistant.json iOS_02.sketch --json
```

JSON であれば、得意のプログラミング言語を使ってパースした結果を基にして、次のアクション（Slackへ通知など）へ移せそうです。


# まとめ
- Design の世界にも「DevOps」に似た「DesignOps」という考え方がある
- 「Sketch Assistant」を使って、Sketchファイルを静的解析できる（Linterを作成できる）
- 「Sketch Assistant CLI」を使うことで CLI を使って Assistant を実行できる

ということが分かりました。

# Next Action
[Abstract API を使った CI の構成を考える](https://blog.kakeragames.com/2020/12/06/ci-with-abstract-api.html) で言及されている「CI による自動化」のパーツが揃いつつあります。Abstract が提供する [Webhooks](https://sdk.abstract.com/docs/webhooks/)が使えるようになると、例えば `commits.created` のイベントをフックにして、Sketch Assistant CLI で静的解析し、その結果を Slack などへ通知し、Sketch ファイルの修正・改善をUIデザイナーに促す、というフローを構築できそうです。構築したあとは、そのフローが有効かどうか評価し、その結果を共有できればと考えています。

# 最後に
現時点では道半ばでありますが、エンジニアとして「DevOps」で培った知見を「DesignOps」に活かすことで、UIデザイナーがデザインに集中できるようアシストし、質の高いデザインを促進する活動に貢献できればと考えています。

明日の [Goodpatch Advent Calendar 2020](https://qiita.com/advent-calendar/2020/goodpatch) は @kazu_death がお送りします。

# 参考
- [Abstract API を使った CI の構成を考える](https://blog.kakeragames.com/2020/12/06/ci-with-abstract-api.html)
- [Sketch — The digital design toolkit](https://www.sketch.com/)
- [Sketch Developer — Assistants](https://developer.sketch.com/assistants/)
- [DesignOpsの基礎 – U-Site](https://u-site.jp/alertbox/design-operations-101)
- [DesignOps Handbook - DesignBetter](https://www.designbetter.co/designops-handbook)
- [DesignOps: From discovery to successful scaling](https://www.abstract.com/books/designops-from-discovery-to-successful-scaling)


[^1]: DevOps については [DevOps とは? - DevOps と AWS](https://aws.amazon.com/jp/devops/what-is-devops/) や [「DevOps」を全く知らない人のために「DevOps」の魅力を伝えるための「DevOps」入門 - Qiita](https://qiita.com/bremen/items/44c3de10413f45f9f41e) などを参照してください