技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

sfdx プラグインを作成してみる

sfdx のインストール

まずは NodeJs をインストールするところから。
下記リンクの中で、Chocolaty を使ったインストールをしている。

white-azalea.hatenablog.jp

この状態で

> nvm list available

|   CURRENT    |     LTS      |  OLD STABLE  | OLD UNSTABLE |
|--------------|--------------|--------------|--------------|
|    19.9.0    |   18.15.0    |   0.12.18    |   0.11.16    |
|    19.8.1    |   18.14.2    |   0.12.17    |   0.11.15    |
... 中略

This is a partial list. For a complete list, visit https://nodejs.org/en/download/releases

とりあえず LTS 入れておけばいいので
nvm install 18.15.0nvm use 18.15.0 を実行すると

> node --version
v18.15.0

そしたら npm update -g npm と打って、NPM を最新化しておこう。
SFDX のインストールはこんな感じ。アップデートも同じコマンドでよい。

上でも下でも好きな方を使えば良い。
尚、下のコマンドをインストールすると、sf がコマンドインターフェースになる。

> npm install sfdx-cli --global
> npm install @salesforce/cli --global

ここでは sf コマンド前提で話を進める。

sfdx プラグインジェネレータのインストール

github.com

この辺からジェネレータをインストールする。
依存するパッケージから

  • npm install -g yarn
  • npm install -g typescript

そしたらジェネレータのインストール(ここから GitBash)

  • sf plugins install @salesforce/plugin-dev

これでプラグイン作成の準備ができた。

最初のプラグイン

sf dev generate plugin コマンドを打つと、プラグインのテンプレートが作成される。

