unplugin-react-fine-grained-signals 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [English](README.md) | [日本語](README.ja.md)
4
4
 
5
5
  [`react-fine-grained-signals`](https://www.npmjs.com/package/react-fine-grained-signals)
6
- 向けの、`useSignals()` 自動挿入と任意のmanaged render scope変換を提供する汎用
6
+ 向けの、`useSignalTracking()` 自動挿入と任意のmanaged render scope変換を提供する汎用
7
7
  bundler integrationです。
8
8
 
9
9
  このpackageは意図的に唯一のbuild-time integrationです。Babel実装は内部に
@@ -15,8 +15,23 @@ bundler integrationです。
15
15
  pnpm add -D unplugin-react-fine-grained-signals
16
16
  ```
17
17
 
18
- `react-fine-grained-signals` はpeer dependencyです。このpackageはESM-onlyです。
19
- CommonJSの`require()`ではなく、ESM設定から`import`してください。
18
+ `react-fine-grained-signals` はpeer dependencyです。Node.jsは
19
+ `^22.18.0 || >=24.11.0` が必要です。変換が依存するBabel 8が宣言している範囲です。
20
+
21
+ pluginとruntimeは同じrelease line(v0.2同士)で使ってください。生成されるコードが
22
+ importするruntimeのentry pointはline間で変わります。v0.1からupgradeする場合は、
23
+ 両方のpackageを同時に更新し、`@useSignals` / `@noUseSignals` commentを書き換えて
24
+ ください。[v0.2移行ガイド](https://github.com/sakzw/react-fine-grained-signals/blob/main/docs/migration/v0.2.ja.md#transform-plugin)を参照してください。
25
+
26
+ このpackageはESMだけを配布しており、CommonJS buildはありません。`import` を
27
+ 使うESMのbuild設定が基本ですが、CommonJSの設定ファイル(`"type": "module"` の
28
+ ないprojectの `webpack.config.js` や `next.config.js`)からも `require()` で
29
+ 読み込めます。上記の範囲のNode.jsはすべて、組み込みの `require(esm)` によって
30
+ ESMのentry pointを読み込むためです。plugin factoryは `default` exportです。
31
+
32
+ ```js
33
+ const signals = require("unplugin-react-fine-grained-signals/webpack").default;
34
+ ```
20
35
 
21
36
  ## Vite
22
37
 
@@ -29,6 +44,68 @@ export default defineConfig({
29
44
  });
30
45
  ```
31
46
 
47
+ ### React Compilerと併用する場合
48
+
49
+ React Compilerはこの変換の *後* に実行される必要があります。そうでないと、
50
+ このpluginがcomponentに `"use no memo"` を付ける前に、compilerが `.value` の
51
+ 読み取りをmemoizationで消してしまい、そのcomponentは最初のrender以降、無言で
52
+ 更新されなくなります。compilerを実行するpluginより前に `signals()` を
53
+ listしてください。
54
+
55
+ ```ts
56
+ import react from "@vitejs/plugin-react";
57
+ import { defineConfig } from "vite";
58
+ import signals from "unplugin-react-fine-grained-signals/vite";
59
+
60
+ export default defineConfig({
61
+ plugins: [signals({ mode: "auto" }), react({ compiler: true })],
62
+ });
63
+ ```
64
+
65
+ 検証した構成(Vite 8、`@vitejs/plugin-react` 6.1)では、
66
+ `react({ compiler: true })` にはplugin-reactのoptionalな
67
+ `oxc-transform-react` packageも必要です。`babel-plugin-react-compiler` と
68
+ 一緒にinstallしてください。ない場合、buildはplugin-react自身の
69
+ 「optionalな `oxc-transform-react` packageが必要」というerrorで止まります。
70
+
71
+ Viteでは、この順序をhookのレベルでも守っています。pluginは `enforce: "pre"`
72
+ で、さらに `transform` hookが `order: "pre"` を宣言しており、Viteは
73
+ `enforce: "pre"` のgroupの中でこの値に基づいて並べ替えます。そのため、
74
+ `react()` を先にlistした場合でも、`@vitejs/plugin-react` 6のcompiler step
75
+ (`react({ compiler: true })`。それ自体は `enforce: "pre"` でhookの `order` は
76
+ なし)より先に実行されます。以前はこの順序だとcomponentが先にcompileされて
77
+ いました。ただしこれはViteの並び順の規則であって、絶対的な保証ではありません。
78
+ transformで同じく `order: "pre"` を宣言し、より前にlistされたpluginは、
79
+ やはり先に実行されます。したがって `signals()` を先頭に置く構成が引き続き
80
+ 推奨であり、hookの順序が効かない場合は必須です。Rollupは配列内の位置だけで
81
+ 順序を決めますし、Babel経由の構成(`@rolldown/plugin-babel` と
82
+ `reactCompilerPreset()`)はこのpluginとの組み合わせで検証していません。
83
+
84
+ React Compilerとの組み合わせには、次の2つの既知の制約があります。どちらも
85
+ v0.1.1から存在するものです。
86
+
87
+ - **inlineのrender helperがcompilerにmemoizeされることがある。** このplugin
88
+ が呼び出し元のrenderの一部として扱う関数には、独自のboundaryも
89
+ `"use no memo"` も付きません。例は、通常の関数として呼び出すPascalCaseの
90
+ helper(`Title()`)や、`items.map(Row)` のようなrender callbackです。React
91
+ Compilerはそのhelper単体をcompileすることがあります。helperのJSXに、
92
+ compilerから見える依存がない場合(module scopeのsignalの読み取りは依存に
93
+ なりません)、compilerはそのJSXをcacheし、呼び出し元のcomponentが再render
94
+ してもhelperの出力は古いままになります。helperの本体に `"use no memo"` を
95
+ 書くか、componentとして(`<Title />`)renderしてください。
96
+ - **作者が書いた `"use memo"` はそのまま残る。** すでにmemoizationの
97
+ directiveを持つ関数には、pluginは `"use no memo"` を追加しません。
98
+ `babel-plugin-react-compiler` 1.0で確認した動作は次のとおりです。
99
+ - managed変換では、compilerは生成された `try` / `finally` をcompileしません。
100
+ 既定の `panicThreshold` ではその関数をskipし、
101
+ `panicThreshold: "all_errors"` ではbuildが失敗します("Handle TryStatement
102
+ without a catch clause")。
103
+ - `transform: "inject"` では、compilerがその関数をcompileし、signalの
104
+ 読み取りをmemoizationで消してしまうことがあるため、componentが更新され
105
+ なくなることがあります。
106
+
107
+ signalを読む関数では `"use memo"` を使わないでください。
108
+
32
109
  ## その他のbundler
33
110
 
34
111
  対応するentry pointを同様の要領で使用します。
@@ -51,21 +128,22 @@ export default {
51
128
  ## オプション
52
129
 
53
130
  - `mode`
54
- - `"manual"`: 先頭文にあるimport済みの `useSignals()`、または
55
- `@useSignals` を付けた名前付きcomponent/custom hookだけを変換します。
131
+ - `"manual"`: 先頭文にあるimport済みの `useSignalTracking()`、または
132
+ `@signalTracking` を付けた名前付きcomponent/custom hookだけを変換します。
56
133
  - `"auto"`(既定): さらに `.value` を読む名前付きJSX componentと
57
134
  名前付き `useX` custom hookを変換します。
58
135
  - `"all"`: さらにすべての名前付きJSX componentを変換します。ネストした
59
136
  callbackの読み取りは親componentが収集しますが、callback自体は変換しません。
60
137
  - `transform`
61
- - `"managed"`(既定): 厳密な `try` / `finally` 境界を追加します。
138
+ - `"managed"`(既定・推奨): 厳密な `try` / `finally` 境界を追加します。
62
139
  `/runtime` からimportし、componentの関数がreturnする時点でrender
63
140
  tracking windowを同期的に閉じます。
64
- - `"inject"`: best-effortなopt-in向けに変換なしの `useSignals()` を
65
- 追加します。制御フローを書き換えず、通常の `useSignals()` を先頭hook
66
- として挿入するため、手書きと同じbest-effort境界になります。この
141
+ - `"inject"`(サポートされた高度・互換性向けmode): best-effortなopt-in向けに
142
+ 変換なしの `useSignalTracking()` を追加します。制御フローを書き換えず、通常の
143
+ `useSignalTracking()` を先頭hookとして挿入するため、手書きと同じbest-effort
144
+ 境界になります。この
67
145
  modeが露呈し得るsibling誤帰属の既知の制約については、
68
- [境界設計の検討docs](../../docs/design/use-signals-boundary-design.ja.md)
146
+ [境界設計の検討docs](../../development/design/use-signals-boundary-design.ja.md)
69
147
  を参照してください。
70
148
  - `reactCompiler`
71
149
  - `"auto"`(既定): 変換したすべての関数に `"use no memo"` を付け、render
@@ -73,9 +151,11 @@ export default {
73
151
  消さないようにします。付けない場合、compile済みcomponentはJSXをcacheし、
74
152
  最初のsignal書き込み以降、無言で更新が止まります。
75
153
  - `"off"`: directiveを付けません。React Compilerをbuildで使っていない場合か、
76
- 対象componentを[互換性の検討docs](../../docs/design/react-compiler-compatibility.ja.md)
154
+ 対象componentを[互換性の検討docs](../../development/design/react-compiler-compatibility.ja.md)
77
155
  に照らして確認済みの場合だけ選んでください。
78
156
  - `importSource`: `react-fine-grained-signals` 互換wrapperへの置き換えです。
157
+ wrapperは、生成されたコードがimportするruntimeのrootの `useSignalTracking` と、
158
+ `/runtime` entryの `useManagedSignals` をre-exportしている必要があります。
79
159
  - `reactImportSource`: wrapされた関数をcomponentと判定する際に、`memo` /
80
160
  `forwardRef` をReact由来とみなすmodule specifierを追加します。置き換えでは
81
161
  なく追加であり、`"react"` からのdirect importは常に認識されます。つまり
@@ -90,7 +170,10 @@ Rules of Hooksに反するからです。認識するのは、定義箇所にinl
90
170
  (`items.map((item) => …)`)、変数に切り出してその束縛名で参照して渡す場合
91
171
  (`const` の `const Row = …; items.map(Row)` でも、function宣言の
92
172
  `function Row() {…}` … `items.map(Row)` でも)、およびそれぞれのoptional
93
- chaining形(`items?.map(Row)`)です。こうしたcallback自体は変換せず、そのJSXと
173
+ chaining形(`items?.map(Row)`)です。`Array.from(arrayLike, mapFn)` の `mapFn`
174
+ (`Array.from({ length: 5 }, Star)`)も同じ扱いで、objectに保持したcomponentを
175
+ 分割代入してから渡す場合(`const { Row } = parts; items.map(Row)` や
176
+ `{ Row: Item }`)も同様です。こうしたcallback自体は変換せず、そのJSXと
94
177
  `.value` 読み取りは呼び出し元のcomponentが収集します。callbackが同じmodule内の
95
178
  別の場所で定義されている場合も同様で、呼び出し元componentの本体自体がsignalを
96
179
  読んでいなくても、そのcomponentが変換対象になります。
@@ -113,7 +196,7 @@ PascalCaseの名前を持っていればよく、`observer(function Row() { …
113
196
  arrow関数(`observer((props) => …)`)は、boundaryを結び付けるための名前を
114
197
  持たないため、変換されないままです。
115
198
 
116
- この検出には既知の制約が4つあります。
199
+ この検出には既知の制約が5つあります。
117
200
 
118
201
  - 再代入したaliasはたどりません。`const RowAlias = Row; items.map(RowAlias)`
119
202
  経由で渡したPascalCaseのhelperは、componentとして扱われたままになります。
@@ -122,12 +205,26 @@ arrow関数(`observer((props) => …)`)は、boundaryを結び付けるた
122
205
  hook挿入は安全ではありません)でも、`Row` を独立したcomponentとしてinstance化
123
206
  する場合(hook挿入こそが更新を成立させます)でも構文が同一で、そのファイルだけ
124
207
  では区別できません。そのため `Row` は、単独で変換条件を満たすなら自分のhookを
125
- 持ったままになります。呼び出し箇所にcallbackをinlineで書くか
126
- (`<Grid renderItem={(item) => <li>{item.value}</li>} />`。上記のinline検出が
127
- 呼び出し元componentに正しく帰属させます)、参照先の関数に `@useSignals` /
128
- `@noUseSignals` コメントで意図を明示してください。
208
+ 持ったままになります。参照先の関数に `@signalTracking` /
209
+ `@noSignalTracking` コメントで意図を明示してください。
210
+
211
+ callbackをinlineで書くこと
212
+ (`<Grid renderItem={(item) => <li>{item.value}</li>} />`)は、一般的な解決策
213
+ ではありません。その読み取りは呼び出し元componentに帰属するので呼び出し元は
214
+ 変換されますが、`Grid` は `renderItem` を *自分の* render中、つまり呼び出し元の
215
+ 関数がreturnした後に呼びます。既定のmanaged変換では、その時点で呼び出し元の
216
+ boundaryはすでに閉じているため、`Grid` 自身がboundaryを持つ場合にしか読み取りは
217
+ 追跡されず、そうでなければrenderされた項目は古いままになります。`Grid` が
218
+ 自分のcomponentなら、`@signalTracking` でopt-inしてください(または `Grid`
219
+ 自身でsignalを読んでください)。
129
220
  - 別moduleからimportしたcallbackはたどりません。変換は1ファイルずつ処理する
130
221
  ためです。
222
+ - objectのkey経由で渡すcallback(`items.map(parts.Row)`、または先に
223
+ `const { Row } = parts` とする場合)はboundaryの対象から外れますが、`auto`
224
+ modeではその `.value` 読み取りが呼び出し元の分として扱われません。呼び出し元
225
+ 自身がsignalを読んでいないと、呼び出し元は変換されず更新されません。呼び出し元で
226
+ signalを読むか、`mode: "all"` を使うか、呼び出し元に `@signalTracking` を
227
+ 付けてください。
131
228
  - 2つの役割を兼ねる関数は、除外されたままになります。`Row` をmodule内のどこかで
132
229
  `map` / `flatMap` / `forEach` に渡していて、なおかつ別の場所でJSXタグとして
133
230
  単独でrenderしている(`<Row item={x} />`)場合、render callbackとしての用法が
@@ -135,12 +232,83 @@ arrow関数(`observer((props) => …)`)は、boundaryを結び付けるた
135
232
  (hookを持たせると、callbackとしての用法でhookの順序が壊れます)。ただし単独で
136
233
  renderされるほうはsubscriptionを持たないため、以降のsignalの書き換えで
137
234
  内容が古いままになります。役割ごとに別名の2つの関数へ分けるか、単独で
138
- renderするほうの関数を `@useSignals` コメントで明示的にopt-inしてください。
235
+ renderするほうの関数を `@signalTracking` コメントで明示的にopt-inしてください。
139
236
 
140
237
  判断に迷う場合は、そうしたhelperを明示的に保ってください。小文字で `use`
141
238
  始まりでない名前にするか、実際にcomponentとしてrenderされるときにだけ手動で
142
239
  opt-inします。
143
240
 
241
+ ## 関数として呼び出されるcomponent
242
+
243
+ module内で通常の関数として呼び出されるPascalCaseの関数 — `Title()`、
244
+ `Title?.()`、`Title!()`、objectに保持されたものなら `ns.Home()`、
245
+ `ns?.group?.Home()`、または兄弟memberからの `this.Home()` — は、Reactが
246
+ 独立したcomponentとしてmountすることはなく、その本体は呼び出し元のrenderの
247
+ 中でそのまま実行されます。そのため、`all` を含むどのmodeでも独自のboundaryを
248
+ 持たず、そのJSXと `.value` 読み取りは、呼び出しを行うcomponentまたはcustom
249
+ hookが収集します。
250
+
251
+ ```jsx
252
+ function Title() { return <h1>{count.value}</h1>; }
253
+ // Outerが変換されて `count` をsubscribeし、Titleはそのまま残ります。
254
+ function Outer({ show }) { return <div>{show && Title()}</div>; }
255
+ ```
256
+
257
+ `Title` の中にboundaryを置くと、`show` がtrueの間だけ実行されるhookが
258
+ `Outer` に加わります。これはReactが拒否する条件付きのhook呼び出しです
259
+ ("Rendered more hooks than during the previous render")。event handler、
260
+ `useEffect` / `useLayoutEffect` / `useInsertionEffect` / `useCallback` /
261
+ `useMemo` のcallback、またはasync関数からの呼び出しはrenderの外で実行される
262
+ ため、その読み取りは収集しません。custom hook(`useX`)はもともと関数として
263
+ 呼び出すものなので、従来どおり独自のboundaryを持ちます。
264
+
265
+ 既知の制約は次のとおりです。
266
+
267
+ - 判定はmodule全体で行います。module内のどこか1か所でも通常の呼び出しが
268
+ あれば、その関数は独自のboundaryを失います。*さらに* `<Title />` としても
269
+ renderしている場合、そのinstanceは自分のsubscriptionを持たず、親が再render
270
+ したときにしか更新されません。この混在に対してpluginは警告を出します。
271
+ どちらか一方の形に統一してください。
272
+ - 認識するのは、その関数自身のbinding、または定義されている正確なobjectの
273
+ key pathを通した呼び出しだけで、同じmodule内に限ります。そのpathのどの
274
+ linkがoptionalでもかまいません(`ns?.group.Home()`)。同じobject literalの
275
+ memberの中では、`this` がそのpathとして扱われます。method、accessor、
276
+ `function` のproperty値、およびそれらの中に入れ子になったarrowでの
277
+ `this.Home()` が該当します。入れ子の `function` やclassは独自の `this` を
278
+ 持つため、たどりません。alias(`const T = Title; T()`)、`Title.call(…)`、
279
+ 別moduleからの呼び出しは認識しません。
280
+ - objectに保持された関数には、computed keyを通しても到達できます
281
+ (`renderers[node.type](node)`、`renderers[key]?.(arg)`、
282
+ `items.map(renderers[key])`、兄弟memberからの `this[key](arg)`)。そうしたaccessが到達しうるslot(同じ深さで、
283
+ 同じ静的なprefixの下にあるもの)には、独自のboundaryを付けません。そこに
284
+ boundaryがあると、呼び出し元の中で可変回数実行されるためです。keyは実行時に
285
+ しか決まらないので、そのslotの読み取りは呼び出し元に収集 *されません*。
286
+ そのため、自分ではsignalを読まない呼び出し元は変換されず、slotが読むsignalの
287
+ 変更では更新されません。呼び出し元でsignalを読むか、呼び出し元に
288
+ `@signalTracking` を付けてください。同じslotをmodule内で
289
+ `<renderers.User />` としてrenderしている場合も同様です。
290
+ - 読み取りを収集するのは、呼び出し元がcomponentまたは `useX` hookである場合
291
+ だけです。小文字のhelperを経由した呼び出し
292
+ (`function renderTitle() { return Title(); }`)はたどらないため、その
293
+ 読み取りは誰もsubscribeしません。componentから直接呼び出すか、
294
+ `<Title />` としてrenderしてください。
295
+ - keyで保持された関数(`ns.Home()`)を収集するのは、このmodule内で、root
296
+ に束縛されたobject literalの中に定義されている場合か、ちょうど1つのmember
297
+ 代入(`Card.Header = …`)で定義されている場合だけです。兄弟memberからの
298
+ `this.Home()` は、そのmemberが属するobject literalから収集します。
299
+ - 呼び出されていることを変換が把握できないobjectのmemberは、独自のboundaryを
300
+ 持ったままになります。parameter経由
301
+ (`function render(set, key) { return set[key](); }`)、alias経由
302
+ (`const R = renderers[key]; R(arg)`)、computedな分割代入経由
303
+ (`const { [key]: R } = renderers`)、配列内のobjectから
304
+ (`columns.map((c) => c.Cell(row))`)、別moduleから、といった到達経路が
305
+ 該当します。そうしたmemberをinlineで呼ぶと呼び出し元の中のhook呼び出しになり、
306
+ 呼び出し回数が変わった時点でクラッシュします。memberに `@noSignalTracking` を
307
+ 付けてください。memberは独自のboundaryを持たなくなり、読み取りは呼び出し元の
308
+ render中に実行されるため、変換された呼び出し元がそれを追跡します。その場合、
309
+ そのmemberを要素としてrenderすると(`<icons.Check />`)、その要素は独自の
310
+ subscriptionを持ちません。
311
+
144
312
  ## Higher-order component (HOC)
145
313
 
146
314
  higher-order component(HOC)が返すcomponentは、自分自身の名前を持たなくても
@@ -152,7 +320,7 @@ export const withCount = (Base) => (props) => <Base {...props} count={count.valu
152
320
 
153
321
  ここでcomponentは内側の関数であり、その識別子はfactory自身のbinding
154
322
  (`withCount`)から継承します。したがって `auto` modeはこれをsubscribeし、
155
- `@useSignals`(`@noUseSignals` も同様)コメントは、返される関数側とfactoryの
323
+ `@signalTracking`(`@noSignalTracking` も同様)コメントは、返される関数側とfactoryの
156
324
  宣言側のどちらに書いても適用されます。通常のclosureを巻き込まないための条件は
157
325
  3つです。囲む関数から直接returnされていること(明示的な `return`、または
158
326
  arrowの簡潔なbody)、返される関数自身がJSXをrenderしていること、そして囲む
@@ -180,7 +348,7 @@ renderするといった条件を満たす限り、他のhookとまったく同
180
348
  です。名前を継承しないのは、返されるclosureだけです。
181
349
 
182
350
  名前の継承はちょうど1段だけたどるため、factoryを返すfactory
183
- (`(a) => (b) => (props) => …`)は解決せず、そこに書いた `@useSignals` コメントは
351
+ (`(a) => (b) => (props) => …`)は解決せず、そこに書いた `@signalTracking` コメントは
184
352
  無言で消えるのではなく警告として報告されます。唯一区別できない形は、factoryの
185
353
  戻り値をそのままiteration methodへ渡す場合(`items.map(makeRow(prefix))`)です。
186
354
  これは構文上componentを返すHOCそのものなので、独自のboundaryを持ちます。その
@@ -188,17 +356,25 @@ renderするといった条件を満たす限り、他のhookとまったく同
188
356
  componentに収集させてください。
189
357
 
190
358
  componentは、自分自身のbindingを持たなくても名前を得られる場合があります。
191
- objectやclassのproperty — `Card.Header = () => <p>{count.value}</p>` や、
192
- `class Holder { Row = () => <p>{count.value}</p> }` のようなfield — として
193
- 保持されているcomponentは、`<Card.Header />` や `<ns.Row />` がそこへ到達する
194
- のと同じ、そのkeyから名前を得ます(小文字のkeyは、小文字のbindingと同様に
195
- 除外されたままです)。また、名前を持たないdefault export
359
+ objectのproperty — `Card.Header = () => <p>{count.value}</p>` など — として
360
+ 保持されているcomponent、またはobject literalのmethodとして書かれた
361
+ component(`export const ns = { Home() { return <p>{count.value}</p>; } }`)は、
362
+ `<Card.Header />` や `<ns.Row />` がそこへ到達するのと同じ、そのkeyから名前を
363
+ 得ます(小文字のkeyは、小文字のbindingと同様に除外されたままです。getter、
364
+ setter、async method、generator methodも除外され、instance経由でしか到達
365
+ できないclassのmethodも対象外です)。また、名前を持たないdefault export
196
366
  (`export default (props) => <p>{count.value}</p>`)は、moduleのファイル名から
197
367
  名前を得ます。これは `import App from "./App"` が実際にそのcomponentへ与えて
198
- いる識別子そのものです。ただし、class内での `this.Row = …` という代入
199
- (たとえばconstructor内)は意図的に対象外です。そうした `this` を束縛する
200
- rendererには、この機能が存在する以前と同様、明示的な `useSignals()` 呼び出し
201
- か `@useSignals` コメントが必要です。
368
+ いる識別子そのものです。ただし、classが保持する関数は意図的に対象外です。class field
369
+ (`class Holder { Row = () => … }`。instanceでも `static` でも)と、class内での
370
+ `this.Row = …` という代入(たとえばconstructor内)は変換されず、そこに付けた
371
+ `@signalTracking` コメントは警告を出したうえで無視されます。これらは
372
+ `this.Row` や `Holder.Row` 経由で到達するため、変換はそれがcomponentなのか、
373
+ `render()` が関数として直接呼ぶもの(`this.Header()`)やitemごとに渡すもの
374
+ (`items.map(this.Row)`)なのかを追跡できません。後者にhookのboundaryを入れると
375
+ class componentのrender中でhookが動き、Reactに拒否されます。要素として
376
+ (`<this.Row />`)だけrenderされる関数なら、その中で `useSignalTracking()` を
377
+ 自分で呼べます。そうでなければ、signalはfunction componentで読んでください。
202
378
 
203
379
  ## `memo` / `forwardRef` の認識
204
380
 
@@ -214,12 +390,12 @@ re-exportの連鎖をたどらないためです。認識できなかった場
214
390
  specifierを設定する(`"@app/react"` のようなbare specifierはどのファイルでも
215
391
  一致します。相対pathは同じ綴りのファイルにしか一致しません)。
216
392
  - 該当箇所で `memo` / `forwardRef` を `"react"` から直接importする。
217
- - `@useSignals` コメントか手書きの `useSignals()` 呼び出しで明示的にopt-inする。
393
+ - `@signalTracking` コメントか手書きの `useSignalTracking()` 呼び出しで明示的にopt-inする。
218
394
 
219
- ## `useSignals()` のopt-inと書き換え
395
+ ## `useSignalTracking()` のopt-inと書き換え
220
396
 
221
- `@useSignals` と `@noUseSignals` は、その関数だけに適用されます。pluginが
222
- 二重の `useSignals()` 呼び出しを挿入することはありません。既存のdirect
397
+ `@signalTracking` と `@noSignalTracking` は、その関数だけに適用されます。pluginが
398
+ 二重の `useSignalTracking()` 呼び出しを挿入することはありません。既存のdirect
223
399
  import、namespace import、barrel import経由の呼び出しは、その関数のopt-inと
224
400
  みなします。先頭文でない呼び出しと、barrel import経由の呼び出しは、いずれの
225
401
  変換モードでもそのまま残ります。`importSource` からdirect importまたは
@@ -228,22 +404,70 @@ namespace importした先頭文の呼び出しは、`"inject"` ではそのま
228
404
  managed storeの宣言と `try` / `finally` scopeに置き換わるため、関数本体は
229
405
  そのまま残るのではなく書き換えられます。
230
406
 
231
- 先頭文での `useSignals()` 呼び出しが `async` 関数やgenerator関数の中にあると、
407
+ 先頭文での `useSignalTracking()` 呼び出しが `async` 関数やgenerator関数の中にあると、
232
408
  既定の `"managed"` 変換ではbuildが失敗し、
233
- `useSignals transform only supports synchronous, non-generator functions`
409
+ `useSignalTracking transform only supports synchronous, non-generator functions`
234
410
  というエラーになります。この組み合わせはそもそもReactとして不正です
235
411
  (hookは同期のfunction componentを必要とします)。関数側を直すのが望ましく、
236
412
  どうしても現状のままbuildを通す必要がある場合は `transform: "inject"` を
237
413
  使うと、関数を書き換えずにこの呼び出しを受け入れます。
238
414
 
415
+ 引数のdefault値は関数本体より前に評価されるため、そこでのsignal読み取り
416
+ (`function Label({ v = count.value })`)はboundaryが開く前に行われ、追跡
417
+ されません。変換対象の関数にそうした引数がある場合、その引数と、評価順を
418
+ 保つためにそれ以降のすべての引数を生成した引数に置き換え、boundaryの中の
419
+ 最初の文として同じ `undefined` のときだけのdefault規則で束縛し直します。
420
+ 引数の個数は変わらないため、`forwardRef` の `(props, ref)` という形は保たれ
421
+ ます。移動するdefault値が参照する名前を本体が宣言している場合や、移動する
422
+ 引数を本体が再宣言している場合は移動を行わず、その読み取りは追跡されない
423
+ ままです。その場合はsignalを本体で読んでください。
424
+
425
+ この移動では、default値の規則と評価順は変わりません。TypeScriptでは、生成
426
+ した引数が元の型とoptionalかどうかを引き継ぐため、出力は有効なTypeScriptの
427
+ ままです。ただし、関数の `length` は変わることがあります。default値を持つ
428
+ 引数は `length` に数えられませんが、それを置き換えた生成引数は数えられる
429
+ ためです。たとえば `function useLabel(prefix?: string, value = count.value)`
430
+ の `length` は、sourceでは1、変換後は2です。変換された関数の `length` には
431
+ 依存しないでください。
432
+
239
433
  ## Buildへの組み込み
240
434
 
241
- いずれの変換モードも再適用するとno-opです。この変換は、対応するbundler
242
- (Vite、webpack、Rspack)では `enforce: "pre"` によって他のplugin変換より
243
- 先に実行されます。Rollupにはこの概念がないため、このpluginを最初にlistして
244
- ください。また、依存関係やJavaScript/TypeScript以外のmoduleはskipします。
245
- `.ts` はJSXなしのTypeScriptとして、`.tsx`、`.jsx`、JavaScriptはJSXを含めて
246
- 解析します。
435
+ いずれの変換モードも再適用するとno-opです。pluginは、対応するbundler
436
+ (Vite、webpack、Rspack)では `enforce: "pre"` で登録され、Viteではさらに
437
+ transform hookが `order: "pre"` を宣言します
438
+ ([React Compilerと併用する場合](#react-compilerと併用する場合)を参照)。
439
+ Rollupにはどちらの概念もないため、このpluginを最初にlistしてください。
440
+ pluginやloaderの順序を自分で決められる場合は、React Compilerを実行するものや
441
+ JSXを変換して消すものより前にこのpluginを置いてください。依存関係と
442
+ JavaScript/TypeScript以外のmoduleはskipし、`.cjs` / `.cts` のmoduleには
443
+ 一切手を加えません。pluginは拡張子を大文字・小文字を区別せずに判定するため、
444
+ `App.TSX` を `App.tsx` とまったく同様に変換します。ただし、その後bundlerが
445
+ そのファイルを同じようにcompileするかはbundler次第で、保証はありません。
446
+ Vite 8は `.JSX` / `.TSX` をJSX / TSXとしてcompileせず、このpluginの有無に
447
+ かかわらず解析に失敗します。`.ts` はJSXなしの
448
+ TypeScriptとして、`.tsx`、`.jsx`、JavaScriptはJSXを含めて解析します。
449
+
450
+ ## Fast Refresh
451
+
452
+ この制約はv0.1.1から存在します。既定のmanaged変換とViteのOxcベースのFast
453
+ Refreshの組み合わせでは、変換されたcomponentのhookを変更する(hookを追加、
454
+ 削除、並べ替える)と、Fast Refreshのstateは保持されません。refreshは
455
+ "Rendered more hooks than during the previous render"(または "fewer")で
456
+ 失敗し、pageの完全なreloadが必要になります。managed変換のboundaryは
457
+ componentのhookを `try` blockの中に置くため、Fast Refreshはそれらのhookを
458
+ componentのhook signatureに含めません。検証したfixtureでは、
459
+ `transform: "inject"` にこの制約はありませんでした。
460
+
461
+ ## React Server Components
462
+
463
+ この変換はserver moduleとclient moduleを区別しません。`"use client"` /
464
+ `"use server"` directiveも、bundlerのenvironmentやlayerも参照しないためです。
465
+ したがって `.value` を読む同期のserver componentには、client専用のrender
466
+ tracking hookが挿入され、serverでrenderした時点で失敗します(async関数は
467
+ 変換しないため、asyncのserver componentは変換されません)。RSC構成では、
468
+ `exclude`(たとえばdirectory単位)でserver componentを変換対象から外すか、
469
+ 個別に `@noSignalTracking` コメントでopt-outするか、`mode: "manual"` を
470
+ 使ってください。
247
471
 
248
472
  ## License
249
473