dackdive's blog

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

Dreamforce'19 Developer Keynoteまとめ

ブログ

動画

ブログ記事を読みながら動画をとばしとばし観た感じです。


Lightning Base Componentsがオープンソース

f:id:dackdive:20191121120137p:plain

動画09:30あたり。

先日のLWCのオープンソース化に続き、Lightning Base Componentsという基本コンポーネント群もオープンソース化したそうです。

リポジトリ

最初、リポジトリ名からBase Compoentsそのものでなくこれらを使ったサンプル集でしかないのかな、と思いましたが
このディレクトリ下がBase Componentsにあたりそうですね。 https://github.com/salesforce/base-components-recipes/tree/master/force-app/main/default/lwc


Evergreen: Salesforceでサーバーレスを実現する新機能

f:id:dackdive:20191121123751p:plain 動画10:15あたり。

かなりインパクトのある発表だったんじゃないでしょうか。
英語だけでなくほぼ同時に日本語でも紹介ブログが投稿されたあたり、本気度が伺えます。

ざっくり

  • Apex, Java, Node.jsでサーバーサイドの処理が書けるようになる
  • Platform Eventsなどを起点としたイベント駆動
    • (いや、Apexから直接実行もできるとあるので、完全にイベント駆動というわけでもないか)
  • 2月のSpring'20でリリース予定

ということは把握しましたが、まだブログもデモもちゃんと見れてないので別途詳細を書く。


Salesforce API Portal

f:id:dackdive:20191121125543p:plain

動画24:45あたり。

利用可能なAPIが一箇所にまとまったサイトができた、かな。おそらくまだリリースはされてない。

この後のBlockchain関連のところ(33:00あたり)でデモされてます。

f:id:dackdive:20191121190249p:plain

URLが https://**********.for.com/kjcapi/... となっているので、各Salesforce組織内に含まれるイメージ?

Select API Instanceというのはおそらくモックを叩くか実際の組織のデータを扱うかを選択できるってことで、これはAPIドキュメントとしてはかなり便利そう。


Einstein 機能強化

f:id:dackdive:20191121185842p:plain

動画24:45ぐらい。

元々のVision & Languageに加えMulti-Language、OCR、Voiceなどの新機能がリリース。


LWCのローカル開発

前に書いたので割愛。


Lightning Full Sandbox & Mask Data

f:id:dackdive:20191121191351p:plain

f:id:dackdive:20191121191358p:plain

数分で構築が完了する新しいSandbox(速度以外は従来のFull Sandboxと何が違うの?)と、そのSandboxで使えるデータのマスク機能。
運用環境のデータでテストしたいけどセンシティブなデータはマスクしたい...!っていうの実際あるので便利。

マスク方法はフィールドごとに以下の3種類から選べる。

  • Anonymization: ランダムな文字列に変換。例: BlakegB1ff95-$
  • Pseudonymization: 仮名化、と呼ぶらしい。元の値とは無関係だが読める文字列に変換。例:KelseyAmber
    • フィールドのコンテキストとか考慮されるんだろか
  • Deletion: フィールドの値をクリア

動画43:54あたりにデモあり。


リアルタイムイベントモニタリング

f:id:dackdive:20191121191731p:plain

Winter'20でGAになった機能らしいです。知らんかった。
イベントデータのほぼリアルタイムのストリーミングおよび保存 (正式リリース)

ヘルプもある:リアルタイムイベントモニタリング

リアルタイムイベントモニタリングを使用すると、次の点についてより貴重なインサイトを得ることができます。

  • 誰がいつどのデータを表示したか
  • どこでデータがアクセスされたか
  • いつユーザが UI を使用してレコードを変更するか
  • 誰がどこからログインしているか
  • 組織の誰がプラットフォームの暗号化管理に関連するアクションを実行しているか
  • どのシステム管理者が別のユーザとしてログインし、そのユーザとしてどのアクションを実行したか
  • Lightning ページの読み込みにどのくらいの時間がかかるか

Transaction Security Policyについては動画47:45あたりにデモあり。

f:id:dackdive:20191121193149p:plain

ユーザーがこのオブジェクトのレコードを閲覧しようとしたら、操作をブロックしつつ管理者に通知する、みたいなことがGUIでできるようになったのかな。


ロードマップ

f:id:dackdive:20191121194728p:plain

動画一番さいご。

"Lightning Web Runtime Beta"と"GitHub App Support Beta"が気になります。
前者はこないだLWCローカル開発機能を調べてたときに見つけた @webruntime/common とかだろうな。

@octokit/restのアーキテクチャについて調べた

