react-fine-grained-signals 0.1.0 → 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Johnson Chu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md CHANGED
@@ -8,21 +8,88 @@
8
8
 
9
9
  ## ドキュメント
10
10
 
11
- ライブラリの使い方のガイド、設計検討メモ、ドキュメント索引全体については[`docs/README.ja.md`](docs/README.ja.md)を参照してください。
11
+ ユーザー向けドキュメントの索引は[`docs/README.ja.md`](docs/README.ja.md)を参照してください。
12
+ v0.1.1からの移行については[v0.2移行ガイド](./docs/migration/v0.2.ja.md)を参照してください。
12
13
 
13
14
  ## インストール
14
15
 
15
16
  ```sh
16
- # 公開予定のパッケージ名です。まだnpmには公開していません。
17
- pnpm add react-fine-grained-signals alien-signals
17
+ pnpm add react-fine-grained-signals
18
18
  ```
19
19
 
20
- `alien-signals` はpeer dependencyなので、上のように併せてinstallしてください。
20
+ React(19以降)はpeer dependencyで、Node.js 22以降が必要です。packageはESMのみで、
21
+ entry pointと型を `exports` で公開しているため、`react-fine-grained-signals/utils`
22
+ やJSXランタイムを解決するには、TypeScriptの `moduleResolution` を `bundler`、
23
+ `node16`、`nodenext` のいずれかにしてください。
21
24
 
22
- `useSignals()` を自動挿入するオプションのbuild pluginは別途installします。[描画最適化](docs/rendering-optimization.ja.md)を参照してください。
25
+ ## セットアップ
26
+
27
+ プリミティブとフックは、installした時点で動作します。buildツールやコンパイラの設定は不要です。
28
+
29
+ ```tsx
30
+ import { useSignal, useSignalTracking } from "react-fine-grained-signals";
31
+
32
+ function Counter() {
33
+ useSignalTracking();
34
+ const count = useSignal(0);
35
+
36
+ return <button onClick={() => count.value++}>{count.value}</button>;
37
+ }
38
+ ```
39
+
40
+ 以下の2つの最適化は任意であり、互いに独立しています。どちらもinstall後に別途設定するもので、上記のフックを使うだけなら不要です。
41
+
42
+ ### JSXランタイム — DOMへの直接バインディング
43
+
44
+ ネイティブホスト要素の子要素として使われたsignalが、周囲のコンポーネントを再レンダーせずに、そのDOMノードだけを更新できるようになります。
45
+
46
+ ファイル単位で有効にするには、先頭行にpragmaを書きます。他のファイルはReactのJSXランタイムのままになるため、アプリの一部だけで直接バインディングを使いたい場合はこちらを選んでください。[`examples/react-router`](examples/react-router)がこの構成です。
47
+
48
+ ```tsx
49
+ /** @jsxImportSource react-fine-grained-signals */
50
+ ```
51
+
52
+ プロジェクト全体で有効にする場合は `tsconfig.json` に設定します。
53
+
54
+ ```json
55
+ {
56
+ "compilerOptions": {
57
+ "jsx": "react-jsx",
58
+ "jsxImportSource": "react-fine-grained-signals"
59
+ }
60
+ }
61
+ ```
62
+
63
+ Viteはこの2つを `tsconfig.json` から読むため、`vite.config.ts` 側にJSXの設定は不要です。既定の変換でも `@vitejs/plugin-react` を使う場合でも同じです。
64
+
65
+ このランタイムが対象とするのはネイティブ要素と、意図的に絞ったpropsだけです。Reactコンポーネントのpropsや子要素に渡したsignalはアンラップされません。許可リスト全体と、JSXをBabelで変換するツールチェーン(`tsconfig.json` を読みません)については[JSXのsignal子要素とhost binding](./docs/guides/jsx-bindings.ja.md)を参照してください。
66
+
67
+ ### ビルドplugin — `useSignalTracking()` の自動挿入
68
+
69
+ 追跡境界をbuild時に挿入するため、コンポーネント側で `useSignalTracking()` を手書きする必要がなくなります。別packageなので個別にinstallします。
70
+
71
+ ```sh
72
+ pnpm add -D unplugin-react-fine-grained-signals
73
+ ```
74
+
75
+ そのうえで、bundlerに応じたentry point(`/vite`、`/rollup`、`/webpack`、`/rspack`、`/esbuild`)をbuild設定に追加します。
76
+
77
+ ```ts
78
+ // vite.config.ts
79
+ import { defineConfig } from "vite";
80
+ import signals from "unplugin-react-fine-grained-signals/vite";
81
+
82
+ export default defineConfig({
83
+ plugins: [signals({ mode: "auto" })],
84
+ });
85
+ ```
86
+
87
+ オプションの詳細と、フックを手書きする場合との使い分けについては[描画最適化](./docs/guides/rendering-optimization.ja.md)を参照してください。
23
88
 
