【テクニカル・上級編】Sketchカスタムプラグイン開発の第一歩:JavaScriptで自分だけの作業効率化ツールを作ろう – UI/UX・デザインツール活用バイブル

GUIの限界を超え、キャンバスをコードで支配する:Sketchカスタムプラグイン開発の深淵

Figma全盛の現代においてなお、macOSネイティブアプリとして圧倒的な描画パフォーマンスと、ローカルファイルシステムへの緊密な統合を誇るSketch。その真のポテンシャルを引き出しているデザイナーやエンジニアが、世界にどれだけいるだろうか。

既存のプラグインをインストールして満足しているようでは、一線級のプロダクトデリバリーにおいて「究極の効率化」は望めない。デザインシステムが巨大化し、トークンの同期やアセットの書き出し、レイアウトの検証が数千枚のアートボード規模に達したとき、GUIの操作はボトルネックへと変貌する。

我々が目指すべきは、「キャンバスをコードで支配し、デザインと開発のパイプラインを完全に自動化すること」である。

本稿では、Sketchの内部アーキテクチャであるCocoaScript(COScript)の深淵から、JavaScript APIを用いた高速なプラグイン開発、さらにはCLIツール(`sketchtool`)を用いたCI/CDパイプラインへの統合まで、一切の妥協を排して解説する。

—

1. Sketchプラグインアーキテクチャの深淵:JS APIとCocoaBridgeの真実

Sketchのプラグイン環境を理解する上で、最も重要な概念が「CocoaScript (COScript)」である。

SketchのコアはObjective-CおよびSwiftで構築されたmacOSネイティブアプリケーションだ。プラグインが動作する際、JavaScriptエンジン(JavaScriptCore)とmacOSのネイティブランタイム(Cocoa)を繋ぐ超高速なブリッジとしてCOScriptが機能する。

+————————————————————-+
| Sketch Plugin (JS) |
+————————————————————-+
│ (JavaScriptCore)
▼
+————————————————————-+
| CocoaScript Bridge / Sketch JS API |
+————————————————————-+
│ (COScript / Objective-C Runtime)
▼
+————————————————————-+
| macOS AppKit / Sketch Native Core |
| (MSDocument, MSLayerGroup, MSColor, etc.) |
+————————————————————-+

このアーキテクチャが意味するのは、提供されている高レベルなJavaScript API(`@skpm`など)の裏側で、macOSのAppKitフレームワークやSketch自体の内部プライベートクラス(`MS`で始まるクラス群)に直接アクセスできるという事実だ。

JavaScript APIとネイティブAPIの使い分け

  • 高レベルJavaScript API: `sketch/dom` や `sketch/ui` など。クリーンで可読性が高く、APIの変更に対して堅牢。
  • 低レベルCocoaScript (Native API): `context.api()` や `MSDocument.currentDocument()` を経由して、Objective-Cのオブジェクトを直接操作する。APIドキュメントにない未公開機能や、極限のパフォーマンスが求められるバッチ処理で使用する。

—

2. プロフェッショナル開発環境の完全自動構成

モダンなプラグイン開発において、グローバル環境を汚染するその場しのぎのスクリプトは不要だ。TypeScript、Webpack、そしてホットリロードを統合した、堅牢なボイラープレートを構築する。

Sketch公式の開発ツールチェーンである `skpm` (Sketch Plugin Manager) をベースに、静的型付けと最適化コンパイルを導入しよう。

2.1. プロジェクトの初期化と依存関係の定義

まずはプロジェクトディレクトリを作成し、必要なパッケージをインストールする。

mkdir sketch-pipeline-optimizer
cd sketch-pipeline-optimizer
npm init -y
npm install –save-dev skpm typescript @types/sketch @types/mocha webpack

2.2. `package.json` の構成

プラグインのメタデータとビルドターゲットを設定する。`manifest` フィールドは、Sketchがプラグインを認識するための重要なエントリーポイントである。