良いREST APIクライアントの設計というものに関心があり、GitHub社の公式REST APIクライアントである @octokit/rest のコードを読んでみたメモです。

(ドキュメントは https://octokit.github.io/rest.js/)

知りたかったこと

漠然と、こういう疑問に対してなんらか知見が得られればいいなーと思っていました。

  • 全体的なディレクトリ構成やレイヤー。どういうふうに責務を分けているか
  • APIエンドポイントに対応するメソッドはどのように実装している?命名ルールやnamespaceなどの分割基準は?
  • Node.js環境とブラウザ環境を両方サポートするために、環境の差異をどのように吸収しているのか
  • エラーハンドリングはどうしてる?
  • テストはどうしてる?
    • 何に対してテストを書いているか。リクエスト部分のモックはどうしているか

調べてわかったことメモ

以下、特にまとまりなく書いてます。


@octokit/restのアーキテクチャ全体像

ライブラリ利用者目線での全体像は HOW_IT_WORKS.md がわかりやすいです。

f:id:dackdive:20191108220724p:plain

(画像はリンク先から引用)

図および本文から

などがわかりました。


@octokit/restと@octokit/request, @octokit/endpoint

依存関係は

@octokit/rest --(require)--> @octokit/request --(require)--> @octokit/endpoint

の順になっています。
それぞれの役割については以下のとおりです。


@octokit/endpoint

GitHub REST APIのエンドポイント(例:GET /gists/:gist_id) と必要なパラメータを受け取り、fetch APIaxiosに渡せる汎用的なリクエストオブジェクトに変換する。

(例)

const { endpoint } = require('@octokit/endpoint');

const options = endpoint('POST /repos/:owner/:repo/branches/:branch/protection/required_status_checks/contexts', {
  owner: 'zaki-yama',
  repo: 'my-repo',
  branch: 'my-branch',
});

console.log(options);

↑の結果

{ method: 'POST',
  url:
   'https://api.github.com/repos/zaki-yama/my-repo/branches/my-branch/protection/required_status_checks/contexts',
  headers:
   { accept: 'application/vnd.github.v3+json',
     'user-agent':
      'octokit-endpoint.js/5.4.1 Node.js/10.15.3 (macOS Mojave; x64)',
     'content-length': 0 } }

これ単体で使うことはなさそうですが、引数に補完が効くあたりは面白いというか便利だなと思いました。

f:id:dackdive:20191108225534p:plain


@octokit/request

先ほどの @octokit/endpoint を使い、実際にAPIを叩くところを行う。
APIリクエスト時のNode.js/ブラウザ環境の差分をどうしてるかとかはここを見ると良さそうです。


@octokit/rest

@octokit/request に認証まわりの機能や Hooks などの機構を追加して、最終的なAPIクライアントの形にしたもの。


(@octokit/request) Node.js環境とブラウザ環境の考慮は?

APIリクエスト部分は@octokit/requestがやっているということがわかったので、こっちを見てみます。
READMEには

It uses @octokit/endpoint to parse the passed options and sends the request using fetch (node-fetch in Node).

とあるので、ブラウザ環境ではfetch API、Node.js環境ではnode-fetchというライブラリを利用しているようです。
ところが、fetchを行っている https://github.com/octokit/request.js/blob/v5.3.1/src/fetch-wrapper.ts を見ても、動作環境の判定らしき処理はありません。

(https://github.com/octokit/request.js/blob/v5.3.1/src/fetch-wrapper.ts#L22-L36 を抜粋)

const fetch: typeof nodeFetch =
  (requestOptions.request && requestOptions.request.fetch) || nodeFetch;

return fetch(
  requestOptions.url,
  Object.assign(
    {
      method: requestOptions.method,
      body: requestOptions.body,
      headers: requestOptions.headers,
      redirect: requestOptions.redirect
    },
    requestOptions.request
  )
)

注)1, 2行目の requestOptions.request.fetch はオプションとして渡すとライブラリ利用者が任意のfetch関数に差し替えることができるもの

そこで node-fetch を見てみると、package.jsonで browser フィールドを指定している (そしてその中身は window.fetch) ことがわかります。

また webpack のドキュメントを読むと
https://webpack.js.org/configuration/resolve/#resolvemainfields
モジュールをimportする際、package.jsonbrowser -> module -> main フィールドの順に探しにいくようです。(target未指定の場合)

そのため、webpackなどのモジュールバンドラーを使ってバンドルしたJSはwindow.fetchを使うようになりそうです。
よって動作環境の違いはnode-fetch側でケアしており、@octokit/requestは特に何もしてませんでした。


(@octokit/rest) 各APIに対応するメソッドはどのように生成しているのか

@octokit/rest 内のコードを検索しても、octokit.repos.listForOrg()のようなメソッドはヒットしません。
で、調べてみたところ、以下のようなしくみで自動生成しているようでした。

生成されるroutes.jsonの中身はこんな感じになっている。

(https://github.com/octokit/rest.js/blob/master/plugins/rest-api-endpoints/routes.json#L4527-L4544 あたり)

{
  ...
  "repos": {
    "listForOrg": {
      "method": "GET",
      "params": {
        "direction": { "enum": ["asc", "desc"], "type": "string" },
        "org": { "required": true, "type": "string" },
        "page": { "type": "integer" },
        "per_page": { "type": "integer" },
        "sort": {
          "enum": ["created", "updated", "pushed", "full_name"],
          "type": "string"
        },
        "type": {
          "enum": ["all", "public", "private", "forks", "sources", "member"],
          "type": "string"
        }
      },
      "url": "/orgs/:org/repos"
    },
    ...
  },
  ...
}

JSONの第一階層(repos)がGraphQL APIscope、第二階層(listForOrg)がidに対応している。

  • このroutes.jsonを処理しているのはoctokit.registerEndpoints()という関数で、ここに定義されている
  • やっていることはひたすら octokit.<scope>.<id> で呼び出せる関数を生やす作業
  • code.jsと同じようなノリで、scripts/update-endpoints/typescript.jsはendpoints.jsonからTypeScript型定義を生成している
    • Mustacheを使っている
    • さらに、似たようなスクリプトが @octokit/endpoint にもある(↑で書いた補完が効くのはそのおかげ)

余談ですが、このoctokit.registerEndpoints()はライブラリ利用者が任意のエンドポイントを生やす関数として提供されているものでもあります。
参考:https://octokit.github.io/rest.js/#custom-endpoint-methods


(@octokit/request) エラーハンドリングはどのように行っている?

https://github.com/octokit/request.js/blob/master/src/fetch-wrapper.ts

RequestErrorという独自のエラークラスをthrowしています。エラークラスの実体は @octokit/request-errorという別のライブラリになっています。
APIリクエスト時に発生したエラーはすべてこのカスタムエラーとしてthrowする方針のようですね。
https://github.com/octokit/request.js/blob/master/src/fetch-wrapper.ts#L121-L124


Hooksのしくみはどうやって実現している?

https://octokit.github.io/rest.js/#hooks
よくわかってませんが、https://github.com/gr2m/before-after-hook というライブラリを使うとさくっと実現できる?


感想 & まだ調べられてないこと

パッケージレベルでかなり細かく分割してそれぞれをシンプルに保っているなあという点と、スキーマ情報から各エンドポイントに対応するメソッドを自動生成してるところなどはなるほどなあと思いました。

まだ調べられてないところとしてはテスト周りでしょうか。
どうやら一部のテスト(test/scenarios/以下)のために @octokit/fixtures-server というモックサーバーライブラリを使用しているらしく、このあたりも調べてみるとまた新たな学びがありそうです。

Salesforce: LWCのローカル開発機能(ベータ)を試す

先日、LWCのローカル開発機能のベータ版がリリースされました。

ブログ記事を読みつつ、手元で動かしてみたメモです。


LWC ローカル開発機能の特徴(ねらい)

ブログ記事では、以下の3点に言及しています。

  1. 可能な限り本番環境に近い環境で、コンポーネントをローカルでレンダリングする
    • ローカルで動けば実際の環境でも動く、を実現したい
  2. エラーの発見と解決を行いやすくする
  3. 実際のデータとのインテグレーション
    • Lightning Data ServiceやApexを使ったリクエストはスクラッチ組織にプロキシされるので、ローカル環境でも実データを使ったコンポーネントの描画ができる


インストール & セットアップ

Salesforce CLIプラグインとしてインストールします。

$ sfdx plugins:install @salesforce/lwc-dev-server

実データのやり取りにはスクラッチ組織を使用するため、DevHubの作成と認証、スクラッチ組織の作成とコードのpushまでは済ませておきます。

$ sfdx force:auth:web:login -d -a <myhuborg>
$ sfdx force:org:create -s -f config/project-scratch-def.json -a "LWC"
$ sfdx force:source:push

また、LWCは自分で作ったことほぼないので、今回は公式のlwc-recipesリポジトリで試します。

$ git clone https://github.com/trailheadapps/lwc-recipes
$ cd lwc-recipes


ローカルサーバーの起動

force:lightning:lwc:start というコマンドが追加されているので、プロジェクトルートで実行します。

$ sfdx force:lightning:lwc:start
Use of this plugin is subject to the Salesforce.com Program Agreement.
By installing this plugin, you agree to the Salesforce.com Program Agreement<https://trailblazer.me/terms>
and acknowledge the Salesforce Privacy Policy<https://www.salesforce.com/company/privacy.jsp>.

Starting LWC Local Development.
    Dev Hub Org: admin@yama.salesforce.com
    Scratch Org: test-lka7jajxzlec@example.com
    Api Version: 46.0
Template version key d2aed51205
[HPM] Proxy created: /  ->  https://power-computing-182-dev-ed.cs6.my.salesforce.com
[HPM] Subscribed to http-proxy events:  [ 'proxyReq', 'error', 'close' ]
Server up on http://localhost:3333

http://localhost:3333 にアクセスすると、このような画面が開きます。

f:id:dackdive:20191007041926p:plain

force-app/main/default/lwc 以下のLWCが一覧で表示されています。
ここから各コンポーネント名をクリックすると、該当のLWCがレンダリングされます。

f:id:dackdive:20191007042243p:plain


特徴

ブログ記事で言及されていた特徴をいくつか。

  • 該当のLWCをVSCodeで開くことができる

f:id:dackdive:20191007042659g:plain

  • エラー表示

f:id:dackdive:20191007043658p:plain

キャプチャは ldsDeleteRecord.js@track デコレータをtypoしたときです。
エラーが発生している行、スタックトレースが表示されています。

  • サーバーサイドとの通信

f:id:dackdive:20191007044531p:plain

実データを扱うコンポーネントについても問題なく表示されます。
Chromeの開発者ツールのNetworkタブより、実際に通信が発生しているのも確認できます。

また、recordIdプロパティを使っているコンポーネントについては、IDをハードコードすることで実データを取得することができます。

例:wireGetRecordStaticContact.js

f:id:dackdive:20191007045049p:plain

このへんの体験は将来的にもうちょっと良くなる予定、って書かれてました。

In coming releases, there will be tools in the Local Development Server to set component attributes so that it won’t be necessary to hard code development values when running and debugging locally.


所感

ちょっと試した限りではLWC版Storybookという印象でした。
サーバーとの通信はモックではなく実際にスクラッチ組織とやり取りするという点は賛否ありそうだなーという感想。

個人的には、そのせいでオブジェクトのメタデータやApexをpushしないといけないのでモックの方がいいかな(もしくは選べる)。

Proxyのしくみとかは気になるのでコード読みたいなと思ったんですが、リポジトリは公開されていない様子。
(おそらく https://github.com/forcedotcom/lwc-dev-server というリポジトリ)

インストールしたプラグイン

~/.local/share/sfdx/node_modules/@salesforce/lwc-dev-server

にあるので、何か手がかりはつかめるかもしれませんがそこまでの気力は湧かず。

複数のモジュール形式(CommonJS, ES Modules, UMD)をサポートしたnpmパッケージの作り方 in TypeScript

はじめに

npmパッケージを開発するとき、パッケージ利用者の実行環境に合わせて適切なモジュール形式のファイルをパッケージに含め、提供する必要があります。
具体的には、たとえば以下のようなバリエーションが考えられます。

  • Node.js環境であれば CommonJS 形式 (module.exports / require() )
  • ブラウザ環境で、webpackやRollupなどのモジュールバンドラーを前提とするならば CommonJSES Modules 形式 (export / import )
  • ブラウザ環境で、モジュールバンドラーなどは使わず<script>タグでファイルを読み込んで利用するならば UMD 形式

このとき、パッケージ提供側はどういったファイルをパッケージに含めるべきなのか、またそれを TypeScript でどのように実現できるのかがあまりよくわかっていなかったので、整理しました。

注:今回想定していない環境

目次



最初にまとめ

最終的なディレクトリ構成は以下のようになりました。

f:id:dackdive:20190923032928p:plain

また、package.jsonには以下main, module, types, filesフィールドを指定します。

// package.json
{
  "main": "lib/index.js",
  "module": "esm/index.js",
  "types": "lib/index.d.js",
  "files": [
    "lib",
    "esm",
    "umd"
  ],
  // ...
}

詳しくはGitHubにサンプルリポジトリを作りました。


1. CommonJS 形式

CommonJSはNode.js環境で採用されているモジュール形式であることと、ブラウザ環境でもwebpackなどのモジュールバンドラーやTypeScriptがこの形式をサポートしていることから、
とりあえずCommonJS形式でパッケージを提供しておけばNode.js環境とブラウザ環境を両方サポートできます。


tsconfig.json

TypeScriptでCommonJS形式でコンパイルする場合、tsconfig.jsonmoduleプロパティにcommonjsを指定します。
また、出力先のディレクトリ名は任意ですが、ここではlibとしてoutDirプロパティに指定しておきます。

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "./lib"
    // ...
  }
}

こうすると、ソースファイルを格納しているディレクトリ(ここではsrc)と同じ構成のまま、
TypeScript → JavaScriptコンパイルした結果がlibディレクトリに出力されます。

- src/
  - index.ts
  - moduleA.ts
  - moduleB.ts
- lib/
  - index.js
  - moduleA.js
  - moduleB.js


package.json

package.jsonでは、mainフィールドにパッケージのエントリーポイントとなるファイルのパスを、filesに出力先ディレクトlibを記載します。

// package.json
{
  "main": "lib/index.js",
  "files": [
    "lib"
  ]
}


2. ES Modules 形式

webpack(v2以降)やRollupなどのモジュールバンドラーには、Tree Shakingという機能が備わっています。
これは、exportしているがどこからもimportされていない不要なモジュールをバンドル時に削除する機能です。

参考:webpackの公式ドキュメント内でTree Shakingについて説明されているページ

パッケージ提供側が多数のモジュールを提供している場合、ES Modules形式つまり import/export で書かれたファイルをCommonJS形式に変換せずそのまま提供することで、利用者側はTree Shakingの恩恵を受けることができます。


tsconfig.json

TypeScriptでこれを実現する場合、CommonJSと同様にmoduleプロパティを使用します。
先ほどはcommonjsという値を指定しましたが、今度はESNextを指定します。

また、先ほどのCommonJS形式のファイルと出力先のディレクトリを分けるため、outDir: "./esm"も指定します。同じくディレクトリ名は任意です。

// tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
    "outDir": "./esm"
    // ...
  }
}