24
89
  ## 開発
25
90
 
91
+ アーキテクチャ、設計、実装履歴は[`development/README.md`](development/README.md)にまとめています。
92
+
26
93
  ```sh
27
94
  pnpm install --frozen-lockfile
28
95
  pnpm typecheck
@@ -50,4 +117,4 @@ pnpm test:browser
50
117
 
51
118
  - [alien-signals](https://www.npmjs.com/package/alien-signals) — 本パッケージが基盤とするシグナルエンジン。
52
119
  - [@preact/signals-core](https://www.npmjs.com/package/@preact/signals-core) — ベンチマークの比較対象。
53
- - [@preact/signals-react](https://www.npmjs.com/package/@preact/signals-react) — `useSignals()` boundaryのstore protocol設計における先行事例。詳細は[Prior art](docs/design/use-signals-boundary-design.ja.md#先行事例)を参照。
120
+ - [@preact/signals-react](https://www.npmjs.com/package/@preact/signals-react) — 外部API `useSignals()` boundaryのstore protocol設計における先行事例。詳細は[Prior art](./development/design/use-signals-boundary-design.ja.md#先行事例)を参照。
package/README.md CHANGED
@@ -8,21 +8,104 @@ An experimental fine-grained rendering layer for React 19, built on [alien-signa
8
8
 
9
9
  ## Documentation
10
10
 
11
- See [`docs/README.md`](docs/README.md) for guides on using the library, design investigation memos, and the full documentation index.
11
+ See [`docs/README.md`](docs/README.md) for the user documentation index.
12
+ For upgrades from v0.1.1, see the [v0.2 migration guide](./docs/migration/v0.2.md).
12
13
 
13
14
  ## Installation
14
15
 
15
16
  ```sh
