package.json の Conditional Exports では順序が意味を持つ

Conditional Exports とは

package.json 内の exports フィールドには, 以下のように条件付きでエクスポートするファイルを指定できる (conditional exports).

{
  "name": "@susisu/example",
  "type": "module",
  "exports": {
    ".": {
      "require": "./lib/index.cjs",
      "default": "./lib/index.js"
    }
  }
}

たとえばこの例では CommonJS ファイル内から require("@susisu/example") のように参照した場合は require に指定された ./lib/index.cjs が, ESM のファイル内から import "@susisu/example" のように参照した場合は default に指定された ./lib/index.js が使われることになる.

requiredefault はそれぞれ条件を表していて, パッケージが参照された場合は条件にマッチしたものが使われるようになっている. どのような条件がサポートされているかランタイムやバンドラによって異なるので, 各々のドキュメントを参照されたい.

順序が意味を持つとはどういうことか

先程の例の defaultrequire の順番を入れ替えてみる.

{
  "name": "@susisu/example",
  "type": "module",
  "exports": {
    ".": {
      "default": "./lib/index.js",
      "require": "./lib/index.cjs"
    }
  }
}

もしこのようになっていると, CommonJS ファイル内から require("@susisu/example") のように参照した場合も, ESM のファイル内から import "@susisu/example" のように参照した場合も, default に指定された ./lib/index.js が使われる. CommonJS ファイルからも ESM である ./lib/index.js を参照することになるので, 場合によってはエラーになるかもしれない.

これは条件の比較が package.json に書いてある順番に先頭から行われるためで, default はあらゆる状況にマッチするので常に default に指定されたファイルが参照されることになってしまう. 条件の種類によって特別扱いがあるとか, 指定の強い (対象範囲の狭い) 条件ほど優先的に比較されるといったことはない.

ソース

Modules: Packages | Node.js v23.0.0 Documentation

Within the "exports" object, key order is significant. During condition matching, earlier entries have higher priority and take precedence over later entries. The general rule is that conditions should be from most specific to least specific in object order.

Package exports | webpack

In an object where each key is a condition, order of properties is significant. Conditions are handled in the order they are specified.

一方 JSON の仕様では

ECMA-404 - Ecma International

The JSON syntax does not impose any restrictions on the strings used as names, does not require that name strings be unique, and does not assign any significance to the ordering of name/value pairs. These are all semantic considerations that may be defined by JSON processors or in specifications defining specific uses of JSON for data interchange.

とあるように, JSON の仕様としてはオブジェクト内の順序が意味を持つかどうかについては特に規定しておらず, どう解釈して利用するかは処理系の実装や別の仕様に任せている.

一方 JavaScript の仕様では

JSON.parse でオブジェクトをパースする場合, 一部挙動が異なるものの, 解釈は概ね JavaScript のオブジェクトリテラルと同じとなっている.

したがって, Object.keys などでキーを列挙する場合は大抵の場合はキーの登場順になるので, 順序が重要な場合はこれをそのまま使うことができる.

> Object.keys(JSON.parse('{ "require": "./lib/index.cjs", "default": "./lib/index.js" }'))
[ 'require', 'default' ]
> Object.keys(JSON.parse('{ "default": "./lib/index.js", "require": "./lib/index.cjs" }'))
[ 'default', 'require' ]

ただしキーが配列のインデックスとして有効な値 (0 <= x <= 2 ** 32 - 2 = 4294967294) である場合はこの限りではなく, 他のキーよりも優先して列挙されるため注意が必要 (参考).

> Object.keys(JSON.parse('{ "require": "./lib/index.cjs", "4294967294": "./lib/number.js", "default": "./lib/index.js" }'))
[ '4294967294', 'require', 'default' ]
> Object.keys(JSON.parse('{ "require": "./lib/index.cjs", "4294967295": "./lib/number.js", "default": "./lib/index.js" }'))
[ 'require', '4294967295', 'default' ]

そもそも条件として未実装ならキーがあってもなくても同じなので, こんなことは全く気にしなくて良い. 実際 Node.js の実装も素朴に JSON.parse を使っていそうだった.

条件を追加できる立場にあって, かつ無意味に仕様や実装を複雑にする嫌がらせをしたい場合はぜひご利用ください (?)