こうすることで、CommonJS形式と同じくsrc下と同じ構成のファイル群のJSがesmに生成されます。

- src/
  - index.ts
  - moduleA.ts
  - moduleB.ts
- lib/  # 中身はCommonJS( module.exports / require )
  - index.js
  - moduleA.js
  - moduleB.js
- esm/  # 中身はES Modules( export / import )
  - index.js
  - moduleA.js
  - moduleB.js


NOTE: tsconfig.jsonの構成をどのようにすべきか

このように、同じTypeScriptのソースファイルから複数の設定でコンパイルする場合、プロパティの異なる複数のtsconfig.jsonが必要になります。
tsconfig.jsonextendsというプロパティがあり、これを使うとある設定ファイルの内容を継承して一部だけ上書きできます。
モジュール形式によらないプロパティはベースの設定ファイルに記述し、モジュール形式によって差が出るプロパティはベースを継承した別の設定ファイルを定義するのが良いかと思います。

// tsconfig.base.json
{
  "compilerOptions": {
    "target": "es5",
    "strict": true,
    // ...
  }
}
// tsconfig.esm.json
{
  "extends": "./tsconfig.base",
  "compilerOptions": {
    "module": "ESNext",
    "outDir": "./esm"
  }
}

また、コンパイル時は-pまたは--projectオプションで使用する設定ファイルを指定できるので、これをpackage.jsonのscriptsに記載しておくと良いでしょう。

// package.json
{
  "scripts": {
    "build": "npm run build:cjs && npm run build:esm",
    // CommonJS用
    "build:cjs": "tsc -p tsconfig.cjs.json",
    // ES Modules 用
    "build:esm": "tsc -p tsconfig.esm.json"
  }
}

詳しくはサンプルリポジトリを参考にしてください。


package.json

package.jsonには、CommonJS形式のときと同じくfilesフィールドに出力したディレクトリ(esm)を追加するほか、moduleフィールドでesm内のエントリーポイントを指定します。

// package.json
{
  "main": "lib/index.js",
  "module": "esm/index.js",
  "files": [
    "lib",
    "esm"
  ]
}

moduleフィールドについて、公式ドキュメントには載っていませんでしたが、以下のStack Overflowが見つかりました。

Node.jsでES Modulesを扱うためのProposalということでしょうか...(あんまりわかってない)
また、Rollupのドキュメントには以下の記述があります。

https://rollupjs.org/guide/en/#publishing-es-modules

If your package.json file also has a module field, ESM-aware tools like Rollup and webpack 2+ will import the ES module version directly.

つまり、モジュールバンドラーはこのフィールドで指定したパスのファイルをES Modules形式で扱うようです。


3. UMD

Reactなどのメジャーなライブラリでは、インストール方法として npm install だけではなく以下のようにCDNで利用できる形式をサポートしています。

