dackdive's blog

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

@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でのデバッグ方法については、公式ドキュメントにかなり多くの情報が載っています。

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

Google I/O'19の「Speed at Scale: Web Performance Tips and Tricks from the Trenches」を観たメモ

途中飛ばしてしまった箇所もありますが、一通り観たのでメモを。

長いので最初にまとめ

動画の最後のスライドにキーワードがまとまってます。

f:id:dackdive:20190604035502p:plain

  • 速くする(Get fast)ための13個のTips
  • 速さを維持する(Stay Fast)ためのTipsとしてPerformance BudgetsおよびLightWallet

が紹介されています。
また、このセッションではパフォーマンス改善の事例が多数紹介されています。

f:id:dackdive:20190604035551p:plain


Performance Budgets

  • パフォーマンスを維持するためのしくみ
    • 予算を管理するように守るべきパフォーマンス指標を設定できる
  • Performance Budgetsでできること
    • Time (TTI ◯秒以下)
    • Resources (JSは◯KB以下)
    • Lighthouse (スコア◯点以上)

f:id:dackdive:20190604035618p:plain

  • Budgetsすなわち予算を設定することで、shipする前にパフォーマンスの問題に気づける
  • 事例: Walmart Grocely
    • PR時にチェックし、バンドルサイズが1%以上増加するとPRは失敗、Issue登録されPerformance Engineerにエスカレーションされる


LightWallet

  • Webサイトのパフォーマンス測定ツールであるLighthouseでPerformance Budgetsをサポート
  • CLI版のLighthouseで使える
    • Lighthouse自体はCLI版とChrome拡張版がある
$ npm i -g lighthouse
$ lighthouse https://example.com --budgetPath budgets.json --chrome-flags="--window-size=1280,660" --view

🤔時間系のBudgetsは指定できない?


画像

Lazy Loading
  • JSのライブラリを使った例だと、こんな書き方をすることが多い
    • LazySizesやReact Lazy Loadなど
<img data-src="images/unicorn.jpg" class="lazyload">
  • 事例: chrome.com サイト
    • 20% Faster page load times (mobile)
    • 26% Faster page laod times (desktop, Windows)
    • 画像サイズに関してはページ初回ロード(initial page)で46%の削減に成功
  • 事例: Netflix

    • たくさんの動画がタイル状に並んだページ
    • 初期描画では最初に見える数行分の画像のみロードし、残りはlazy load
    • ページ全体で4.4MBの画像があるのを、初期描画では1.2MBに抑えることができた
    • メモリだと45MB(Full Page Load) -> 8MB(Initial Page State)
  • ブラウザのAPIで画像のlazy loadをサポート(comming to Chrome this summer.)

    • iframeも
<img loading=eager>
<img loading=lazy>


Responsive Images
<!-- By width -->
<img src="cat.jpg"
  srcset="cat-240.jpg 240w,
          cat-480.jpg 480w,
          cat-960.jpg 960w>


Images CDNs
  • image optimization as a service
    • Cloudinary, Imgix, Thumborなど

f:id:dackdive:20190604035824p:plain

  • 事例: Trivago
    • Image CDNとしてCloudinaryを採用
    • Image CDNを利用することにより、自社のエンジニアはコアビジネスに集中できた
    • 全体の画像サイズを80%削減
  • 日本の事例もいくつか(一休、ANA)

f:id:dackdive:20190604035910p:plain

JavaScript

Defer Third-Party JavaScript
  • third-partyのコードはJS実行時間の57%を占める
  • 事例:Telegraph
    • JSをすべて(third-partyだけでなく自社のものも含め)deferして描画を3s高速化した
  • ref. asyncdeferの違いについては \<script> タグに async / defer を付けた場合のタイミング - Qiita
    • deferはJSの実行順序を保証してくれる
  • 事例: chrome.com
    • トップページに埋め込んでいたYouTube動画をlazy loadしたところTTIで10sの高速化


Remove Expensive Libraries
  • moment.js, jQuery, Bootstrapのようなサイズの大きいライブラリをリプレイスする
    • 例: lodash -> lodash-es, momentjs -> date-fns
    • サイズが小さいことに加え、Tree-shakeableなライブラリを選ぶのも重要
  • 事例: Tokopedia
    • ランディングページをSvelteでリライトした
    • 新バージョンはabove-the-fold contentの描画に必要なJSは37KB
    • Reactで作成した現行バージョンは320KB
    • ランディングページ以外はReactのまま。Service Workerを使ってprecache
  • Update dependencies
    • 事例: Zalando (ヨーロッパのfashion retailer)
      • Reactを15.6.1 -> 16.2.0に上げただけでload timeが100ms向上した
  • Code-splitting


Critical CSS

  • Critical CSS: above-the-fold contentの描画に必要なCSS
  • ドキュメントにインラインで埋め込むべき。14KB以下
  • 事例: TUI (ヨーロッパの旅行サイト)
    • First Contentful Paintの描画を2.4s -> 1.2sに短縮
  • 事例: 日経
    • Critical CSSが300KBもあった
    • シチュエーション(ユーザーがログイン状態か、など)に応じた適切なCritical CSSを返すサーバーを構築
    • ESI: Edge Side Inclusion