16
- # Planned package name — it is not published to npm yet.
17
- pnpm add react-fine-grained-signals alien-signals
17
+ pnpm add react-fine-grained-signals
18
18
  ```
19
19
 
20
- `alien-signals` is a peer dependency, so install it alongside this package.
20
+ React (19 or newer) is a peer dependency, and Node.js 22 or newer is required.
21
+ The package is ESM-only and publishes its entry points and types through
22
+ `exports`, so TypeScript needs `moduleResolution` set to `bundler`, `node16`, or
23
+ `nodenext` to resolve `react-fine-grained-signals/utils` and the JSX runtime.
21
24
 
22
- The optional build plugin for automatic `useSignals()` insertion is installed separately — see [Rendering optimization](docs/rendering-optimization.md).
25
+ ## Setup
26
+
27
+ The primitives and hooks work as soon as the package is installed — no build
28
+ or compiler configuration is involved:
29
+
30
+ ```tsx
31
+ import { useSignal, useSignalTracking } from "react-fine-grained-signals";
32
+
33
+ function Counter() {
34
+ useSignalTracking();
35
+ const count = useSignal(0);
36
+
37
+ return <button onClick={() => count.value++}>{count.value}</button>;
38
+ }
39
+ ```
40
+
41
+ The two optimizations below are opt-in and independent of each other. Both are
42
+ configured after installing, and neither is needed to use the hooks above.
43
+
44
+ ### JSX runtime — direct DOM bindings
45
+
46
+ Lets a signal used as the child of a native host element update that DOM node
47
+ on its own, without re-rendering the component around it.
48
+
49
+ Opt in one file at a time with a pragma on the first line. Every other file
50
+ keeps React's JSX runtime, so this is the form to reach for when only part of
51
+ an app needs the direct bindings — it is how
52
+ [`examples/react-router`](examples/react-router) is set up:
53
+
54
+ ```tsx
55
+ /** @jsxImportSource react-fine-grained-signals */
56
+ ```
57
+
58
+ Or opt the whole project in through `tsconfig.json`:
59
+
60
+ ```json
61
+ {
62
+ "compilerOptions": {
63
+ "jsx": "react-jsx",
64
+ "jsxImportSource": "react-fine-grained-signals"
65
+ }
66
+ }
67
+ ```
68
+
69
+ Vite reads both of those from `tsconfig.json` — with its default transform and
70
+ with `@vitejs/plugin-react` alike — so `vite.config.ts` needs no JSX
71
+ configuration of its own.
72
+
73
+ The runtime covers native elements only, and a deliberately narrow set of
74
+ props. Signals passed to a React component's props or children are not
75
+ unwrapped. See [JSX signal children and host bindings](./docs/guides/jsx-bindings.md)
76
+ for the full allow-list, and for toolchains that transform JSX through Babel,
77
+ which do not read `tsconfig.json`.
78
+
79
+ ### Build plugin — automatic `useSignalTracking()`
80
+
81
+ Inserts the tracking boundary during the build, so components no longer call
82
+ `useSignalTracking()` by hand. It is a separate package:
83
+
84
+ ```sh
85
+ pnpm add -D unplugin-react-fine-grained-signals
86
+ ```
87
+
88
+ It then needs a bundler entry point — `/vite`, `/rollup`, `/webpack`,
89
+ `/rspack`, or `/esbuild` — added to the build configuration:
90
+
91
+ ```ts
92
+ // vite.config.ts
93
+ import { defineConfig } from "vite";
94
+ import signals from "unplugin-react-fine-grained-signals/vite";
95
+
96
+ export default defineConfig({
97
+ plugins: [signals({ mode: "auto" })],
98
+ });
99
+ ```
100
+
101
+ See [Rendering optimization](./docs/guides/rendering-optimization.md) for the options
102
+ and for the trade-offs against calling the hook by hand.
23
103
 
24
104
  ## Development
25
105
 
106
+ Architecture, design, and implementation history are indexed in
107
+ [`development/README.md`](development/README.md).
108
+
26
109
  ```sh
27
110
  pnpm install --frozen-lockfile
28
111
  pnpm typecheck
@@ -51,4 +134,4 @@ Run `pnpm dev:browser` to build the transform package and inspect the same examp
51
134
 