https://reactjs.org/docs/cdn-links.html

<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

これをHTMLに書くとReactReactDOMグローバル変数に定義され、使えるようになるため、利用者側はモジュールバンドラーを使った環境を構築する必要がありません。
オンラインエディターなどでも簡単に試すことができます。

これを実現するためのモジュール形式がUMDです。


webpack.config.js

パッケージをUMD形式で配布するためには最終的にファイルを1つにバンドルする必要があり、これはTypeScript単体では完結しません。何らかのモジュールバンドラーを利用する必要があります。
ここではwebpackを例にします。

webpackでUMD形式のファイルを作成するには、output.libraryおよびoutput.libraryTargetフィールドを使用します。

https://webpack.js.org/configuration/output/#outputlibrary
https://webpack.js.org/configuration/output/#outputlibrarytarget

// webpack.config.js
const path = require("path");

module.exports = {
  mode: "production",
  entry: "./src/index.ts",
  output: {
    path: path.resolve(__dirname, "umd"),
    filename: "my-typescript-package.js",
    library: "MyTsPackage",
    libraryTarget: "umd"
  },
  module: {
    rules: [
      {
        test: /\.ts(x*)?$/,
        exclude: /node_modules/,
        use: {
          loader: "ts-loader",
          options: {
            configFile: "tsconfig.umd.json"
          }
        }
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js"]
  }
};

libraryTargetumdを指定することでUMD形式でのバンドルを行います。またバンドルしたファイルを読み込んだときにlibraryに指定したグローバル変数名でアクセスできるようになります。

なお、webpackを使用する場合、TypeScriptもwebpack内で使用するためts-loaderが必要となります。
options.configFileは設定ファイルがtsconfig.jsonという名前の場合は不要です。


tsconfig.json

tsconfig.json側は特別な設定は必要ありません。出力先のディレクトリも今回はwebpack.config.js側で指定しているため不要です。

ただ、
最新版TypeScript+webpack 4の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き) - ICS MEDIA
にもあるように、moduleESNextを指定していないと、ソースファイル内に使用していないexportが混ざっていてもTree Shakingが機能せず、バンドルファイルに含まれることになります。
(これはアプリケーションを開発する際は重要ですが、npmパッケージとして提供したいようなライブラリの場合は使っていないexportは基本ないはず...)

また、ベースの設定ファイルでdeclaration: trueにして型定義ファイルを出力するようにしている場合、UMD用の設定では出力をOFF(declaration: false)にしておくのが良いと思います。


package.json

filesumdディレクトリを追加することと、UMDビルド用のスクリプトを追加すること以外は特にありません。

   "scripts": {
-    "build": "npm run build:cjs && npm run build:esm",
+    "build": "npm run build:cjs && npm run build:esm && npm run build:umd",
     "build:cjs": "tsc -p tsconfig.cjs.json",
     "build:esm": "tsc -p tsconfig.esm.json",
+    "build:umd": "webpack",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "files": [
     "lib",
-    "esm"
+    "esm",
+    "umd"
   ],

ここまでで、CommonJS、ES Modules、UMDのファイルが以下のような構成で出力されるようになっています。

- src/
  - index.ts
  - moduleA.ts
  - moduleB.ts
- lib/  # 中身はCommonJS( module.exports / require )
  - index.js
  - moduleA.js
  - moduleB.js
- esm/  # 中身はES Modules( export / import )
  - index.js
  - moduleA.js
  - moduleB.js
- umd/  # UMD
  - my-typescript-package.js


NOTE: development用とproduction用の2つのバンドルを用意する場合は?

先ほど例として挙げたReactもそうですが、UMD形式に対応しているライブラリはdevelopment用 (foo.development.js)とproduction用(foo.production.js)の2種類を提供していることがあります。

https://reactjs.org/docs/cdn-links.html
f:id:dackdive:20190918030405p:plain:w400

(試していませんが) これを実現したい場合、webpackの
Configuration Types > Exporting multiple configurations
によると、webpack.config.jsで配列を返すようにすれば複数の設定でバンドルした結果を出力してくれます。
modeoutput.filenameだけ異なる2つの設定を用意し、まとめてumd/以下に出力すれば良いでしょう。


NOTE: 出力先のディレクトリ名について

UMD形式についてもディレクトリ名は任意です。
が、umdまたはdistという名前にしておくと、unpkg.comというCDNがnpm publish時に自動的に認識してくれ、publishしたパッケージが

https://unpkg.com/<package name>@<version>/<`umd` or `dist`>/<file name>

というURLで利用できるようになるので、この慣習にならっておくのがおすすめです。

参考:https://unpkg.com/#workflow

f:id:dackdive:20190918030605p:plain


4. (おまけ) 型定義ファイル(.d.ts)

モジュール形式とは別の話ですが、TypeScriptで開発したパッケージの場合、本体のスクリプトと一緒に型定義ファイルもパッケージに含めて提供してあげる必要があります。

必要なことは

  • tsconfig.jsondeclaration: trueを追加
  • package.jsontypesフィールドに出力した型定義ファイルのエントリーポイントを指定

の2点です。

後者は、たとえばlib/ディレクトリ以下に

- src/
  - index.ts
  - moduleA.ts
  - moduleB.ts
- lib/
  - index.js
  - index.d.ts
  - moduleA.js
  - moduleA.d.ts
  - moduleB.js
  - moduleB.d.ts

というように型定義ファイルを出力するようにしている場合、

// package.json
{
  "main": "lib/index.js",
  "types": "lib/index.d.js"
}

となります。

参考:https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#including-declarations-in-your-npm-package

※サンプルリポジトリではlib/にもesm/にも型定義ファイルを出力するようになっていますが、どちらかだけで十分のはず


余談

上に書きませんでしたが、調べている過程で見つけたTips的な情報を。


package.jsonfilesフィールドは必ず必要なのか

filesはnpm publish時にパッケージに含めるファイルやディレクトリを指定するためのフィールドで、指定しなかった場合は全てのファイルがパッケージに含められます。
そのため、余計なファイルが含まれることを許容すればこのフィールドは指定しなくても良さそうに思えます。

が、 https://docs.npmjs.com/files/package.json#files を読むと

You can also provide a .npmignore file in the root of your package or in subdirectories, which will keep files from being included. (中略) If there is a .gitignore file, and .npmignore is missing, .gitignore’s contents will be used instead.

とあります。つまり

  • .npmignore に列挙したファイルやディレクトリはパッケージから除外される
  • .npmignore を置いていない場合、.gitignoreの内容が使われる

ということです。
今回のlibのようにコンパイルしたファイルの出力先はgit commitしないよう.gitignoreに記載するのが一般的だと思われるので、filesの指定がないと意図せずパッケージにも含まれなくなってしまいます。
このような事故を避けるため、またパッケージの内容を明示的にするためにもfilesは記載しておくのがおすすめです。


package.jsonunpkgフィールドとは

UMD形式をサポートしているパッケージをいくつか調べたところ、package.jsonunpkgというフィールドを指定しているパッケージがありました。

これについては unpkg.com のサイトを確認したところ

https://unpkg.com/#examples

If you omit the file path (i.e. use a “bare” URL), unpkg will serve the file specified by the unpkg field in package.json, or fall back to main.

とあるので、UMD形式で出力したファイルのパスを指定しておけば、利用者は

https://unpkg.com/<package name>@<version>

というURLでパッケージを読み込めるというわけですね。
(あんましなさそうですが)


webpack.config.jsのexternalsオプションとは

これもUMD形式をサポートしているパッケージでよく見かけました。

参考:

// webpack.config.js
externals: {
  react: 'React',
  'react-dom': 'ReactDOM',
}

// または
externals: {
  react: {
    amd: 'react',
    commonjs: 'react',
    commonjs2: 'react',
    root: 'React',
  },
  // ...
}

これについてはwebpackの公式ドキュメント
https://webpack.js.org/configuration/externals/

によると、このオプションで指定したモジュールはバンドルしたファイルに含まれなくなり、パッケージ利用者側が別途モジュールを読み込む必要があります。


リファレンス

モジュール形式については以下が参考になりました。

参考にしたライブラリ

TypeScript: 配列の値をString Literal Typesとして使う

メモ。
こんな感じで、ある配列として定義されている値に対し、「その値のいずれか」を意図した String Literal Types を作りたい。

const size = ['small', 'medium', 'large'];

type Size = 'small' | 'medium' | 'large';

でも、配列と型定義とで値を二度書きたくない。


こうする

TypeScript 3.4 で導入された const assertion を使うと、次のように書ける。

const size = ['small', 'medium', 'large'] as const; // readonly ["small", "medium", "large"]

type Size = typeof size[number]; // 'small' | 'medium' | 'large'

(Playground)

こちらの Stack Overflow を参考にした。


リファレンス

TypeScript: ReactNode型とReactElement型とReactChild型

メモ。

  • ReactNode
  • ReactElement
  • ReactChild

の関係性、何回か調べている気がするので整理しておく。

f:id:dackdive:20190807012948p:plain

@types/react の型定義
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts
を参照した。

図で、ReactNodeArrayArray<ReactNode> 以外の線は Union Types を表している。
たとえば

type ReactChild = ReactElement | ReactText

である。


メモ

  • JSX が受け付けるすべてのものをまとめた type として ReactNode があり、そこから string や null などを除いた純粋な React コンポーネントを意味するのが ReactElement
  • ほとんどが type alias だが、ReactNodeArrayReactPortal だけは interface だった
    • ReactNodeArrayArray<ReactNode> を継承してるだけ

npm installしたパッケージをVSCodeでデバッグする

npmでインストールしたパッケージが期待通りに動かず内部の動作を確認したい、となったときの話。

これまでは該当のスクリプトconsole.log() とか直接書いてデバッグしてたんですが、あまりにも効率が悪いのでもうちょっと良いやり方を見つけたい。
VSCodeでbreakpoint置きつつ変数の中身を見たりステップ実行したい。
と思ったので調べました。

ここではデバッグするパッケージの例として Storybook を取り上げてみます。
Storybookはたいていpackage.json

"scripts": {
  "storybook": "start-storybook -p 6006 -s ./assets"
}

のようなスクリプトを書いて実行することが多いと思います。
このとき、start-storybookというコマンド内部で何が行われているかを調査します。


手順

1. start-storybookコマンドで実行されるファイルを特定する

まずは、start-storybookコマンドによって実行されるファイルを見つけます。
npm scriptにコマンドで書けるということは node_modules/.bin 以下に該当のコマンドがあるはずなので探すと、

$ ls -l node_modules/.bin | grep start-storybook
lrwxr-xr-x  1 yamazaki  staff  32  6 20 22:37 start-storybook@ -> ../@storybook/react/bin/index.js

ということで、今回の場合 node_modules/@storybook/react/bin/index.js を見ればいいことがわかります。

また、VSCodeの場合シンボリックリンクはマウスオーバーでリンク先がわかります。

f:id:dackdive:20190620230331g:plain

(もうちょっと良い表示方法ないのかな)

別の方法として、対象のnpmパッケージのpackage.jsonbinを見るという方法もあります。

見つけたファイルの冒頭にbreakpointを置いておきます。
今回の @storybook/react/bin/index.js は @storybook/react/dist/server/index.js を読み込んでいるだけだったので、そちらの先頭にbreakpointをつけました。

f:id:dackdive:20190620230941p:plain


2. デバッグ用の設定ファイル(launch.json)を作る

次は、VSCode上でデバッグを行うために必要な設定ファイルを作ります。 コマンドパレット(Macならcmd+shift+P)で

Debug: Open 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": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/index.js"
    }
  ]
}

