{ // 標準ライブラリは target 任せにせず、ここで明示する (#1314)。 // // lib を書かないと TypeScript は target から標準ライブラリを決める。target が // ES2020 のままだと Array.prototype.at / String.prototype.at (ES2022 で入った) // は本来そこで未定義になる。それでも検査が通っていたのは、TypeScript が // 親ディレクトリの node_modules/@types を自動で拾う性質のため——リポジトリ // 直下の @types/node@20 に入っている互換用の宣言 // (compatibility/indexable.d.ts) が at を後付けしていた。 // // ui/node_modules に新しい @types/node が入って直下の 20 を覆い隠すと後付けが // 消え、触っていないファイル 12 本・27 か所が TS2550 で一斉に落ちる // (#1264 の試作で実際に発生)。 // // 親の型定義が画面側に混ざり込む経路は types: [] で断った (#1316)。 // 詳しくは下の "types" の項を参照。 // // target は ES2020 のまま据え置く。tsc は noEmit なので target は出力に効かない // 一方、変換器はこのファイルを読む。vite 8 が実際に走らせるのは rolldown 側の // 変換 (transformWithOxc)——transformWithEsbuild は非推奨になっていて、esbuild // は既定では入っていない。oxc は tsconfig を自前で解決して target と // useDefineForClassFields を見る。いま useDefineForClassFields は書いていない // ので既定値は target から決まり、ES2022 以上に上げると true に転じて // クラスフィールドの代入意味論 ( / ) が変わる。出荷物の挙動が // 動くということ。今回直したいのは型検査で見える標準ライブラリの範囲だけなので、 // lib だけを明示する。 // // 中身は直前の暗黙値 (lib.es2020.full) の書き起こし。full が参照する 6 枚のうち // 落としたのは webworker.importscripts と scripthost の 2 枚だけで、どちらも // 画面側で未使用 (importScripts の参照 0 件)。DOM.AsyncIterable は落とさない—— // ReadableStream と FileSystemDirectoryHandle の for await はこの 1 枚が持って // いて、外すと「触っていないのに落ちる」が今度は逆向きに起きる。 // // 回帰の見張りは scripts/ui-typecheck-gate.test.ts。 "compilerOptions": { "target": "ES2020", "lib": ["ES2022", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], // 親ディレクトリの型定義を自動で取り込まない (#1316)。 // // TypeScript は types を書かないと node_modules/@types を親へ遡って**全部** // 自動で取り込む。ui/node_modules には @types/node が無いので、リポジトリ直下の // @types/node@20 がそっくり画面側の検査に入っていた。結果、ブラウザに無いものが // 検査を素通りする——setTimeout の戻り値に NodeJS.Timeout を当てる / // process.env を読む / Buffer を作る、のいずれも診断ゼロで通っていた (実測)。 // とくに Buffer はそのまま出荷され、実行時に壊れる。 // // ここが**出荷物の関所**。この設定は npm run build:ui → npm --prefix ui run // build の `tsc --noEmit` が使う。逆に tsconfig.typecheck.json 側は関所に // できない: e2e が better-sqlite3 を読み、その .d.ts の // `/// ` が Node の型をプログラム全体へ呼び戻すため、 // あちらでは types をどう書いても Buffer が通ってしまう (--explainFiles で確認)。 // // 空にすると e2e / vite / playwright の設定だけでなく、src 配下のテストも落ちる // (node:fs / node:path / global を使うものが 15 本・47 件)。テストは下の exclude で // この設定から外し、tsconfig.typecheck.json (types: ["node"]) が引き受ける。 // // ただし **types: [] だけでは関所として不十分**。止まるのは自動取り込みだけで、 // プログラム内のどれかの .d.ts が `/// ` を書いていれば // そこから Node の型が入り込む (すぐ上に書いた better-sqlite3 と同じ仕組み)。 // 実際に画面側へその 1 行を置くと、tsc は診断ゼロのまま通る。だから見張りは // 字面ではなく、本物のプログラムを組んで「読み込まれた @types/node のファイルが // 0 本であること」を見る (scripts/ui-typecheck-gate.test.ts)。 "types": [], // 部品が連れてくる Node の型を、型の空間だけで断つ (#1264)。 // // types: [] が止めるのは自動取り込みだけで、プログラム内の .d.ts が書いた // `/// ` は止まらない (すぐ上に書いた通り)。表計算の // 部品を入れたことで、その経路が実際に開いた: // @univerjs/sheets → @univerjs/protocol → @grpc/grpc-js → @grpc/proto-loader // 最後の 1 本が Node を参照していて、出荷物のプログラムに入る @types/node が // 0 本から 63 本に跳ねる (実測)。 // // 差し替えるのは型の解決だけ。実行時の @univerjs/protocol は列挙子と定数だけで // @grpc を読み込まず、ビルド後の dist にも grpc / protobuf は入っていない // (実測)。画面側から @grpc/* を読む箇所も 0 件なので、差し替えで壊れる利用者は // いない。理由の詳細は差し替え先のファイルの冒頭に書いた。 "paths": { "@grpc/proto-loader": ["./src/types/grpc-proto-loader-stub.d.ts"] }, "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "noEmit": true }, "include": [ "src" ], // テストとテスト基盤は出荷物ではないので、この設定 (出荷物の関所) では見ない // (#1316)。 // // 理由は 2 つある。 // // 1. types: [] と両立しない。vitest で Node の上を走るテストは node:fs や // global を正当に使う (外すと 15 本・47 件が落ちる)。 // 2. src/test/ を入れておくと、出荷物のプログラムがリポジトリ直下の // node_modules を引きずる。src/test/dom-setup.ts が読む vitest は // ui/package.json に書かれておらず、直下から解決されていた——#1316 が // 退治したのと同じ「親から拾ってくる」経路。外すと直下から入るファイルは // 109 本から 60 本に減り、残る 60 本は TypeScript の標準ライブラリだけになる // (実測)。テスト基盤を読んでいるのはテストだけで、画面側の実装からの参照は // 0 件。 // // 外したぶんの検査は tsconfig.typecheck.json が担当する。「ui/src のファイルが // 1 本残らずあちらの検査対象に入っていること」を // scripts/ui-typecheck-gate.test.ts が実ファイル一覧との差分で見張る。 "exclude": [ "node_modules", "dist", "src/**/*.test.ts", "src/**/*.test.tsx", "src/test/**" ] }