$ sf dev generate plugin

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │    Time to build an sf   │
   `---------´   │   plugin! Version 0.7.0
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? Are you building a plugin for an internal Salesforce team? No
? Enter the name of your new plugin: hello-plugin
? Enter a robust description for your plugin: hello world
? Enter the author of the plugin: azalea
? Select the % code coverage do you want to enforce: 0%
Cloning into 'D:\OneDrive\workspace\sandbox\sfdxMasterSetupPlugin\Example\hello-plugin'...
Initialized empty Git repository in D:/OneDrive/workspace/sandbox/sfdxMasterSetupPlugin/Example/hello-plugin/.git/
    force hello-plugin\.nycrc
    force hello-plugin\package.json

Changes to package.json were detected.

Running yarn install for you to install the required dependencies.
yarn install v1.22.19
[1/5] Validating package.json...
[2/5] Resolving packages...
[3/5] Fetching packages...
[4/5] Linking dependencies...
warning " > ts-node@10.9.1" has unmet peer dependency "@types/node@*".
warning "@salesforce/dev-scripts > typedoc@0.22.18" has incorrect peer dependency "typescript@4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x || 4.7.x".
warning "oclif > yeoman-environment@3.12.1" has unmet peer dependency "mem-fs@^1.2.0 || ^2.0.0".
warning "oclif > yeoman-environment@3.12.1" has unmet peer dependency "mem-fs-editor@^8.1.2 || ^9.0.0".
[5/5] Building fresh packages...
$ yarn husky install
yarn run v1.22.19
... (中略)

すると、hello-plugin のテンプレートが作成された。
早速使ってみる

$ cd hello-plugin/
$ bin/dev hello world
Hello World at Tue Apr 11 2023.
$ bin/dev hello world --name Astro
Hello Astro at Tue Apr 11 2023.
$ bin/dev hello world --help
Say hello.

USAGE
  $ sf hello world [--json] [-n <value>]

FLAGS
  -n, --name=<value>  [default: World] The name of the person you'd like to say hello to.

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Say hello.

  Say hello either to the world or someone you know.

EXAMPLES
  Say hello to the world:

    $ sf hello world

  Say hello to someone you know:

    $ sf hello world --name Astro

いい感じだ。
ソースを眺めるとこんな感じ

ソースを見てみると

  • messages/hello.world.md
# summary

Say hello.

# description

Say hello either to the world or someone you know.

# flags.name.summary

The name of the person you'd like to say hello to.

# examples

- Say hello to the world:

  <%= config.bin %> <%= command.id %>

- Say hello to someone you know:

  <%= config.bin %> <%= command.id %> --name Astro

# info.hello

Hello %s at %s.

マークダウンのトップレベルセクションがリソース名かな。

  • src/commands/hello/world.ts
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.load('hello-plugin', 'hello.world', [
  'summary',
  'description',
  'examples',
  'flags.name.summary',
  'info.hello',
]);

export type HelloWorldResult = {
  name: string;
  time: string;
};

export default class World extends SfCommand<HelloWorldResult> {
  public static readonly summary = messages.getMessage('summary');
  public static readonly description = messages.getMessage('description');
  public static readonly examples = messages.getMessages('examples');

  public static flags = {
    name: Flags.string({
      char: 'n',
      summary: messages.getMessage('flags.name.summary'),
      default: 'World',
    }),
  };

  public async run(): Promise<HelloWorldResult> {
    const { flags } = await this.parse(World);
    const time = new Date().toDateString();
    this.log(messages.getMessage('info.hello', [flags.name, time]));
    return {
      name: flags.name,
      time,
    };
  }
}

上から順に見ていこう。
まずは単純にリソースの読み込み。

Messages.importMessagesDirectory(__dirname);
const messages = Messages.load('hello-plugin', 'hello.world', [
  'summary',
  'description',
  'examples',
  'flags.name.summary',
  'info.hello',
]);

hello-pluginプラグインの名前で、hello.world はリソースファイル名かな。
最後の配列で読み込むリソースを指定しているようだ。

  public static readonly summary = messages.getMessage('summary');
  public static readonly description = messages.getMessage('description');
  public static readonly examples = messages.getMessages('examples');

多分名称が予約されている定数。
ヘルプ表示時に利用される文言らしい。

  public static flags = {
    name: Flags.string({
      char: 'n',
      summary: messages.getMessage('flags.name.summary'),
      default: 'World',
    }),
  };

引数定義だね。
name 引数を、略称 n で、説明は flags.name.summary リソース参照。デフォルトは「World」と読める。
思ったよりシンプルな引数構成だ。

  public async run(): Promise<HelloWorldResult> {
    const { flags } = await this.parse(World);
    const time = new Date().toDateString();
    this.log(messages.getMessage('info.hello', [flags.name, time]));
    return {
      name: flags.name,
      time,
    };
  }

run というくらいだし、プラグインのエントリポイントだと思われる。
1 コマンド 1 機能と考えればコレが妥当なのかもしれませんね。

フラグをパースして、ログに吐き出すだけと

ちょっといじって反応を見よう

まずはリソースをイジってみる

# summary

hello というスクリプト。  
マークダウンで書けるというのも興味深い。

# description

`hello world` か、任意の人名で応答するスクリプトです。

# flags.name.summary

name フラグに任意の名称を設定して、hello 表示しませう。

# examples

- hello world を出力します:

  <%= config.bin %> <%= command.id %>

- 任意の人名で hello メッセージを出力します:

  <%= config.bin %> <%= command.id %> --name Astro

# info.hello

Hello %s (at %s )

するとこんな感じのヘルプとなった。

$ bin/dev hello world --help
hello というスクリプト。  

USAGE
  $ sf hello world [--json] [-n <value>]

FLAGS
  -n, --name=<value>  [default: World] name フラグに任意の名称を設定して、hello 表示しませう。

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  hello というスクリプト。
  マークダウンで書けるというのも興味深い。

  `hello world` か、任意の人名で応答するスクリプトです。

EXAMPLES
  hello world を出力します:

    $ sf hello world

  任意の人名で hello メッセージを出力します:

    $ sf hello world --name Astro
  • summary には1行しか適用されない縛りみたいなものがありそうだ。
  • FLAGS はおそらくソースと組み合わせでの出力だろう。
  • GLOBAL FLAGS は全プラグイン標準搭載と思われる。
  • EXAMPLES はリソースの examples をテンプレートに動作してるようだ。

次はソースもイジってみる。

import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.load('hello-plugin', 'hello.world', [
  'summary',
  'description',
  'examples',
  'flags.name.summary',
  'info.hello',
]);

export type HelloWorldResult = {
  who: string;
  time: string;
};

export default class World extends SfCommand<HelloWorldResult> {
  public static readonly summary = messages.getMessage('summary');
  public static readonly description = messages.getMessage('description');
  public static readonly examples = messages.getMessages('examples');

  public static flags = {
    who: Flags.string({
      char: 'w',
      aliases: ['n', 'name'],
      summary: messages.getMessage('flags.name.summary'),
      default: 'World',
    }),
  };

  public async run(): Promise<HelloWorldResult> {
    const { flags } = await this.parse(World);
    const time = new Date().toDateString();
    this.log(messages.getMessage('info.hello', [flags.who, time]));
    return {
      who: flags.who,
      time,
    };
  }
}

やったことは name 引数を who に書き換えたものだ。
ヘルプを見ると

USAGE
  $ sf hello world [--json] [-w <value>]

FLAGS
  -w, --who=<value>  [default: World] name フラグに任意の名称を設定して、hello 表示しませう。

思った通り、定義によって生成されているらしい。
aliases 定義は、FragDefinition を追跡してみたらこんな定義を見つけたので入れてみた。

export type FlagProps = {
    name: string;
    char?: AlphabetLowercase | AlphabetUppercase;
    /**
     * A short summary of flag usage to show in the flag list.
     * If not provided, description will be used.
     */
    summary?: string;
    /**
     * A description of flag usage. If summary is provided, the description
     * is assumed to be a longer description and will be shown in a separate
     * section within help.
     */
    description?: string;
    /**
     * The flag label to show in help. Defaults to "[-<char>] --<name>" where -<char> is
     * only displayed if the char is defined.
     */
    helpLabel?: string;
    /**
     * Shows this flag in a separate list in the help.
     */
    helpGroup?: string;
    /**
     * Accept an environment variable as input
     */
    env?: string;
    /**
     * If true, the flag will not be shown in the help.
     */
    hidden?: boolean;
    /**
     * If true, the flag will be required.
     */
    required?: boolean;
    /**
     * List of flags that this flag depends on.
     */
    dependsOn?: string[];
    /**
     * List of flags that cannot be used with this flag.
     */
    exclusive?: string[];
    /**
     * Exactly one of these flags must be provided.
     */
    exactlyOne?: string[];
    /**
     * Define complex relationships between flags.
     */
    relationships?: Relationship[];
    /**
     * Make the flag as deprecated.
     */
    deprecated?: true | Deprecation;
    /**
     * Alternate names that can be used for this flag.
     */
    aliases?: string[];
    /**
     * Emit deprecation warning when a flag alias is provided
     */
    deprecateAliases?: boolean;
    /**
     * Delimiter to separate the values for a multiple value flag.
     * Only respected if multiple is set to true. Default behavior is to
     * separate on spaces.
     */
    delimiter?: ',';
};

試しに実行してみると

$ bin/dev hello world --who HAHAHA
Hello HAHAHA (at Tue Apr 11 2023 )

$ bin/dev hello world --n HAHAHA
Hello HAHAHA (at Tue Apr 11 2023

思った通りに動いてくれるようだ。

プラグインをインストールしてみる

sf plugins link . コマンドを実行すると、hello-plugin を sfdx コマンドとしてインストールし始める。

$ sf plugins link .
@salesforce/cli: linking plugin hello-plugin... - [3/5] Fetching packages...
warning " > ts-node@10.9.1" has unmet peer dependency "@types/node@*".
warning "@salesforce/dev-scripts > typedoc@0.22.18" has incorrect peer dependency "typescript@4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x || 4.7.x".
warning "oclif > yeoman-environment@3.12.1" has unmet peer dependency "mem-fs@^1.2.0 || ^2.0.0".
@salesforce/cli: linking plugin hello-plugin... done

実際に動かしてみるか。

$ sf hello world --name Astro
Hello Astro (at Tue Apr 11 2023

因みにリンクしてるとはパスが通ってるだけっぽいので、

# info.hello

Hello Mr, %s (at %s )

こんな修正してやると

$ sf hello world --name Astro
Hello Mr, Astro (at Tue Apr 11 2023

書き換わった。
リンクの解除は sf plugins unlink . できるようだ。

コマンドの追加

手動で追加もできそうではあるけど、コマンド生成のジェネレータもあるらしい。

sf dev generate command --name call:external:service

参考

github.com

github.com

github.com

github.com