という雛形が作成されているかと思います。


3. デバッグしたいコマンドをprogramに記載する

作成したlaunch.jsonprogramという値が、デバッグ実行されるコマンドです。
なので、ここにnpm scriptとして書かれていたコマンドをコピーしてきます。

注意事項としては

  • npm scriptにstart-storybookと書いていた場合、programには${workspaceFolder}/node_modules/.bin/start-storybookと書く
    • ${workspaceFolder}は必須っぽいです
  • 引数がある場合、programではなくargsという属性に配列で記載する

という点があります。

なので、今回の場合

{
  // 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": [
    {
      "type": "node",
      "request": "launch",
      "name": "debug start-storybook",
      "program": "${workspaceFolder}/node_modules/.bin/start-storybook",
      "args": ["-p 6006", "-s ./assets"]
    }
  ]
}

となります。

なおnameは識別しやすい好きな名前に変えていいです。


4. デバッグ実行する

ここまでで準備はできたので、コマンドを実行します。
左側のDebugアイコンか、Macだとcmd+shift+Dデバッグ用のパネルが表示されます。

f:id:dackdive:20190620232929p:plain

パネル上部にnameで指定したコマンドが表示されます。目的のコマンドを選んで▶をクリックします。

f:id:dackdive:20190620233142p:plain

無事、最初にbreakpointを置いた箇所で止まってくれました。
左側のパネルのVARIABLES、WATCH、CALL STACK、BREAKPOINTSなどはだいたいブラウザのDeveloper Toolsと同じ機能です。

というわけで、無事やりたいことが実現できました。


よくわかっていないこと

公式ドキュメントによると、launch.jsonconfigurationsの書き方は何種類かあるようです。

そのうちの1つに Launch via NPM というのがあるので、npm scriptはこっちを使えばいいのかと思ってましたがうまく起動できませんでした。
(エラーにはならないが何も起きない)


リファレンス

VSCodeでのデバッグ方法については、公式ドキュメントにかなり多くの情報が載っています。

自分もまだ完全には理解できてません。