{
“name”: “sketch-pipeline-optimizer”,
“version”: “1.0.0”,
“description”: “Design System Linter and Optimizer”,
“engines”: {
“sketch”: “>=80.0”
},
“skpm”: {
“name”: “Pipeline Optimizer”,
“manifest”: “src/manifest.json”,
“main”: “dist/plugin.sketchplugin”
},
“scripts”: {
“build”: “skpm-build”,
“watch”: “skpm-build –watch –run”,
“publish”: “skpm-publish”
},
“devDependencies”: {
“@skpm/builder”: “^0.7.0”,
“typescript”: “^5.0.0”
}
}

2.3. `src/manifest.json` の定義

プラグインのメニュー階層と、実行されるJavaScript関数をマッピングする。

{
“$schema”: “https://raw.githubusercontent.com/skpm/skpm/master/development/manifest.schema.json”,
“name”: “Pipeline Optimizer”,
“identifier”: “com.architecture.pipeline.optimizer”,
“version”: “1.0.0”,
“commands”: [
{
“name”: “Audit & Fix Unlinked Colors”,
“identifier”: “audit-unlinked-colors”,
“script”: “./commands/audit-colors.js”
}
],
“menu”: {
“title”: “Pipeline Optimizer”,
“items”: [
“audit-unlinked-colors”
]
}
}

—

3. 実践:メモリ効率を極限まで高めたカスタムプラグインの実装

ここでは、実戦的なユースケースを想定したプラグインを実装する。

【課題】
デザインシステムが定義する「カラー変数(Color Variables)」にリンクされていない、「野良カラー(Unlinked Colors)」が数千枚のレイヤーに散らばっている。これらを検出し、最も近いシステムカラーへ自動的に一括置換(スナップ)する。

数万個のオブジェクトをメモリリークを起こさずに高速走査するため、JavaScript APIのラッパーを介さず、Objective-Cのランタイムにブリッジする低レイヤAPIを併用して実装する。

`src/commands/audit-colors.ts` の実装

import sketch from ‘sketch’;

// macOSのネイティブクラス群への参照を取得(Cocoa Bridge)
const { Document, Settings } = sketch;

interface ColorMap {
[hex: string]: string; // 未リンクカラー -> システムカラーID
}

/

  • 2つのHEXカラーのユークリッド距離(近似RGB空間)を計算し、類似度を判定する

/
function getDistance(hex1: string, hex2: string): number {
const r1 = parseInt(hex1.substring(1, 3), 16);
const g1 = parseInt(hex1.substring(3, 5), 16);
const b1 = parseInt(hex1.substring(5, 7), 16);

const r2 = parseInt(hex2.substring(1, 3), 16);
const g2 = parseInt(hex2.substring(3, 5), 16);
const b2 = parseInt(hex2.substring(5, 7), 16);

return Math.sqrt(Math.pow(r1 – r2, 2) + Math.pow(g1 – g2, 2) + Math.pow(b1 – b2, 2));
}

