npm Library¶
Node.js Testing genType
genType 経由でファーストクラスの TypeScript バインディングを備えた、ReScript で記述された 公開可能な npm パッケージ。テンプレートは小規模ながら現実的な API 表面を同梱している — 同期的な挨拶、非同期の fetchWithTimeout(AbortController)、2 つのリストヘルパー —ので、初日からコンパイル・テスト・公開できる素材が揃っている。
プロジェクトを npm(パブリック/プライベートレジストリ)にリリースする予定で、JS/TS 利用者に ReScript ソースから 生成された本物の .d.ts 型を提供したい場合に選択する。本テンプレートは ESM のみの利用者を想定している。CJS 相互運用が必要であれば後から重ねて対応できるが、デフォルトはモダンバンドラと Node 24+ をターゲットにしている。
生成内容¶
my-project/
├── rescript.json
├── package.json # ESM, "main" / "types" / "exports" / "files" set
├── tsconfig.json # strict, declaration: true (for genType .d.ts)
├── src/
│ ├── Index.res # public entry — re-exports + greetChecked
│ ├── ListUtils.res # chunk + partitionMap (pure helpers)
│ ├── Fetcher.res # fetchWithTimeout (AbortController + setTimeout)
│ ├── Validation.res # zod or sury — selected in the wizard
│ └── __tests__/
│ ├── Index.test.mjs # greet smoke test (imports .res.mjs)
│ ├── ListUtils.test.mjs # chunk + partitionMap suites
│ └── Fetcher.test.mjs # fetchWithTimeout with vi.stubGlobal("fetch")
├── README.md # script docs + API Surface table + Publish flow
├── LICENSE # MIT, holder = project name
├── .nvmrc # Node 24
├── .gitignore # node_modules + ReScript build artifacts
├── .editorconfig # 2-space indent, LF line endings
└── .github/
├── dependabot.yml # weekly npm updates
└── workflows/ci.yml # install + rescript build + vitest
ウィザードオプション¶
オプション |
効果 |
|---|---|
Project name |
npm の |
Package manager |
npm / pnpm / yarn / bun。 |
Validation library |
|
主要な依存¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ |
|
|
標準ライブラリ |
|
|
コンパイル後の |
|
|
バリデーションのバックエンド(ウィザードで選択) |
|
|
モジュールごとのテストランナー |
|
|
|
|
|
|
|
主要なファイル¶
src/Index.res¶
公開エントリポイント。パッケージが提供するすべてのエクスポートは、定義として、あるいは別モジュールからの再エクスポートとして ここに集約されている。それぞれに @genType が付与されており、genType が Index.gen.d.ts に対応する宣言を生成する。
/** Returns a greeting addressed to [name]. */
@genType
let greet = (name: string) => {
`Hello from <projectName>, ${name}!`
}
/**
* Validates an untyped JSON input before greeting. Returns an [Error] message
* for TS/JS consumers that pass a malformed payload instead of throwing.
*/
@genType
let greetChecked = (input: JSON.t) =>
switch Validation.parseGreetInput(input) {
| Ok({name}) => Ok(greet(name))
| Error(message) => Error(message)
}
@genType let chunk = ListUtils.chunk
@genType let fetchWithTimeout = Fetcher.fetchWithTimeout
greetChecked は「呼び出し側を信用しない」場合の定番パターンである: JSON を Validation.parseGreetInput に通し、FFI 境界をまたぐ例外を投げる代わりに result を返す。型チェッカが不正な引数を捕捉できない素の JavaScript から利用しても安全な理由はここにある。
src/ListUtils.res¶
Array 操作の実例を兼ねた 2 つの汎用ヘルパー:
chunk(xs, ~size)— 配列を固定サイズのバケットに分割する。末尾のバケットは短くなる場合がある。partitionMap(xs, f)— 各要素にfを適用し、Ok/Errorの結果を 2 つの配列のタプルに振り分ける。
いずれも可変な ref アキュムレータで実装されており、読みやすく、実行時も高速で、ReScript の標準ライブラリが関数型と命令型のスタイルを組み合わせて使える点を示す好例である。
src/Fetcher.res¶
fetchWithTimeout は ReScript における Promise + AbortController の定番パターンを示している:
@val external fetch: (string, 'opts) => promise<'response> = "fetch"
type abortController
@new external makeAbortController: unit => abortController = "AbortController"
@get external signal: abortController => 'signal = "signal"
@send external abort: abortController => unit = "abort"
@genType
let fetchWithTimeout = async (url: string, ~timeoutMs: int): promise<'response> => {
let controller = makeAbortController()
let timer = setTimeout(() => controller->abort, timeoutMs)
try {
let response = await fetch(url, {"signal": controller->signal})
clearTimeout(timer)
response
} catch {
| err =>
clearTimeout(timer)
raise(err)
}
}
clearTimeout の呼び出しは成功・失敗の 両方 の分岐に置かれており、タイマーがリークしないようになっている。中断可能な非同期ヘルパーを書くときはこの形を踏襲すること — ストリーミング、リトライ、キャンセル トークンに自然に拡張できる。
src/Validation.res¶
parseGreetInput: JSON.t => result<greetInput, string> は、JS/TS 利用者が渡す 公開 API 引数 をバリデートする。シグネチャは zod 版と sury 版で同一なので、呼び出し側と型はどちらの バックエンドが選ばれたかで分岐する必要がない。
型なし入力を受ける新しい公開関数を追加する場合、ここに新しい parseFooInput を定義し、Index.res で実処理を行う前に呼び出すこと。result を返すことで契約が誠実に保たれる: JS 側は値か 可読なエラーメッセージを受け取り、FFI 境界をまたいで例外が伝搬することはない。
src/__tests__/Index.test.mjs¶
スモークテストは コンパイル後 の Index.res.mjs をインポートし、エクスポートされた greet を検証する:
import { describe, expect, it } from "vitest";
import { greet } from "../Index.res.mjs";
describe("greet", () => {
it("returns a greeting containing the supplied name", () => {
expect(greet("world")).toContain("world");
});
});
これは公開 API が JS 利用者の視点から到達可能であることを保証する定番のガードである — greet がエクスポートされなくなったり、@genType が外れたりすると、スイートが失敗する。新しい公開関数を追加する際は同じパターンに従うこと: Index.res.mjs からインポートし、実際に呼び出し、ランタイム値を検証する。
src/__tests__/ListUtils.test.mjs¶
ヘルパー向けのモジュール単位の Vitest スイート。注目すべき点として、ReScript が生成するバリアントのランタイムタグを直接検証している:
const [oks, errs] = partitionMap([1, 2, 3], (n) =>
n % 2 === 0 ? { TAG: "Ok", _0: n * 10 } : { TAG: "Error", _0: `odd: ${n}` }
);
{ TAG: "Ok", _0: ... } は ESM モードにおいて ReScript がポリモーフィックバリアントに対して 出力する形である。ReScript が生成したユニオンに対して JS 側のテストを書くときはこの点に留意すること — コンストラクタ関数ではなく、生のバリアントペイロードを渡して関数を呼ぶことになる。
src/__tests__/Fetcher.test.mjs¶
Vitest の vi.stubGlobal でグローバルな fetch をスタブする非同期テスト:
import { afterEach, describe, expect, it, vi } from "vitest";
import { fetchWithTimeout } from "../Fetcher.res.mjs";
afterEach(() => vi.restoreAllMocks());
describe("fetchWithTimeout", () => {
it("returns the response when fetch resolves in time", async () => {
const fake = { ok: true };
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(fake));
await expect(fetchWithTimeout("https://example.test", 100)).resolves.toBe(fake);
});
});
afterEach(() => vi.restoreAllMocks()) は必須である — これがないと、あるテストでスタブした fetch が次のテストへと漏れ、同様にスタブしていないスイートを壊してしまう。新しい fetch ベースのモジュールを 追加する際は同じパターンに従うこと。
package.json のエントリポイント¶
公開関連のフィールドは、利用者が import { greet } from "<pkg>" と書くだけで本物の型を得られるよう 配線されている:
{
"type": "module",
"main": "./src/Index.res.mjs",
"types": "./src/Index.gen.d.ts",
"exports": {
".": {
"types": "./src/Index.gen.d.ts",
"import": "./src/Index.res.mjs"
}
},
"files": [
"src/**/*.res.mjs",
"src/**/*.res",
"src/**/*.gen.d.ts",
"src/**/*.gen.tsx"
]
}
files の許可リストは意図的に絞られている: コンパイル済みの JS、対応する .gen.d.ts 型、(React コンポーネントバインディング用の)任意の .gen.tsx、元の .res ソースのみが公開 tarball に含まれる。それ以外のテスト・設定・node_modules などはデフォルトで除外される。
prepare スクリプト¶
scripts.prepare = "rescript" を設定しているため、別プロジェクトで本ライブラリを新規に npm install した際に、パッケージが利用される前に ReScript ソースがコンパイルされる。上記の files 許可リストと組み合わせれば、利用者は自分で rescript をインストールする必要がない — import してそのまま使えばよい。
npm スクリプト¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prepare と build は現在いずれも rescript だが、別々に保たれている。これにより、npm が依存する prepare の契約を壊さずに、後から build をより広いパイプライン(バンドリング、minify、アセットコピーなど)に つなげられる。
公開フロー¶
README には Publish セクションに対応する 4 ステップのレシピが同梱されている:
package.jsonのバージョンをバンプする。pnpm build(または利用しているパッケージマネージャの同等コマンド)を実行して、files許可リストが依存する.res.mjsと.gen.d.tsのアーティファクトを再生成する。pnpm testを実行して Vitest が通ることを確認する。npm publish --access publicを実行する(プライベートなスコープ付きパッケージの場合は--access publicを外す)。
prepare スクリプトがあるため、利用者が tarball をインストールすると 同時に rescript も実行される — これにより、公開アーティファクトが古くても利用者の node_modules には新鮮なコンパイル結果が入る。これはあくまで セーフティネットとして扱い、npm publish 前に pnpm build を実行することの代替とはしないこと。
npm のスコープ(@your-org/my-lib)下で公開する場合は、package.json の name と README 冒頭のインストールスニペットを更新すること。bin フィールドは本テンプレートでは利用していない(ライブラリは通常バイナリを 配布しない)ため、ここで改名するものはない。
2日目以降のレシピ¶
TypeScript からの変換 — 既存の
.tsモジュールを、他のライブラリ ファイルと並ぶ.resモジュールに移植するデッドコードの検出 —
reanalyzeを実行して、公開前にすべての エクスポートが実際に到達可能であることを確認するImport の最適化 — 公開面が広がっても
Index.resを整然と保つ
プロジェクトを開いた後の ReScript 側のエディタワークフローについては、機能概要 を参照すること。
補足¶
nodeエンジンは>=24に固定されており、.nvmrcには24が記載されている。これより古いメジャーバージョンは CI で検証されない。Vitest のスイートは
.resソースではなく、コンパイル後 の.res.mjs出力をインポートする。CI はvitest runの前にrescriptを実行してアーティファクトが存在することを保証する。ローカルでは別ターミナルでpnpm res:devを実行しておけば、保存時に再コンパイルされ、Vitest のウォッチャがその変更を拾う。Fetcher.test.mjsはvi.stubGlobalでグローバルなfetchをスタブし、afterEach(() => vi.restoreAllMocks())を使用する。新しい fetch ベースのモジュールを追加する際は 同じパターンに従い、スイートが互いに干渉しないようにすること。ListUtils.test.mjsは ReScript のバリアントランタイムタグ({ TAG: "Ok", _0: ... })を 直接検証する。これが ReScript がポリモーフィックバリアントに対して出力する形である — ReScript 生成のユニオンに対して JS 側のテストを書くときはこの点に留意すること。tsconfig.jsonはstrict: trueとdeclaration: trueを有効にして同梱している。これにより genType の.d.ts出力がtsc --noEmitを通過する。安易にこれらのフラグを緩めないこと — 公開される.d.tsファイルは TS 利用者が目にする唯一の契約である。prepareスクリプトがあるため、未公開 の tarball(npm pack && npm install ./pkg.tgz)を インストールした利用者にも新鮮な ReScript コンパイルが行われる。node_modulesがない CI マシンでは 驚かされることがある。引っかかった場合は--ignore-scriptsで上書きすること。.github/dependabot.ymlはpackage-ecosystem: npmに対して毎週 PR を作成する。ReScript /@rescript/runtimeのバンプは一緒にレビューすること —TemplateVersions.ktで 同じメジャーに固定されているのには理由がある。