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 +265 -41
- package/README.md +266 -41
- package/dist/esbuild.d.ts +3 -3
- package/dist/esbuild.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loader.d.ts +2 -3
- package/dist/rollup.d.ts +3 -3
- package/dist/rollup.js +1 -1
- package/dist/rspack.d.ts +3 -3
- package/dist/rspack.js +1 -1
- package/dist/{unplugin-CG9_mdcA.d.ts → unplugin-B9gaexDa.d.ts} +3 -3
- package/dist/{unplugin-B2_q8KNi.js → unplugin-CPDTNSYq.js} +775 -112
- package/dist/vite.d.ts +14 -5
- package/dist/vite.js +1 -1
- package/dist/webpack.d.ts +3 -3
- package/dist/webpack.js +1 -1
- package/package.json +25 -16
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [日本語](README.ja.md)
|
|
4
4
|
|
|
5
|
-
Universal bundler integration for automatic `
|
|
5
|
+
Universal bundler integration for automatic `useSignalTracking()` insertion and the
|
|
6
6
|
optional managed render-scope transform in
|
|
7
7
|
[`react-fine-grained-signals`](https://www.npmjs.com/package/react-fine-grained-signals).
|
|
8
8
|
|
|
@@ -16,8 +16,25 @@ supported bundlers.
|
|
|
16
16
|
pnpm add -D unplugin-react-fine-grained-signals
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
`react-fine-grained-signals` is a peer dependency.
|
|
20
|
-
|
|
19
|
+
`react-fine-grained-signals` is a peer dependency. Node.js `^22.18.0 || >=24.11.0`
|
|
20
|
+
is required — the range Babel 8, which the transform runs on, declares.
|
|
21
|
+
|
|
22
|
+
Use the plugin and the runtime from the same release line (v0.2 with v0.2): the
|
|
23
|
+
generated code imports runtime entry points that change between lines. When
|
|
24
|
+
upgrading from v0.1, upgrade both packages together and rename the
|
|
25
|
+
`@useSignals` / `@noUseSignals` comments — see the
|
|
26
|
+
[v0.2 migration guide](https://github.com/sakzw/react-fine-grained-signals/blob/main/docs/migration/v0.2.md#transform-plugin).
|
|
27
|
+
|
|
28
|
+
This package ships ESM only; there is no CommonJS build. An ESM build
|
|
29
|
+
configuration that uses `import` is the primary setup, but a CommonJS
|
|
30
|
+
configuration file (a `webpack.config.js` or `next.config.js` in a project
|
|
31
|
+
without `"type": "module"`) can still load it with `require()`: every Node.js
|
|
32
|
+
version in the range above loads the ESM entry points through its built-in
|
|
33
|
+
`require(esm)` support. The plugin factory is the `default` export:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const signals = require("unplugin-react-fine-grained-signals/webpack").default;
|
|
37
|
+
```
|
|
21
38
|
|
|
22
39
|
## Vite
|
|
23
40
|
|
|
@@ -30,6 +47,65 @@ export default defineConfig({
|
|
|
30
47
|
});
|
|
31
48
|
```
|
|
32
49
|
|
|
50
|
+
### With React Compiler
|
|
51
|
+
|
|
52
|
+
React Compiler must run *after* this transform: it would otherwise memoize away
|
|
53
|
+
the `.value` reads before this plugin could mark the component `"use no memo"`,
|
|
54
|
+
and the component would silently stop updating after its first render. List
|
|
55
|
+
`signals()` before the plugin that runs the compiler:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import react from "@vitejs/plugin-react";
|
|
59
|
+
import { defineConfig } from "vite";
|
|
60
|
+
import signals from "unplugin-react-fine-grained-signals/vite";
|
|
61
|
+
|
|
62
|
+
export default defineConfig({
|
|
63
|
+
plugins: [signals({ mode: "auto" }), react({ compiler: true })],
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In the setup this was tested with (Vite 8, `@vitejs/plugin-react` 6.1),
|
|
68
|
+
`react({ compiler: true })` also needs plugin-react's optional
|
|
69
|
+
`oxc-transform-react` package. Install it next to `babel-plugin-react-compiler`;
|
|
70
|
+
without it the build stops with plugin-react's own "requires the optional
|
|
71
|
+
`oxc-transform-react` package" error.
|
|
72
|
+
|
|
73
|
+
Under Vite the order is also defended at the hook level. The plugin is
|
|
74
|
+
`enforce: "pre"`, and its `transform` hook additionally declares
|
|
75
|
+
`order: "pre"`, which Vite sorts on within the pre-enforced group. That makes it
|
|
76
|
+
run ahead of `@vitejs/plugin-react` 6's compiler step (`react({ compiler: true })`,
|
|
77
|
+
itself `enforce: "pre"` with no hook order) even when `react()` is listed first —
|
|
78
|
+
before this, that order compiled the component first. This is Vite's ordering,
|
|
79
|
+
not an absolute guarantee: a plugin whose transform also declares
|
|
80
|
+
`order: "pre"` and is listed earlier still runs first. Keeping `signals()`
|
|
81
|
+
first is therefore still the recommended configuration, and it is required
|
|
82
|
+
wherever hook ordering does not apply: Rollup orders by array position alone,
|
|
83
|
+
and the Babel route (`@rolldown/plugin-babel` with `reactCompilerPreset()`) has
|
|
84
|
+
not been verified against this plugin.
|
|
85
|
+
|
|
86
|
+
Two known limitations apply with React Compiler. Both were present in v0.1.1.
|
|
87
|
+
|
|
88
|
+
- **Inline render helpers can be memoized by the compiler.** A function this
|
|
89
|
+
plugin treats as part of its caller's render gets no boundary and no
|
|
90
|
+
`"use no memo"` of its own. Examples are a PascalCase helper called as a
|
|
91
|
+
plain function (`Title()`) and a render callback such as `items.map(Row)`.
|
|
92
|
+
React Compiler can still compile that helper by itself. If the helper's JSX
|
|
93
|
+
has no dependency the compiler can see (a module-level signal read is not
|
|
94
|
+
one), the compiler caches the JSX, and the helper's output stays stale even
|
|
95
|
+
though the calling component re-renders. Put `"use no memo"` in the helper's
|
|
96
|
+
body, or render it as a component (`<Title />`) instead.
|
|
97
|
+
- **An author-written `"use memo"` is kept.** The plugin does not add
|
|
98
|
+
`"use no memo"` to a function that already states a memoization directive.
|
|
99
|
+
Tested with `babel-plugin-react-compiler` 1.0:
|
|
100
|
+
- With the managed transform, the compiler does not compile the generated
|
|
101
|
+
`try` / `finally`. With the default `panicThreshold` it skips the function.
|
|
102
|
+
With `panicThreshold: "all_errors"` the build fails ("Handle TryStatement
|
|
103
|
+
without a catch clause").
|
|
104
|
+
- With `transform: "inject"`, the compiler compiles the function and can
|
|
105
|
+
memoize away its signal reads, so the component can stop updating.
|
|
106
|
+
|
|
107
|
+
Do not use `"use memo"` in functions that read signals.
|
|
108
|
+
|
|
33
109
|
## Other bundlers
|
|
34
110
|
|
|
35
111
|
Use the matching entry point in the same way:
|
|
@@ -52,21 +128,22 @@ export default {
|
|
|
52
128
|
## Options
|
|
53
129
|
|
|
54
130
|
- `mode`:
|
|
55
|
-
- `"manual"`: only an explicit first-statement imported `
|
|
56
|
-
or a `@
|
|
131
|
+
- `"manual"`: only an explicit first-statement imported `useSignalTracking()` call
|
|
132
|
+
or a `@signalTracking` comment on a named component/custom hook opts it in.
|
|
57
133
|
- `"auto"` (default): additionally transforms named JSX components and
|
|
58
134
|
named `useX` custom hooks that read `.value`.
|
|
59
135
|
- `"all"`: additionally transforms every named JSX component. Nested
|
|
60
136
|
callbacks are collected by their owning component but are never transform
|
|
61
137
|
targets themselves.
|
|
62
138
|
- `transform`:
|
|
63
|
-
- `"managed"` (default): adds an exact `try` / `finally` boundary, importing
|
|
139
|
+
- `"managed"` (default and recommended): adds an exact `try` / `finally` boundary, importing
|
|
64
140
|
from the package's `/runtime` entry and closing the render-tracking
|
|
65
141
|
window synchronously at the point the component function returns.
|
|
66
|
-
- `"inject"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
142
|
+
- `"inject"` (supported advanced/compatibility mode): adds bare
|
|
143
|
+
`useSignalTracking()` for best-effort opt-in. It inserts a normal
|
|
144
|
+
`useSignalTracking()` call without rewriting control flow, so it has the same
|
|
145
|
+
best-effort tracking boundary as a handwritten call — see
|
|
146
|
+
[the boundary design investigation](../../development/design/use-signals-boundary-design.md)
|
|
70
147
|
for the known sibling-misattribution limitation this mode can expose.
|
|
71
148
|
- `reactCompiler`:
|
|
72
149
|
- `"auto"` (default): marks every transformed function with `"use no memo"`,
|
|
@@ -75,8 +152,10 @@ export default {
|
|
|
75
152
|
stops updating after the first signal write.
|
|
76
153
|
- `"off"`: omits the directive. Choose it only when React Compiler is not in
|
|
77
154
|
the build, or when the affected components were verified against
|
|
78
|
-
[the compatibility note](../../
|
|
155
|
+
[the compatibility note](../../development/design/react-compiler-compatibility.md).
|
|
79
156
|
- `importSource`: overrides `react-fine-grained-signals` for a compatible wrapper.
|
|
157
|
+
The wrapper must re-export the runtime's root `useSignalTracking` and its
|
|
158
|
+
`/runtime` entry's `useManagedSignals`, which the generated code imports.
|
|
80
159
|
- `reactImportSource`: an additional module specifier whose `memo` and
|
|
81
160
|
`forwardRef` exports count as React's own when the plugin decides whether a
|
|
82
161
|
wrapped function is a component. Recognition is additive, not a replacement:
|
|
@@ -93,7 +172,11 @@ of Hooks forbid. Recognition covers the inline definition site
|
|
|
93
172
|
(`items.map((item) => …)`), a callback factored out and referenced by its own
|
|
94
173
|
binding, whether that binding is a `const` (`const Row = …; items.map(Row)`) or
|
|
95
174
|
a function declaration (`function Row() {…}` … `items.map(Row)`), and the
|
|
96
|
-
optional-chained form of either (`items?.map(Row)`).
|
|
175
|
+
optional-chained form of either (`items?.map(Row)`). The `mapFn` of
|
|
176
|
+
`Array.from(arrayLike, mapFn)` (`Array.from({ length: 5 }, Star)`) counts the
|
|
177
|
+
same way, and so does a component held in an object that is destructured before
|
|
178
|
+
being passed (`const { Row } = parts; items.map(Row)`, or `{ Row: Item }`).
|
|
179
|
+
Such a callback is left
|
|
97
180
|
alone, and its JSX and `.value` reads are collected by the component that
|
|
98
181
|
invokes it — including when the callback is defined elsewhere in the same
|
|
99
182
|
module, so that component is transformed even when its own body reads no
|
|
@@ -119,7 +202,7 @@ An anonymous or arrow function passed the same way —
|
|
|
119
202
|
`observer((props) => …)` — carries no name for a boundary to attach to, and is
|
|
120
203
|
left untransformed.
|
|
121
204
|
|
|
122
|
-
This detection has
|
|
205
|
+
This detection has five known limitations:
|
|
123
206
|
|
|
124
207
|
- A re-assigned alias is not followed, so a PascalCase helper reached through
|
|
125
208
|
`const RowAlias = Row; items.map(RowAlias)` is still treated as a component.
|
|
@@ -129,13 +212,24 @@ This detection has four known limitations:
|
|
|
129
212
|
where hook injection is unsafe) or instantiates `Row` as its own component
|
|
130
213
|
(where hook injection is what makes it update), and nothing in the file says
|
|
131
214
|
which — so `Row` keeps its own hook whenever it is independently eligible.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
215
|
+
State the intent explicitly on the referenced function with a
|
|
216
|
+
`@signalTracking` / `@noSignalTracking` comment.
|
|
217
|
+
|
|
218
|
+
Writing the callback inline (`<Grid renderItem={(item) => <li>{item.value}</li>} />`)
|
|
219
|
+
is not a general fix. Its reads are attributed to the owning component, so
|
|
220
|
+
the owner is transformed, but `Grid` calls `renderItem` during its *own*
|
|
221
|
+
render, after the owner's function has returned. With the default managed
|
|
222
|
+
transform the owner's boundary is already closed by then, so the reads are
|
|
223
|
+
tracked only if `Grid` has a boundary of its own; otherwise the rendered items
|
|
224
|
+
stay stale. If `Grid` is your own component, opt it in with
|
|
225
|
+
`@signalTracking` (or have it read the signal itself).
|
|
137
226
|
- A callback imported from another module is not followed, because the
|
|
138
227
|
transform sees one file at a time.
|
|
228
|
+
- A callback reached through an object key (`items.map(parts.Row)`, or
|
|
229
|
+
`const { Row } = parts` first) is kept off a boundary, but in `auto` mode its
|
|
230
|
+
`.value` reads are not credited to the caller. A caller that reads no signal
|
|
231
|
+
itself is then left untransformed and does not update; read the signal in the
|
|
232
|
+
caller, use `mode: "all"`, or add `@signalTracking` to the caller.
|
|
139
233
|
- A function used in both roles keeps the exclusion. If `Row` is passed to
|
|
140
234
|
`map` / `flatMap` / `forEach` anywhere in the module and is *also* rendered
|
|
141
235
|
independently as a JSX tag (`<Row item={x} />`), the render-callback usage
|
|
@@ -144,12 +238,84 @@ This detection has four known limitations:
|
|
|
144
238
|
independently rendered instance then has no subscription and goes stale on
|
|
145
239
|
later signal writes. Split the two roles into two differently named
|
|
146
240
|
functions, one per role, or opt the independently rendered function in
|
|
147
|
-
explicitly with a `@
|
|
241
|
+
explicitly with a `@signalTracking` comment.
|
|
148
242
|
|
|
149
243
|
When in doubt, keep such helpers explicit: name them lowercase and without a
|
|
150
244
|
`use` prefix, or opt them in manually only when they are genuinely rendered as
|
|
151
245
|
components.
|
|
152
246
|
|
|
247
|
+
## Components called as plain functions
|
|
248
|
+
|
|
249
|
+
A PascalCase function that the module calls as a plain function — `Title()`,
|
|
250
|
+
`Title?.()`, `Title!()`, or, for one held in an object, `ns.Home()`,
|
|
251
|
+
`ns?.group?.Home()`, or `this.Home()` from a sibling member — is never
|
|
252
|
+
mounted by React as a component of its own: its body simply runs inside the
|
|
253
|
+
caller's render. It therefore never gets a boundary of its own, in any mode
|
|
254
|
+
including `all`, and its JSX and `.value` reads are collected by the component
|
|
255
|
+
or custom hook that makes the call:
|
|
256
|
+
|
|
257
|
+
```jsx
|
|
258
|
+
function Title() { return <h1>{count.value}</h1>; }
|
|
259
|
+
// Outer is transformed and subscribes to `count`; Title is left as it is.
|
|
260
|
+
function Outer({ show }) { return <div>{show && Title()}</div>; }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A boundary inside `Title` would add hooks to `Outer` that run only while `show`
|
|
264
|
+
is true — a conditional hook call that React rejects ("Rendered more hooks than
|
|
265
|
+
during the previous render"). A call made from an event handler, from the
|
|
266
|
+
callback of `useEffect` / `useLayoutEffect` / `useInsertionEffect` /
|
|
267
|
+
`useCallback` / `useMemo`, or from an async function runs outside the render,
|
|
268
|
+
so its reads are not collected. Custom hooks (`useX`) are called as functions
|
|
269
|
+
by design and keep their own boundary exactly as before.
|
|
270
|
+
|
|
271
|
+
Known limitations:
|
|
272
|
+
|
|
273
|
+
- The decision is module-wide: a single plain call anywhere in the module takes
|
|
274
|
+
away the function's own boundary. If it is *also* rendered as `<Title />`,
|
|
275
|
+
that instance has no subscription of its own and only refreshes when its
|
|
276
|
+
parent re-renders; the plugin prints a warning for this mix. Use one form
|
|
277
|
+
consistently.
|
|
278
|
+
- Only calls through the function's own binding, or through the exact object
|
|
279
|
+
key path it is defined at, are recognized, and only within the module. Any
|
|
280
|
+
link of that path may be optional (`ns?.group.Home()`). Inside a member of
|
|
281
|
+
the same object literal, `this` counts as that path: `this.Home()` in a
|
|
282
|
+
method, an accessor, a `function` property value, or an arrow nested in one
|
|
283
|
+
of those. A nested `function` or class has a `this` of its own and is not
|
|
284
|
+
followed. An alias (`const T = Title; T()`), `Title.call(…)`, and calls from
|
|
285
|
+
other modules are not recognized.
|
|
286
|
+
- A function held in an object can also be reached through a computed key:
|
|
287
|
+
`renderers[node.type](node)`, `renderers[key]?.(arg)`,
|
|
288
|
+
`items.map(renderers[key])`, or `this[key](arg)` from a sibling member.
|
|
289
|
+
Every slot that such an access can reach (the
|
|
290
|
+
same depth, under the same static prefix) gets no boundary of its own,
|
|
291
|
+
because a boundary there would run a variable number of times inside the
|
|
292
|
+
caller. Its reads are *not* collected by the caller, because the key is only
|
|
293
|
+
known at runtime. So a caller that reads no signal itself is not transformed
|
|
294
|
+
and does not update on the slot's signals. Read the signal in the caller, or
|
|
295
|
+
add `@signalTracking` to the caller. This also applies when the module
|
|
296
|
+
renders the same slot as `<renderers.User />`.
|
|
297
|
+
- The reads are collected only when the caller is itself a component or a
|
|
298
|
+
`useX` hook. A call routed through a lowercase helper
|
|
299
|
+
(`function renderTitle() { return Title(); }`) is not followed, so nothing
|
|
300
|
+
subscribes to those reads; call the function directly from the component, or
|
|
301
|
+
render it as `<Title />`.
|
|
302
|
+
- A keyed function (`ns.Home()`) is collected only when this module defines it
|
|
303
|
+
in an object literal bound to the root, or with exactly one member assignment
|
|
304
|
+
(`Card.Header = …`). A sibling's `this.Home()` is collected from the object
|
|
305
|
+
literal that member belongs to.
|
|
306
|
+
- An object member the transform cannot see being called keeps its own
|
|
307
|
+
boundary. That covers a member reached through a parameter
|
|
308
|
+
(`function render(set, key) { return set[key](); }`), through an alias
|
|
309
|
+
(`const R = renderers[key]; R(arg)`), through computed destructuring
|
|
310
|
+
(`const { [key]: R } = renderers`), from an object inside an array
|
|
311
|
+
(`columns.map((c) => c.Cell(row))`), or from another module. Calling such a
|
|
312
|
+
member inline is then a hook call inside the caller, which crashes as soon
|
|
313
|
+
as the number of calls changes. Put `@noSignalTracking` on the member: it
|
|
314
|
+
then has no boundary of its own, and a transformed caller tracks its reads
|
|
315
|
+
because they run during the caller's render. Rendering that member as an
|
|
316
|
+
element (`<icons.Check />`) then gives the element no subscription of its
|
|
317
|
+
own.
|
|
318
|
+
|
|
153
319
|
## Higher-order components
|
|
154
320
|
|
|
155
321
|
A component a higher-order component returns is recognized even though it has no
|
|
@@ -161,7 +327,7 @@ export const withCount = (Base) => (props) => <Base {...props} count={count.valu
|
|
|
161
327
|
|
|
162
328
|
The inner function is the component here, and it inherits its identity from the
|
|
163
329
|
factory's own binding — `withCount` — so `auto` mode subscribes it and a
|
|
164
|
-
`@
|
|
330
|
+
`@signalTracking` (or `@noSignalTracking`) comment on either the returned function or the
|
|
165
331
|
factory's declaration applies to it. Three conditions keep ordinary closures out:
|
|
166
332
|
the function must be returned directly by the enclosing function (an explicit
|
|
167
333
|
`return`, or an arrow's concise body); the returned function must render JSX
|
|
@@ -189,7 +355,7 @@ normally whenever it qualifies — reading a signal, rendering JSX — exactly l
|
|
|
189
355
|
any other hook; only the returned closure inherits no name from it.
|
|
190
356
|
|
|
191
357
|
The name is inherited from exactly one level out, so a factory returning a
|
|
192
|
-
factory (`(a) => (b) => (props) => …`) resolves to nothing, and a `@
|
|
358
|
+
factory (`(a) => (b) => (props) => …`) resolves to nothing, and a `@signalTracking`
|
|
193
359
|
comment there reports that rather than silently doing nothing. The one shape
|
|
194
360
|
this cannot tell apart is a factory whose result is handed straight to an
|
|
195
361
|
iteration method (`items.map(makeRow(prefix))`): that is syntactically a HOC
|
|
@@ -198,15 +364,24 @@ reference (`items.map(Row)`) or write it inline, so the owning component
|
|
|
198
364
|
collects it.
|
|
199
365
|
|
|
200
366
|
A component can also carry no binding of its own and still be named: one held
|
|
201
|
-
as an object
|
|
202
|
-
|
|
203
|
-
key, the same way `<Card.Header />` or `<ns.Row />` reaches it
|
|
204
|
-
stays excluded, exactly as a lowercase binding does
|
|
367
|
+
as an object property — `Card.Header = () => <p>{count.value}</p>` — or written as an
|
|
368
|
+
object-literal method (`export const ns = { Home() { return <p>{count.value}</p>; } }`)
|
|
369
|
+
is named by its key, the same way `<Card.Header />` or `<ns.Row />` reaches it
|
|
370
|
+
(a lowercase key stays excluded, exactly as a lowercase binding does; getters,
|
|
371
|
+
setters, async and generator methods are excluded, and so are class methods,
|
|
372
|
+
which only an instance reaches), and a nameless default
|
|
205
373
|
export (`export default (props) => <p>{count.value}</p>`) is named after the
|
|
206
374
|
module's own file — the identity an `import App from "./App"` already gives it.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
`@
|
|
375
|
+
Functions held by a class are deliberately excluded, though: a class field
|
|
376
|
+
(`class Holder { Row = () => … }`, instance or `static`) and a `this.Row = …`
|
|
377
|
+
assignment are never transformed, and a `@signalTracking` comment on one is
|
|
378
|
+
reported and ignored. They are reached through `this.Row` or `Holder.Row`, which
|
|
379
|
+
the transform cannot follow to tell a component from a function that
|
|
380
|
+
`render()` calls directly (`this.Header()`) or passes per item
|
|
381
|
+
(`items.map(this.Row)`) — and a hook boundary in either of those runs inside
|
|
382
|
+
the class component's render, which React rejects. If such a function is only
|
|
383
|
+
ever rendered as an element (`<this.Row />`), it may call `useSignalTracking()`
|
|
384
|
+
itself; otherwise read signals in a function component instead.
|
|
210
385
|
|
|
211
386
|
## `memo` / `forwardRef` recognition
|
|
212
387
|
|
|
@@ -224,13 +399,13 @@ skipped, so signal writes stop re-rendering it. Three workarounds:
|
|
|
224
399
|
way);
|
|
225
400
|
- import `memo` / `forwardRef` directly from `"react"` at the affected call
|
|
226
401
|
sites;
|
|
227
|
-
- opt the component in explicitly with a `@
|
|
228
|
-
`
|
|
402
|
+
- opt the component in explicitly with a `@signalTracking` comment or a manual
|
|
403
|
+
`useSignalTracking()` call.
|
|
229
404
|
|
|
230
|
-
## `
|
|
405
|
+
## `useSignalTracking()` opt-in and rewriting
|
|
231
406
|
|
|
232
|
-
`@
|
|
233
|
-
not affect nested functions. The plugin never adds a second `
|
|
407
|
+
`@signalTracking` and `@noSignalTracking` apply only to their owning function; they do
|
|
408
|
+
not affect nested functions. The plugin never adds a second `useSignalTracking()`
|
|
234
409
|
call: a direct, namespace, or barrel-imported call it finds is treated as the
|
|
235
410
|
function's existing opt-in. A call that is not the function's first statement,
|
|
236
411
|
and any barrel-imported call, is left alone in both transform modes. A
|
|
@@ -240,21 +415,71 @@ absorbed into the generated boundary: the statement is removed and replaced by
|
|
|
240
415
|
the managed store declaration plus the `try` / `finally` scope, so the function
|
|
241
416
|
body is rewritten rather than left byte-for-byte untouched.
|
|
242
417
|
|
|
243
|
-
A first-statement `
|
|
418
|
+
A first-statement `useSignalTracking()` call inside an `async` or generator function
|
|
244
419
|
fails the build under the default `"managed"` transform, with
|
|
245
|
-
`
|
|
420
|
+
`useSignalTracking transform only supports synchronous, non-generator functions`. That
|
|
246
421
|
combination is already invalid React — hooks require a synchronous function
|
|
247
422
|
component — so prefer fixing the function; `transform: "inject"` accepts it
|
|
248
423
|
without rewriting the function, if the file must keep building unchanged.
|
|
249
424
|
|
|
425
|
+
Parameter defaults are evaluated before the function body runs, so a signal
|
|
426
|
+
read in one — `function Label({ v = count.value })` — would happen before the
|
|
427
|
+
boundary opens and never be tracked. When a transformed function has such a
|
|
428
|
+
parameter, that parameter and every parameter after it (to keep their
|
|
429
|
+
evaluation order) are replaced by generated ones and bound again as the first
|
|
430
|
+
statements inside the boundary, with the same `undefined`-only default rule.
|
|
431
|
+
The parameter count is unchanged, so `forwardRef`'s `(props, ref)` shape is
|
|
432
|
+
kept. If the body declares a name a moved default refers to, or re-declares a
|
|
433
|
+
moved parameter, the move is skipped and that read stays untracked; read the
|
|
434
|
+
signal in the body instead.
|
|
435
|
+
|
|
436
|
+
The move keeps the default rule and the evaluation order. In TypeScript the
|
|
437
|
+
generated parameter keeps the original's type and optionality, so the output
|
|
438
|
+
stays valid TypeScript. The function's `length` can change, though. A
|
|
439
|
+
parameter with a default does not count toward `length`, but the generated
|
|
440
|
+
parameter that replaces it does. For example,
|
|
441
|
+
`function useLabel(prefix?: string, value = count.value)` has a `length` of 1
|
|
442
|
+
in the source and 2 after the transform. Do not rely on the `length` of a
|
|
443
|
+
transformed function.
|
|
444
|
+
|
|
250
445
|
## Build integration
|
|
251
446
|
|
|
252
|
-
Reapplying either transform mode is a no-op. The
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
447
|
+
Reapplying either transform mode is a no-op. The plugin is registered with
|
|
448
|
+
`enforce: "pre"` on bundlers that support it (Vite, webpack, Rspack), and under
|
|
449
|
+
Vite its transform hook also declares `order: "pre"` (see
|
|
450
|
+
[With React Compiler](#with-react-compiler)). Rollup has neither concept, so
|
|
451
|
+
list this plugin first there. Wherever the order of plugins or loaders is in
|
|
452
|
+
your hands, put this plugin before anything that runs React Compiler or
|
|
453
|
+
compiles JSX away. The transform skips dependencies and
|
|
454
|
+
non-JavaScript/TypeScript modules, and never touches `.cjs` / `.cts` modules.
|
|
455
|
+
The plugin matches extensions case-insensitively, so it transforms `App.TSX`
|
|
456
|
+
exactly like `App.tsx`. Whether the bundler then compiles such a file the same
|
|
457
|
+
way is up to the bundler, and is not guaranteed: Vite 8 does not compile
|
|
458
|
+
`.JSX` / `.TSX` as JSX / TSX and fails to parse them, with or without this
|
|
459
|
+
plugin. Plain `.ts` files are parsed as TypeScript without JSX, while `.tsx`,
|
|
460
|
+
`.jsx`, and JavaScript files may use JSX.
|
|
461
|
+
|
|
462
|
+
## Fast Refresh
|
|
463
|
+
|
|
464
|
+
This limitation was present in v0.1.1. With the default managed transform and
|
|
465
|
+
Vite's Oxc-based Fast Refresh, changing a transformed component's hooks
|
|
466
|
+
(adding, removing, or reordering a hook) does not preserve Fast Refresh state.
|
|
467
|
+
The refresh fails with "Rendered more hooks than during the previous render"
|
|
468
|
+
(or "fewer"), and the page needs a full reload. The managed boundary puts the
|
|
469
|
+
component's hooks inside a `try` block, where Fast Refresh does not include
|
|
470
|
+
them in the component's hook signature. In the tested fixture,
|
|
471
|
+
`transform: "inject"` did not have this limitation.
|
|
472
|
+
|
|
473
|
+
## React Server Components
|
|
474
|
+
|
|
475
|
+
The transform does not tell server modules from client modules: it reads
|
|
476
|
+
neither `"use client"` / `"use server"` directives nor the bundler's
|
|
477
|
+
environment or layer. A synchronous server component that reads `.value` would
|
|
478
|
+
therefore receive the client-only render-tracking hook and fail when it renders
|
|
479
|
+
on the server (async server components are never transformed, because async
|
|
480
|
+
functions never are). In an RSC setup, keep server components out of the
|
|
481
|
+
transform with `exclude` (for example by directory), opt individual ones out
|
|
482
|
+
with a `@noSignalTracking` comment, or use `mode: "manual"`.
|
|
258
483
|
|
|
259
484
|
## License
|
|
260
485
|
|
package/dist/esbuild.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-
|
|
1
|
+
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-B9gaexDa.js";
|
|
2
2
|
//#region src/esbuild.d.ts
|
|
3
3
|
/** The plugin contract consumed by esbuild. */
|
|
4
|
-
interface EsbuildPlugin {
|
|
4
|
+
export interface EsbuildPlugin {
|
|
5
5
|
name: string;
|
|
6
6
|
setup(build: unknown): void | Promise<void>;
|
|
7
7
|
}
|
|
8
8
|
declare const _default: (options?: ReactFineGrainedSignalsOptions) => EsbuildPlugin;
|
|
9
9
|
//#endregion
|
|
10
|
-
export { BundlerTransformOutput, EsbuildLoaderName,
|
|
10
|
+
export { BundlerTransformOutput, EsbuildLoaderName, type ReactFineGrainedSignalsMode, ReactFineGrainedSignalsOptions, type ReactFineGrainedSignalsReactCompiler, type ReactFineGrainedSignalsTransform, canTransform, createBundlerPlugin, _default as default, pluginName, reactFineGrainedSignals, resolveEsbuildLoader };
|
package/dist/esbuild.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-
|
|
1
|
+
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-CPDTNSYq.js";
|
|
2
2
|
//#region src/esbuild.ts
|
|
3
3
|
var esbuild_default = createBundlerPlugin("esbuild");
|
|
4
4
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-
|
|
1
|
+
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-B9gaexDa.js";
|
|
2
2
|
export { BundlerTransformOutput, EsbuildLoaderName, type ReactFineGrainedSignalsMode, ReactFineGrainedSignalsOptions, type ReactFineGrainedSignalsReactCompiler, type ReactFineGrainedSignalsTransform, canTransform, createBundlerPlugin, reactFineGrainedSignals as default, reactFineGrainedSignals, pluginName, resolveEsbuildLoader };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-
|
|
1
|
+
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-CPDTNSYq.js";
|
|
2
2
|
export { canTransform, createBundlerPlugin, reactFineGrainedSignals as default, reactFineGrainedSignals, pluginName, resolveEsbuildLoader };
|
package/dist/loader.d.ts
CHANGED
|
@@ -10,6 +10,5 @@ interface LoaderContext {
|
|
|
10
10
|
emitWarning?: ((warning: Error) => void) | undefined;
|
|
11
11
|
emitError?: ((error: Error) => void) | undefined;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
//#endregion
|
|
15
|
-
export { reactFineGrainedSignalsLoader as default };
|
|
13
|
+
export default function reactFineGrainedSignalsLoader(this: LoaderContext, source: string, map?: unknown): void;
|
|
14
|
+
//#endregion
|
package/dist/rollup.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-
|
|
1
|
+
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-B9gaexDa.js";
|
|
2
2
|
//#region src/rollup.d.ts
|
|
3
3
|
/** A minimal Rollup-compatible plugin shape. */
|
|
4
|
-
interface RollupPlugin {
|
|
4
|
+
export interface RollupPlugin {
|
|
5
5
|
name: string;
|
|
6
6
|
enforce?: "pre" | "post";
|
|
7
7
|
transform?: (code: string, id: string) => BundlerTransformOutput;
|
|
8
8
|
}
|
|
9
9
|
declare const _default: (options?: ReactFineGrainedSignalsOptions) => RollupPlugin;
|
|
10
10
|
//#endregion
|
|
11
|
-
export { BundlerTransformOutput, EsbuildLoaderName, type ReactFineGrainedSignalsMode, ReactFineGrainedSignalsOptions, type ReactFineGrainedSignalsReactCompiler, type ReactFineGrainedSignalsTransform,
|
|
11
|
+
export { BundlerTransformOutput, EsbuildLoaderName, type ReactFineGrainedSignalsMode, ReactFineGrainedSignalsOptions, type ReactFineGrainedSignalsReactCompiler, type ReactFineGrainedSignalsTransform, canTransform, createBundlerPlugin, _default as default, pluginName, reactFineGrainedSignals, resolveEsbuildLoader };
|
package/dist/rollup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-
|
|
1
|
+
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-CPDTNSYq.js";
|
|
2
2
|
//#region src/rollup.ts
|
|
3
3
|
var rollup_default = createBundlerPlugin("rollup");
|
|
4
4
|
//#endregion
|
package/dist/rspack.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-
|
|
1
|
+
import { a as createBundlerPlugin, c as resolveEsbuildLoader, d as ReactFineGrainedSignalsTransform, i as canTransform, l as ReactFineGrainedSignalsMode, n as EsbuildLoaderName, o as pluginName, r as ReactFineGrainedSignalsOptions, s as reactFineGrainedSignals, t as BundlerTransformOutput, u as ReactFineGrainedSignalsReactCompiler } from "./unplugin-B9gaexDa.js";
|
|
2
2
|
//#region src/rspack.d.ts
|
|
3
3
|
/** The compiler-plugin contract used by Rspack. */
|
|
4
|
-
interface RspackPlugin {
|
|
4
|
+
export interface RspackPlugin {
|
|
5
5
|
apply(compiler: unknown): void;
|
|
6
6
|
}
|
|
7
7
|
declare const _default: (options?: ReactFineGrainedSignalsOptions) => RspackPlugin;
|
|
8
8
|
//#endregion
|
|
9
|
-
export { BundlerTransformOutput, EsbuildLoaderName, type ReactFineGrainedSignalsMode, ReactFineGrainedSignalsOptions, type ReactFineGrainedSignalsReactCompiler, type ReactFineGrainedSignalsTransform,
|
|
9
|
+
export { BundlerTransformOutput, EsbuildLoaderName, type ReactFineGrainedSignalsMode, ReactFineGrainedSignalsOptions, type ReactFineGrainedSignalsReactCompiler, type ReactFineGrainedSignalsTransform, canTransform, createBundlerPlugin, _default as default, pluginName, reactFineGrainedSignals, resolveEsbuildLoader };
|
package/dist/rspack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-
|
|
1
|
+
import { a as resolveEsbuildLoader, i as reactFineGrainedSignals, n as createBundlerPlugin, r as pluginName, t as canTransform } from "./unplugin-CPDTNSYq.js";
|
|
2
2
|
//#region src/rspack.ts
|
|
3
3
|
var rspack_default = createBundlerPlugin("rspack");
|
|
4
4
|
//#endregion
|
|
@@ -7,12 +7,12 @@ type ReactFineGrainedSignalsReactCompiler = "auto" | "off";
|
|
|
7
7
|
//#region src/unplugin.d.ts
|
|
8
8
|
interface ReactFineGrainedSignalsOptions {
|
|
9
9
|
/**
|
|
10
|
-
* `manual` preserves explicit
|
|
10
|
+
* `manual` preserves explicit useSignalTracking() opt-in. `auto` detects signal
|
|
11
11
|
* reads in components and custom hooks; `all` also wraps JSX components that
|
|
12
12
|
* do not statically expose a .value read.
|
|
13
13
|
*/
|
|
14
14
|
mode?: ReactFineGrainedSignalsMode;
|
|
15
|
-
/** `managed` (default) adds an exact try/finally boundary; `inject` adds bare
|
|
15
|
+
/** `managed` (default) adds an exact try/finally boundary; `inject` adds bare useSignalTracking() for best-effort opt-in. */
|
|
16
16
|
transform?: ReactFineGrainedSignalsTransform;
|
|
17
17
|
/**
|
|
18
18
|
* `auto` (default) marks every transformed function with `"use no memo"`, so
|
|
@@ -21,7 +21,7 @@ interface ReactFineGrainedSignalsOptions {
|
|
|
21
21
|
* when its memoization has been verified against this library's tracking.
|
|
22
22
|
*/
|
|
23
23
|
reactCompiler?: ReactFineGrainedSignalsReactCompiler;
|
|
24
|
-
/** Package
|
|
24
|
+
/** Package whose root exports bare `useSignalTracking` and whose `/runtime` entry exports `useManagedSignals`. */
|
|
25
25
|
importSource?: string;
|
|
26
26
|
/**
|
|
27
27
|
* Extra module specifier whose `memo`/`forwardRef` exports are treated as
|