52
135
  - [alien-signals](https://www.npmjs.com/package/alien-signals) — the signal engine this package builds on.
53
136
  - [@preact/signals-core](https://www.npmjs.com/package/@preact/signals-core) — benchmark comparison target.
54
- - [@preact/signals-react](https://www.npmjs.com/package/@preact/signals-react) — prior art for the `useSignals()` boundary's store protocol; see [Prior art](docs/design/use-signals-boundary-design.md#prior-art).
137
+ - [@preact/signals-react](https://www.npmjs.com/package/@preact/signals-react) — prior art for the external `useSignals()` boundary's store protocol; see [Prior art](./development/design/use-signals-boundary-design.md#prior-art).
@@ -0,0 +1,55 @@
1
+ import { n as SIGNAL_BRAND, t as coreRuntime } from "./core-runtime-2jSE9Tz5.js";
2
+ //#region src/core/base.ts
3
+ const signalInstances = /* @__PURE__ */ new WeakSet();
4
+ /** Marks an internal signal implementation for the public identity guard. */
5
+ function registerSignal(value) {
6
+ signalInstances.add(value);
7
+ Object.defineProperty(value, SIGNAL_BRAND, {
8
+ value: 1,
9
+ enumerable: false,
10
+ writable: false,
11
+ configurable: false
12
+ });
13
+ return value;
14
+ }
15
+ /** Returns whether a value came from this package's signal APIs, any copy. */
16
+ function isSignal(value) {
17
+ if (typeof value !== "object" || value === null) return false;
18
+ if (signalInstances.has(value)) return true;
19
+ const brand = value[SIGNAL_BRAND];
20
+ if (typeof brand !== "number" || brand < 1) return false;
21
+ return typeof value.peek === "function";
22
+ }
23
+ /** Creates a writable reactive value. */
24
+ function signal(initialValue) {
25
+ return coreRuntime.signal(initialValue);
26
+ }
27
+ /** Creates a lazily evaluated reactive value. */
28
+ function computed(getter) {
29
+ return coreRuntime.computed(getter);
30
+ }
31
+ /** Runs a reactive side effect and returns a disposer. */
32
+ function effect(fn) {
33
+ return coreRuntime.effect(fn);
34
+ }
35
+ /**
36
+ * Package-internal: an effect that is never owned by the effect or computed
37
+ * running when it is created. Hooks and JSX bindings create theirs from React
38
+ * commits, which can run synchronously inside a user effect (`flushSync`), and
39
+ * must not be disposed when that unrelated effect next re-runs.
40
+ */
41
+ function detachedEffect(fn) {
42
+ return coreRuntime.detachedEffect(fn);
43
+ }
44
+ /** Groups writes, deferring effect notifications until the callback completes. */
45
+ function batch(fn) {
46
+ return coreRuntime.batch(fn);
47
+ }
48
+ /** Runs a callback without collecting reactive dependencies. */
49
+ function untracked(fn) {
50
+ return coreRuntime.untracked(fn);
51
+ }
52
+ //#endregion
53
+ export { isSignal as a, untracked as c, effect as i, computed as n, registerSignal as o, detachedEffect as r, signal as s, batch as t };
54
+
55
+ //# sourceMappingURL=base-WD_74mfC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-WD_74mfC.js","names":[],"sources":["../src/core/base.ts"],"sourcesContent":["import { coreRuntime } from \"./core-runtime.js\";\nimport { SIGNAL_BRAND, SIGNAL_BRAND_MIN_VERSION, SIGNAL_BRAND_VERSION } from \"./signal-brand.js\";\n\n/** A readable reactive value. */\nexport interface ReadonlySignal<T> {\n readonly value: T;\n peek(): T;\n}\n\n/** A readable and writable reactive value. */\nexport interface Signal<T> extends ReadonlySignal<T> {\n value: T;\n}\n\nconst signalInstances = new WeakSet<object>();\n\n// The brand carries a protocol version instead of `true` so a future instance\n// can tell which contract a foreign signal claims. See signal-brand.ts for why\n// v0.2 uses a different key from v0.1.x rather than a larger version number.\n\n/** Marks an internal signal implementation for the public identity guard. */\nexport function registerSignal<T extends object>(value: T): T {\n signalInstances.add(value);\n // Non-enumerable keeps the brand out of JSON, `Object.keys`, spread, and\n // React's prop diffing; non-writable and non-configurable keep it from being\n // retargeted or stripped once the value is public. The hardening is free:\n // measured against a writable/configurable descriptor the cost is identical,\n // because it is the `defineProperty` call itself, and V8 keeps the instance\n // in fast properties either way (checked with `%HasFastProperties`). Stamping\n // the prototype instead would be worse — that pushes the prototype into\n // dictionary mode, which every lookup through it then pays for.\n Object.defineProperty(value, SIGNAL_BRAND, {\n value: SIGNAL_BRAND_VERSION,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n return value;\n}\n\n/** Returns whether a value came from this package's signal APIs, any copy. */\nexport function isSignal(value: unknown): value is ReadonlySignal<unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n // The WeakSet is both the cheaper lookup and the authority for values created\n // by this instance; the brand is the fallback for every other instance.\n if (signalInstances.has(value)) return true;\n const brand = (value as { [SIGNAL_BRAND]?: unknown })[SIGNAL_BRAND];\n if (typeof brand !== \"number\" || brand < SIGNAL_BRAND_MIN_VERSION) return false;\n // A foreign brand is a claim, not a proof. Checking the cheap half of the\n // contract turns a malformed brand into `false` instead of a crash inside a\n // render, and costs nothing on the local-instance path above.\n return typeof (value as ReadonlySignal<unknown>).peek === \"function\";\n}\n\n/** Creates a writable reactive value. */\nexport function signal<T>(initialValue: T): Signal<T> {\n return coreRuntime.signal(initialValue);\n}\n\n/** Creates a lazily evaluated reactive value. */\nexport function computed<T>(getter: () => T): ReadonlySignal<T> {\n return coreRuntime.computed(getter);\n}\n\n/** Runs a reactive side effect and returns a disposer. */\nexport function effect(fn: () => void | (() => void)): () => void {\n return coreRuntime.effect(fn);\n}\n\n/**\n * Package-internal: an effect that is never owned by the effect or computed\n * running when it is created. Hooks and JSX bindings create theirs from React\n * commits, which can run synchronously inside a user effect (`flushSync`), and\n * must not be disposed when that unrelated effect next re-runs.\n */\nexport function detachedEffect(fn: () => void | (() => void)): () => void {\n return coreRuntime.detachedEffect(fn);\n}\n\n/** Groups writes, deferring effect notifications until the callback completes. */\nexport function batch<T>(fn: () => T): T {\n return coreRuntime.batch(fn);\n}\n\n/** Runs a callback without collecting reactive dependencies. */\nexport function untracked<T>(fn: () => T): T {\n return coreRuntime.untracked(fn);\n}\n"],"mappings":";;AAcA,MAAM,kCAAkB,IAAI,QAAgB;;AAO5C,SAAgB,eAAiC,OAAa;CAC5D,gBAAgB,IAAI,KAAK;CASzB,OAAO,eAAe,OAAO,cAAc;EACzC,OAAA;EACA,YAAY;EACZ,UAAU;EACV,cAAc;CAChB,CAAC;CACD,OAAO;AACT;;AAGA,SAAgB,SAAS,OAAkD;CACzE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CAGxD,IAAI,gBAAgB,IAAI,KAAK,GAAG,OAAO;CACvC,MAAM,QAAS,MAAuC;CACtD,IAAI,OAAO,UAAU,YAAY,QAAA,GAAkC,OAAO;CAI1E,OAAO,OAAQ,MAAkC,SAAS;AAC5D;;AAGA,SAAgB,OAAU,cAA4B;CACpD,OAAO,YAAY,OAAO,YAAY;AACxC;;AAGA,SAAgB,SAAY,QAAoC;CAC9D,OAAO,YAAY,SAAS,MAAM;AACpC;;AAGA,SAAgB,OAAO,IAA2C;CAChE,OAAO,YAAY,OAAO,EAAE;AAC9B;;;;;;;AAQA,SAAgB,eAAe,IAA2C;CACxE,OAAO,YAAY,eAAe,EAAE;AACtC;;AAGA,SAAgB,MAAS,IAAgB;CACvC,OAAO,YAAY,MAAM,EAAE;AAC7B;;AAGA,SAAgB,UAAa,IAAgB;CAC3C,OAAO,YAAY,UAAU,EAAE;AACjC"}