Skip to content

chore: update package.json scripts and add pack-smoke script - #319

Draft
shinagawa-web wants to merge 5 commits into
mainfrom
ci/verify-published-exports
Draft

shinagawa-web wants to merge 5 commits into
mainfrom
ci/verify-published-exports

Conversation

@shinagawa-web

@shinagawa-web shinagawa-web commented Sep 3, 2026 •

Copy link
Copy Markdown
Collaborator

概要

配布物(npm publish後に実際にユーザーが受け取るファイル)がCIで検証されていなかったため、exports の設定ミスや、一部エントリポイントが特定の依存を無条件に必要としてしまう不具合が、これまで誰にも気づかれずに存在していました。
今回、実際にtarballをpack→隔離環境にinstallしてrequire/import両方を検証する test:pack-smoke と、exportsのtypes解決の正しさを静的に検証する test:attw(@arethetypeswrong/cli)の2つのチェックをCIに追加し、それによって検出された問題を修正します。

見つかった問題と対応

1. .(root)と /express が、msw未インストール環境で実際にクラッシュする

現象: msw を一切使わないユーザーが @notainc/typed-api-spec や @notainc/typed-api-spec/express をimport/requireしただけで、Cannot find module 'msw' で例外が発生する。

原因: src/index.ts が ./msw の関数(newMswHttp等)を無条件に re-export していました。mswはテスト用のモックライブラリで、本来は使う人だけが @notainc/typed-api-spec/msw を明示的にimportすべきものです。加えて src/express/index.ts が共有の型を "../index"(root barrel)経由でimportしていたため、tsupがバンドルする際に express のエントリポイントにも root 全体(=msw依存込み)が引き込まれていました。結果として、msw を使う予定がまったくない利用者でも、root か express のどちらかを触った瞬間に msw パッケージのインストールを強制される状態になっていました。

対応: src/index.ts から ./msw の re-export を削除(@notainc/typed-api-spec/msw から直接importする形に一本化)。src/express/index.ts は共有型の参照元を "../index" から "../core" に変更し、root全体を引き込まないようにしました。

影響(破壊的変更): import { newMswHttp } from "@notainc/typed-api-spec" としていた利用者は import { newMswHttp } from "@notainc/typed-api-spec/msw" への変更が必要です。中身は同一なのでimport元を変えるだけで済みます。

2. 全サブパスで、ESM向けの型定義(.d.mts)が exports から参照されておらず、node16 (from ESM) 解決で型が壊れている

現象: moduleResolution: "node16"/"nodenext" の設定で、かつESM側(import)からこのパッケージを使うTypeScriptプロジェクトでは、本来ESM向けに生成されているはずの型定義ファイルではなく、CJS向けの型定義ファイルが解決されてしまう(attwの分類では 🎭 Masquerading as CJS)。すぐに型エラーになるとは限りませんが、CJSとESMで型の解釈が食い違う場合に実行時と型情報がズレる潜在的なリスクがあります。

原因: package.json の exports フィールドで、各サブパスの types が require/import の中に入れ子になっておらず、1つの共通キーとして書かれていました。

"./core": {
  "types": "./dist/core/index.d.ts",
  "require": "./dist/core/index.js",
  "import": "./dist/core/index.mjs"
}

この書き方だと、TypeScriptはCJSとESMのどちらから解決する場合でも同じ types(CJS向けの .d.ts)を使ってしまいます。tsupは元々ESM向けの .d.mts も生成していたのですが、exports がそれを一度も参照していなかったため、生成されているのに使われない状態でした。

対応: types を require/import それぞれの内側に移動し、ESM側は対応する .d.mts を参照するように修正しました。

"./core": {
  "require": { "types": "./dist/core/index.d.ts", "default": "./dist/core/index.js" },
  "import": { "types": "./dist/core/index.d.mts", "default": "./dist/core/index.mjs" }
}

影響: 利用者側の対応は不要です(型解決が正しくなる方向の修正のみ)。

3. /fetch と /json で、CJS側のdefault exportの型と実体が食い違っている

現象: node16 (from CJS) 解決で ❗️ Incorrect default export が検出される。

原因: FetchT(/fetch)と JSONT(/json)はどちらもTypeScriptの型エイリアスであり、ランタイム上の値を持ちません。にもかかわらずソースでは export default FetchT; という値エクスポートの構文で書かれています。TypeScriptはこれをコンパイルできてしまい、.d.ts には type FetchT as default という記述が生成されますが、ビルド後の実際のJSファイルには(値が存在しないため)default というキー自体が一切出力されません。型定義は「defaultエクスポートがある」と主張しているのに、実体には存在しない、という食い違いです。

対応: FetchT・JSONT を export default ではなく named type export(export type { FetchT } 等)に変更します。

影響(破壊的変更): import JSONT from "@notainc/typed-api-spec/json" としていた利用者(examples/miscに実例あり)は import type { JSONT } from "@notainc/typed-api-spec/json" への変更が必要です(JSONTはランタイム値を持たない型なので、import { JSONT } ではなく import type を使うのが正しい書き方です)。実行時の挙動は変わらず、コンパイル時のimport構文の修正のみで済みます。

@shinagawa-web
shinagawa-web marked this pull request as draft September 3, 2026 08:58
…ints

src/index.ts unconditionally re-exported ./msw, and src/express/index.ts
imported shared types via the root barrel ("../index") instead of "../core".
Because tsup bundles re-exports per entrypoint, this meant both the root
package and @notainc/typed-api-spec/express required msw at runtime even
for consumers who never touch mocking, crashing with "Cannot find module
'msw'" when it wasn't installed. Root no longer re-exports msw helpers
(use @notainc/typed-api-spec/msw directly), and express now imports its
shared types straight from core.
package.json exports declared a single "types" key shared by both the
require and import conditions, so ESM consumers under moduleResolution
node16/nodenext always resolved to the CJS-flavored .d.ts. tsup already
emits a matching .d.mts per entrypoint, but nothing referenced it. Nest
"types" under "require"/"import" so each module system gets its own
declaration file, fixing attw's "Masquerading as CJS" finding for every
subpath under node16 (from ESM).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant