ESM 共有ライブラリの作成


Alfons MoralesUnsplashによる写真

monorepo の対象を探しているうちに、API を呼び出して何かを実行する基本的なアプリケーションを作成することにしました.そこで、 Public APIs を調べて、使用する交換 API を選択します.それらの API の中から、 Free Currency Rates API を選択します.

パッケージの初期化



以前のルート リポジトリでは、共有ライブラリを packages フォルダーに保存するので、その下に exchange API を呼び出す exchange-api パッケージを作成します.

// packages/exchange-api/package.json
{
    "name": "exchange-api",

    ...

    "type": "module",

    ...

    "exports": "./lib/index.js",
    "types": "lib",
    "files": [
        "lib"
    ]
}


この ESM パッケージとして、 "type": "module" を設定し、 exports の代わりに main を使用します. TypeScript でビルドされた出力は lib に配置され、他のパッケージには typesfiles が追加されます.

API 呼び出し用に node-fetch 、日付形式用に date-fns 、および typescript を追加します.

yarn workspace exchange-api add date-fns node-fetch
yarn workspace exchange-api add -D typescript

tsconfig.json を作成します.

// packages/exchange-api/tsconfig.json
{
    "extends": "../../tsconfig.json",
    "include": [
        "**/*.js",
        "**/*.ts"
    ]
}


ルート tsconfig.json を参照します. TypeScript ビルド用のもう 1 つの構成ファイル.

// packages/exchange-api/tsconfig.build.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "noEmit": false,
        "outDir": "./lib",
        "newLine": "lf",
        "declaration": true
    },
    "include": [
        "src"
    ]
}


入力ファイルは src に、出力ファイルは lib に.型宣言も発行します.
build スクリプトを追加します.

// packages/exchange-api/package.json
{
    ...

    "scripts": {
        "build": "tsc -p ./tsconfig.build.json"
    },

    ...
}


では、パッケージを作成しましょう.

ビルド パッケージ



1.RateDate.ts



まず、日付を扱うクラスを作成します.

// packages/exchange-api/src/RateDate.ts
import { format } from 'date-fns';

class RateDate {
  readonly #date: Date;

  constructor(value: number | string | Date) {
    this.#date = new Date(value);
  }

  toString(): string {
    return format(this.#date, 'yyyy-MM-dd');
  }
}

export default RateDate;


入力からネイティブ Date オブジェクトを作成し、日付を date-fns で文字列にフォーマットします.
ES2019構文のprivateフィールドでネイティブオブジェクトをprivateに設定し、変更する必要がないのでTypeScriptのreadonlyプロパティを利用する.

次に、API を呼び出す関数を作成します.

2.exchange.ts


RateDate クラスと node-fetch をインポートします.

// packages/exchange-api/src/exchange.ts
import fetch from 'node-fetch';

import RateDate from './RateDate.js';


API 呼び出しの型と定数を設定します.

// packages/exchange-api/src/exchange.ts
...

type ApiVersion = number;
type Currency = string;
type Extension = 'min.json' | 'json';

const apiEndpoint = 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api';
const apiVersion: ApiVersion = 1;
const extension: Extension = 'json';


APIを呼び出して通貨を計算する関数を作成します.

// packages/exchange-api/src/exchange.ts
...

async function exchange(
  amount: number,
  from: Currency = 'krw',
  to: Currency = 'usd',
  date: number | string | Date = 'latest',
): Promise<{
  rate: number;
  amount: number;
} | void> {
  const dateStr = date !== 'latest' ? new RateDate(date).toString() : date;
  const fromLowerCase = from.toLowerCase();
  const toLowerCase = to.toLowerCase();
  const apiURLString = `${apiEndpoint}@${apiVersion}/${dateStr}/currencies/${fromLowerCase}/${toLowerCase}.${extension}`;
  const apiURL = new URL(apiURLString);

  try {
    const apiResponse = await fetch(apiURL.toString());

    if (apiResponse.status !== 200) {
      return {
        rate: 0,
        amount: 0,
      };
    } else {
      const convertedResponse = (await apiResponse.json()) as { [key: string]: string | number };
      const exchangeRate = convertedResponse[toLowerCase] as number;

      return {
        rate: exchangeRate,
        amount: Number(amount) * exchangeRate,
      };
    }
  } catch (error: unknown) {
    console.log("Can't fetch API return.");
    console.log((error as Error).toString());
  }
}

export default exchange;


交換するデフォルトの通貨は krw から usd です.

日付は基本的に latest になり、それ以外の日付は toStringRateDate 関数でフォーマットされます.これらの定数を構成して API エンドポイントの URI を構築し、それを呼び出します.
async/awaittry/catch を使用します.

呼び出しに失敗した場合、関数は void を返し、エラーをログに記録します.呼び出しに成功し、応答コードが 200 でない場合、為替レートと金額は 0 になります.

呼び出しが成功した場合は、為替レートと計算された交換額を返します.

// packages/exchange-api/src/exchange.ts
import fetch from 'node-fetch';

import RateDate from './RateDate.js';

type ApiVersion = number;
type Currency = string;
type Extension = 'min.json' | 'json';

const apiEndpoint = 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api';
const apiVersion: ApiVersion = 1;
const extension: Extension = 'json';

async function exchange(
  amount: number,
  from: Currency = 'krw',
  to: Currency = 'usd',
  date: number | string | Date = 'latest',
): Promise<{
  rate: number;
  amount: number;
} | void> {
  const dateStr = date !== 'latest' ? new RateDate(date).toString() : date;
  const fromLowerCase = from.toLowerCase();
  const toLowerCase = to.toLowerCase();
  const apiURLString = `${apiEndpoint}@${apiVersion}/${dateStr}/currencies/${fromLowerCase}/${toLowerCase}.${extension}`;
  const apiURL = new URL(apiURLString);

  try {
    const apiResponse = await fetch(apiURL.toString());

    if (apiResponse.status !== 200) {
      return {
        rate: 0,
        amount: 0,
      };
    } else {
      const convertedResponse = (await apiResponse.json()) as { [key: string]: string | number };
      const exchangeRate = convertedResponse[toLowerCase] as number;

      return {
        rate: exchangeRate,
        amount: Number(amount) * exchangeRate,
      };
    }
  } catch (error: unknown) {
    console.log("Can't fetch API return.");
    console.log((error as Error).toString());
  }
}

export default exchange;

exchange 関数を完了しました.

3.index.ts



パッケージは index.js に設定されたエントリ ポイント package.json で完了します

// packages/exchange-api/src/index.ts
import exchange from './exchange.js';

export { exchange as default };


テスト パッケージ



1.構成



テスト パッケージには Jest を使用します.

yarn workspace exchange-api add -D @babel/core @babel/preset-env @babel/preset-typescript babel-jest jest


パッケージ間でテスト環境を共有するには、ルート リポジトリに Babel config と Jest トランスフォームを設定します.

// babel.config.json
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ],
        "@babel/preset-typescript"
    ]
}