export default function(context: any) {
const doc = Document.getSelectedDocument();
if (!doc) {
sketch.UI.message(“❌ エラー: ドキュメントが開かれていません。”);
return;
}

// 1. システム定義の共有カラー(Color Variables)をインデックス化
const documentData = doc.sketchObject.documentData(); // Native MSColorCreator / MSSharedStyleController
const sharedSwatches = documentData.sharedSwatches().swatches();

if (sharedSwatches.count() === 0) {
sketch.UI.message(“⚠️ 警告: ドキュメント内にColor Swatch(カラー変数)が定義されていません。”);
return;
}

sketch.UI.message(“🔍 デザインシステムのカラー変数をスキャン中…”);

// Swatchの情報をキャッシュ
const systemColors: { id: string; hex: string; name: string }[] = [];
for (let i = 0; i < sharedSwatches.count(); i++) { const swatch = sharedSwatches.objectAtIndex(i); const color = swatch.color(); const hex = `#${color.immutableModelObject().hexValue()}`; systemColors.push({ id: swatch.uuid(), hex: hex.toUpperCase(), name: swatch.name() }); } // 2. メモリ効率を考慮した、深さ優先探索(DFS)によるレイヤーの高速走査 let auditCount = 0; let fixedCount = 0; const startTime = CFAbsoluteTimeGetCurrent(); // macOSの高精度タイマー // 全ページを走査 doc.pages.forEach(page => {
// 再帰的にレイヤーを走査する内部関数
function processLayer(layer: any) {
auditCount++;

// メモリ消費を抑えるため、必要のないオブジェクトのインスタンス化を避ける
// ネイティブオブジェクトから直接判定を行う
const nativeLayer = layer.sketchObject;

// 塗り(Fills)のチェック
if (nativeLayer.style && nativeLayer.style().fills) {
const fills = nativeLayer.style().fills();
for (let i = 0; i < fills.count(); i++) { const fill = fills.objectAtIndex(i); if (fill.isEnabled() && fill.fillType() === 0) { // 0 = Solid Fill const colorObj = fill.color(); const hex = `#${colorObj.immutableModelObject().hexValue()}`.toUpperCase(); // すでにSwatchにリンクされているか確認 const isLinked = fill.color().swatchID() !== null; if (!isLinked) { // 最も近いシステムカラーを見つける let closestSwatch = systemColors[0]; let minDistance = Infinity; for (const systemColor of systemColors) { const dist = getDistance(hex, systemColor.hex); if (dist < minDistance) { minDistance = dist; closestSwatch = systemColor; } } // 距離が一定の閾値(例: RGB空間で50以内)であれば自動修正 if (minDistance < 50) { // ネイティブのMSColorとMSColorSwatchを結合 const targetSwatch = documentData.sharedSwatches().swatchWithID(closestSwatch.id); if (targetSwatch) { // レイヤーのスタイルにSwatch(カラー変数)を適用 fill.setColor(targetSwatch.color()); fill.color().setSwatchID(closestSwatch.id); fixedCount++; } } } } } } // 子要素の走査(グループまたはアートボードの場合) if (layer.layers && layer.layers.length > 0) {
layer.layers.forEach((child: any) => processLayer(child));
}
}

page.layers.forEach(layer => processLayer(layer));
});

const endTime = CFAbsoluteTimeGetCurrent();
const elapsedTime = (endTime – startTime).toFixed(3);

// 3. 結果の可視化
sketch.UI.alert(
“監査・自動修復完了”,
`【実行結果】\n・走査レイヤー数: ${auditCount}\n・修復済みカラー: ${fixedCount}\n・処理時間: ${elapsedTime} 秒\n\nデザインシステムに準拠していない「野良カラー」を検出し、最も近似するカラー変数へ安全に再マッピングしました。`
);
}

アーキテクチャの解説:なぜこのコードは高速なのか?

1. CFAbsoluteTimeGetCurrent() の採用: JavaScriptの `Date.now()` ではなく、CoreFoundationのネイティブタイマーを叩くことで、マイクロ秒精度の正確なベンチマークを可能にしている。
2. JS DOMラッパーの回避: `sketch.fromNative(layer)` のような高レベルなラッパー生成は、ループ内では膨大なオーバーヘッドとなる。このスクリプトでは `layer.sketchObject` を取得し、Objective-Cのネイティブメソッド(`style().fills()` や `swatches()`)を直接叩いている。これにより、メモリのガベージコレクション(GC)の発生頻度を抑え、数万オブジェクトの走査時におけるメモリリークを皆無にしている。

—

4. デバッグの極意:Safari Web Inspectorによるリアルタイムプロファイリング

Sketchプラグイン開発における最大の罠は、「デバッグのしづらさ」にある。`console.log` だけに頼るデバッグからは即刻卒業しなければならない。

Sketchは、内部のJS実行環境(JavaScriptCore)をSafariのウェブインスペクタ(Web Inspector)に公開している。

段階的デバッグ手順

1. 開発モードの有効化:
macOSのターミナルを開き、Sketchのデバッグフラグを有効にする。

defaults write com.bohemiancoding.sketch3 AlwaysDumpJSStack -bool true
defaults write com.bohemiancoding.sketch3 WebKitDeveloperExtras -bool true

2. Safariとの接続:

  • Safariを開き、環境設定 > 詳細 > 「メニューバーに”開発”メニューを表示」を有効化。
  • Sketchを起動し、作成したプラグインコマンドを実行。
  • Safariのメニューから「開発」>「[お使いのMac名]」>「Sketch」または「Plugin: Pipeline Optimizer」を選択。

3. ブレークポイントとヒープスナップショット:
これでSafariの強力な開発者ツールが立ち上がる。JSソースコードに `debugger;` 文を挿入しておけば、実行時にそのラインで実行を一時停止させ、ローカル変数のスコープやコールスタック、Objective-Cオブジェクトの内部構造(`cocoaObject`)を完全にインスペクトできる。

—

5. ヘッドレス自動化:`sketchtool` CLIとCI/CDパイプラインの完全融合

真のプロダクトエンジニアリングは、ローカルのGUIだけで完結しない。デザイナーがコミット(保存)した `.sketch` ファイルをCI/CD(GitHub Actions等)で自動検証し、エンジニアが即座に利用できるアセットとしてエクスポートするパイプラインを構築する。

Sketchには、標準で `sketchtool` という強力なCLIが同梱されている。これをヘッドレスサーバー(macOSランナー)上で実行することで、完全な自動化が実現する。

5.1. CLIからのプラグイン実行とアセットエクスポート

`sketchtool` を使用すると、SketchアプリをGUI起動することなく、ヘッドレスモードでファイルを操作できる。

Sketchアプリ内に内包されているsketchtoolへのパスを通す
export PATH=”/Applications/Sketch.app/Contents/Resources/sketchtool/bin:$PATH”

1. ドキュメントからすべてのアートボードをPNGとしてエクスポートする
sketchtool export artboards “DesignSystem.sketch” –output=”./exported-assets” –formats=”png” –scales=”1, 2″

2. メタデータの抽出(JSON形式)
sketchtool metadata “DesignSystem.sketch” > metadata.json

5.2. GitHub Actionsへの統合:デザインシステムの自動バリデーション

以下は、デザイナーが `main` ブランチに `.sketch` ファイルをプッシュした際、自動的にヘッドレスでSketchを起動し、カラーパレットの監査を実行して、未リンクカラーが見つかった場合にプルリクエストをブロックするCIワークフローの定義である。

name: Design System CI / Guard-Rail

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
validate-design:
# sketchtoolはmacOSバイナリであるため、macOSランナーが必須
runs-on: macos-latest

steps:

  • name: Checkout Code

uses: actions/checkout@v3

  • name: Set up Node.js

uses: actions/setup-node@v3
with:
node-size: 18

  • name: Install Dependencies

run: npm ci

  • name: Locate and Symlink sketchtool

run: |
# GitHub Hosted Runner上のSketchを探索、またはインストール
# ※ 実際の実装では、Runner環境にSketchを事前にインストールしておくか、
# キャッシュされたSketch.appを使用します。
sudo hdiutil attach Sketch.dmg
sudo cp -R /Volumes/Sketch/Sketch.app /Applications/
export PATH=”/Applications/Sketch.app/Contents/Resources/sketchtool/bin:$PATH”
echo “SKETCHTOOL_PATH=$PATH” >> $GITHUB_ENV

  • name: Run Headless Audit Script

run: |
# sketchtoolのrunコマンドを使い、GUIなしで特定のJSプラグインを実行
# 終了ステータス(Exit Code)でCIの成否を判定する
sketchtool run dist/plugin.sketchplugin audit-unlinked-colors “DesignSystem.sketch” –no-gui

—

6. エピローグ:コードでデザインを彫刻するということ

デザインとエンジニアリングの間に横たわる溝は、単に「対話」を増やすだけでは埋まらない。それはシステムとしてのインターフェースの不一致に起因するものだからだ。

Sketchのプラグイン開発や `sketchtool` による自動化は、デザインデータを単なる「グラフィックの集合」から「クエリ可能な構造化データ」へと昇華させる。

我々がコードによってキャンバスを彫刻し、ルールを厳格に適用するとき、デザイナーの意図は一寸の狂いもなく本番のコードへと同期される。このパイプラインを自らの手で構築することこそが、真のデザインシステムアーキテクトに求められる資質なのだ。

さあ、今すぐエディタを開き、最初の一行を書き換えよう。キャンバスの支配権は、あなたの手の中にある。

タイトルとURLをコピーしました