// scripts/jest-transformer.js
module.exports = require('babel-jest').default.createTransformer({
  rootMode: 'upward',
});

scripts/jest-transformer.js は、ルート リポジトリで構成を検索するように Babel を設定します. Babel Config Files を参照してください.
package.json に Jest 構成を追加します.

// packages/exchange-api/package.json
{
    ...

    "scripts": {
        "build": "tsc -p ./tsconfig.build.json",
        "test": "yarn node --experimental-vm-modules --no-warnings $(yarn bin jest)",
        "test:coverage": "yarn run test --coverage",
        "test:watch": "yarn run test --watchAll"
    },

    ...

    "jest": {
        "collectCoverageFrom": [
            "src/**/*.{ts,tsx}"
        ],
        "displayName": "EXCHANGE-API TEST",
        "extensionsToTreatAsEsm": [
            ".ts"
        ],
        "transform": {
            "^.+\\.[t|j]s$": "../../scripts/jest-transformer.js"
        },
        "moduleNameMapper": {
            "^(\\.{1,2}/.*)\\.js$": "$1"
        }
    }
}


TypeScript ファイルは jest-transformer.js によって変換され、.ts によって extensionsToTreatAsEsm ファイルが ESM に処理されます. test スクリプトを設定して Jest を構成し、ESM をサポートします.構成とスクリプトについては、Jest ECMAScript Modules を参照してください.

2.テストを書く



次に、テストを書き留めます.

// packages/exchange-api/__tests__/RateDate.spec.ts
import RateDate from '../src/RateDate';

describe('RateDate specification test', () => {
  it('should return string format', () => {
    const dataString = '2022-01-01';
    const result = new RateDate(dataString);

    expect(result.toString()).toEqual(dataString);
  });
});

toString クラスの RateDate 関数をテストして、正しくフォーマットします.

// packages/exchange-api/__tests__/exchange.spec.ts
import exchange from '../src/exchange';

describe('Exchange function test', () => {
  it('should exchange with default value', async () => {
    const result = await exchange(1000);

    expect(result).toHaveProperty('rate');
    expect(result).toHaveProperty('amount');
    expect(result.rate).not.toBeNaN();
    expect(result.amount).not.toBeNaN();
  });

  it('should make currency lowercase', async () => {
    const result = await exchange(1000, 'USD', 'KRW', '2022-01-01');

    expect(result).toHaveProperty('rate');
    expect(result).toHaveProperty('amount');
    expect(result.rate).not.toBeNaN();
    expect(result.amount).not.toBeNaN();
  });

  it('should return empty object when wrong input', async () => {
    const result = await exchange(1000, 'test');

    expect(result).toHaveProperty('rate');
    expect(result).toHaveProperty('amount');
    expect(result.rate).toEqual(0);
    expect(result.amount).toEqual(0);
  });
});

exchange 関数をテストして、デフォルト値と入力値でうまく機能し、間違った入力に対して 0 のオブジェクトを返します.

3. テストを実行する



パッケージをテストします.

yarn workspace exchange-api test


それはテストに合格します.

 PASS   EXCHANGE-API TEST  __tests__/RateDate.spec.ts
 PASS   EXCHANGE-API TEST  __tests__/exchange.spec.ts

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        3.687 s
Ran all test suites.


概要



私はパッケージしか使っていなかったので、パッケージをビルドするのは初めてなので非常に興味深い時期です.今回は、パッケージのエクスポートとタイプについて考えるべきであり、Node.js パッケージの理解を深めることができました.
RateDate クラスを作成して、他の日付操作が必要になるかもしれませんが、書式設定なしでは何もないので、役に立たない可能性があり、削除できます.

Jest、Mocha、Jasmine などの中で最も人気があると思われるため、テスト用に Jest を選択します.

次回は、babel-jest が機能するアプリケーションを作成しましょう.