xote 7.1.0-beta.1 → 7.1.0-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xote",
3
- "version": "7.1.0-beta.1",
3
+ "version": "7.1.0-beta.3",
4
4
  "repository": {
5
5
  "url": "https://github.com/brnrdog/xote"
6
6
  },
@@ -63,10 +63,13 @@
63
63
  "rescript.json",
64
64
  "ppx/ppx.ml",
65
65
  "ppx/build.sh",
66
+ "ppx/postinstall.js",
67
+ "ppx/bin",
66
68
  "ppx/README.md",
67
69
  "ppx/LICENSE.OCaml"
68
70
  ],
69
71
  "scripts": {
72
+ "postinstall": "node ppx/postinstall.js",
70
73
  "res:build": "rescript",
71
74
  "res:clean": "rescript clean",
72
75
  "res:dev": "rescript -w",
package/ppx/README.md CHANGED
@@ -5,15 +5,18 @@ props-deriving component whose returned JSX is decomposed into **fine-grained
5
5
  reactive leaves** — the compile-time counterpart to the runtime
6
6
  [`View.tracked`](../src/View.res) helper.
7
7
 
8
- > **Status:** experimental, opt-in. The PPX is exercised in CI and used by the
9
- > [docs site](../docs-website) itself (the Counter demo), but it is **not part
10
- > of the published npm package** consumers who want it build it themselves
11
- > (see [Opt-in for consumers](#opt-in-for-consumers)). It demonstrates that the
8
+ > **Status:** the standard authoring model. The npm package ships the PPX as a
9
+ > prebuilt binary per platform, selected at install time (see
10
+ > [Distribution](#distribution)), so enabling `@xote.component` is a single
11
+ > `ppx-flags` line with no toolchain requirement. The PPX is exercised in CI
12
+ > and used by the [docs site](../docs-website) itself. It grew out of the
12
13
  > [`rescript-signals` #34](https://github.com/brnrdog/rescript-signals/pull/34)
13
- > auto-tracking idea can target Xote's view layer *and* compile away the
14
+ > auto-tracking idea, targeting Xote's view layer *and* compiling away the
14
15
  > wholesale-replacement tradeoff of `View.tracked`. See
15
16
  > [`docs/proposals/tracked-blocks.md`](../docs/proposals/tracked-blocks.md) for
16
- > the full design context (this is "Phase 2, fine-grained variant").
17
+ > the full design context, and
18
+ > [RFC #141](https://github.com/brnrdog/xote/issues/141) for the decision to
19
+ > distribute prebuilt binaries and make this the primary model.
17
20
 
18
21
  ## What problem it solves
19
22
 
@@ -89,6 +92,7 @@ Applied recursively to the component's returned JSX:
89
92
  | `<View.Text/Int/Float/Bool>` child | yes | thunked → reactive text node (leaf) |
90
93
  | `<View.Text/…>` child | no | left as-is (static text) |
91
94
  | Element / nested JSX | — | recurse into attributes and children |
95
+ | Fragment (`<>…</>`) | — | recurse into each child independently (so nested reactive regions stay separate — not collapsed into one thunk) |
92
96
  | Bare child, control flow (`if`/`switch` selecting different nodes) | yes | branches decomposed fine-grained, then wrapped in `View.tracked` — see below |
93
97
  | Bare child, otherwise (`{Signal.get(x)}`, `{"lit"}`, `{someNode}`) | — | wrapped in `View.child` — see [Bare value children](#bare-value-children) |
94
98
 
@@ -206,41 +210,58 @@ exception**; they keep their original copyright headers. The
206
210
  and license text is in [`LICENSE.OCaml`](./LICENSE.OCaml), which ships in the
207
211
  npm tarball alongside `ppx.ml`.
208
212
 
209
- ## Build
213
+ ## Distribution
210
214
 
211
- ```sh
212
- sh build.sh # produces ./ppx (needs ocamlopt; any recent OCaml, tested 4.14)
213
- ```
215
+ The npm package ships the PPX **prebuilt** for the common platforms, under
216
+ `ppx/bin/`:
214
217
 
215
- Wire it into a project's `rescript.json`:
218
+ | Platform | Binary |
219
+ |---|---|
220
+ | Linux x64 | `ppx-linux-x64.exe` |
221
+ | Linux arm64 | `ppx-linux-arm64.exe` |
222
+ | macOS x64 (Intel) | `ppx-darwin-x64.exe` |
223
+ | macOS arm64 (Apple Silicon) | `ppx-darwin-arm64.exe` |
224
+ | Windows x64 | `ppx-win32-x64.exe` |
225
+
226
+ The binaries are compiled in CI by
227
+ [`.github/workflows/ppx-binaries.yml`](../.github/workflows/ppx-binaries.yml)
228
+ and injected into the tarball by the release workflow. On install, Xote's
229
+ `postinstall` script ([`postinstall.js`](./postinstall.js)) copies the binary
230
+ matching `process.platform`-`process.arch` to `ppx/ppx` (`ppx/ppx.exe` on
231
+ Windows), which is the path consumers reference:
216
232
 
217
233
  ```json
218
- { "ppx-flags": ["xote-tracked-ppx/ppx"] }
234
+ { "ppx-flags": ["xote/ppx/ppx"] }
219
235
  ```
220
236
 
221
- ## Opt-in for consumers
237
+ That one line in your `rescript.json` is the whole setup — no OCaml toolchain
238
+ required. Two edge cases:
222
239
 
223
- The compiled binary is platform-specific and is **not** shipped in the npm
224
- package, and deliberately neither is a `ppx-flags` entry in Xote's own
225
- published `rescript.json`. That last point matters: a ReScript consumer
226
- recompiles a dependency's sources during its own build and applies that
227
- dependency's `ppx-flags`, so a PPX listed in Xote's published config would
228
- force `ocamlopt` on **every** Xote user. Instead the PPX is strictly opt-in.
240
+ - **Install scripts disabled.** Some package managers skip dependency install
241
+ scripts (pnpm does by default). Approve them for `xote`
242
+ (`pnpm approve-builds`) or run `node node_modules/xote/ppx/postinstall.js`
243
+ once.
244
+ - **Unsupported platform.** If no prebuilt binary matches, the install script
245
+ falls back to compiling `ppx.ml` from source when `ocamlopt` is on the
246
+ `PATH`, and otherwise prints instructions without failing the install.
229
247
 
230
- The `ppx.ml` source *is* published, so a consumer who wants `@xote.component`
231
- can build it from the installed package and reference it from **their own**
232
- `rescript.json`:
248
+ A `ppx-flags` entry is deliberately **not** in Xote's own published
249
+ `rescript.json`: a ReScript consumer recompiles a dependency's sources during
250
+ its own build and applies that dependency's `ppx-flags`, and Xote's `src/`
251
+ carries no `@xote.component` annotations, so listing the PPX there would make
252
+ every build depend on the binary for no benefit. The annotation applies to
253
+ *your* components, which is why the flag lives in *your* `rescript.json` —
254
+ exactly like the `jsx` configuration you already mirror.
233
255
 
234
- ```sh
235
- npm install xote
236
- sh node_modules/xote/ppx/build.sh # needs ocamlopt
237
- ```
256
+ ## Build from source
238
257
 
239
- ```json
240
- { "ppx-flags": ["xote/ppx/ppx"] }
258
+ ```sh
259
+ sh build.sh # produces ./ppx (needs ocamlopt; any recent OCaml, tested 4.14)
241
260
  ```
242
261
 
243
- Consumers who don't do this are entirely unaffected.
262
+ `build.sh` also accepts an output path relative to `ppx/`
263
+ (`sh build.sh bin/ppx-linux-x64.exe`), which is how the CI matrix produces
264
+ the prebuilt binaries.
244
265
 
245
266
  ## In this repo
246
267
 
@@ -271,7 +292,7 @@ Or step by step from `example/`:
271
292
  sh setup.sh # link toolchain + Xote from the repo root (idempotent)
272
293
  sh ../build.sh # build the ppx
273
294
  npm run build # compile Demo.res through the ppx
274
- npm run verify # jsdom runtime check (52 assertions)
295
+ npm run verify # jsdom runtime check (71 assertions)
275
296
  ```
276
297
 
277
298
  ## Known limitations (it's a prototype)
package/ppx/build.sh CHANGED
@@ -1,10 +1,17 @@
1
1
  #!/bin/sh
2
- # Compiles the fine-grained @tracked ppx to a native binary using the system
3
- # OCaml compiler. ReScript 12 hands a ppx an OCaml 4.06 parsetree; ppx.ml
4
- # vendors those exact AST types, so the only build dependency is ocamlopt
5
- # (any recent OCaml works — tested with 4.14).
2
+ # Compiles the fine-grained @xote.component ppx to a native binary using the
3
+ # system OCaml compiler. ReScript 12 hands a ppx an OCaml 4.06 parsetree;
4
+ # ppx.ml vendors those exact AST types, so the only build dependency is
5
+ # ocamlopt (any recent OCaml works — tested with 4.14).
6
+ #
7
+ # Usage: build.sh [output-path]
8
+ # output-path is relative to this directory and defaults to ./ppx.
9
+ # CI passes bin/ppx-<platform>-<arch>.exe to produce the prebuilt binaries
10
+ # shipped in the npm package.
6
11
  set -e
7
12
  cd "$(dirname "$0")"
8
- ocamlopt -w -a-31 ppx.ml -o ppx
13
+ out="${1:-ppx}"
14
+ mkdir -p "$(dirname "$out")"
15
+ ocamlopt -w -a-31 ppx.ml -o "$out"
9
16
  rm -f ppx.cmi ppx.cmx ppx.o
10
- echo "built $(pwd)/ppx"
17
+ echo "built $(pwd)/$out"
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Selects the prebuilt @xote.component ppx binary for the current platform.
4
+ *
5
+ * The npm package ships one native binary per supported platform under
6
+ * ppx/bin/ (built in CI, see .github/workflows/ppx-binaries.yml). This script
7
+ * copies the matching one to ppx/ppx (ppx/ppx.exe on Windows), which is the
8
+ * path consumers reference from their rescript.json:
9
+ *
10
+ * "ppx-flags": ["xote/ppx/ppx"]
11
+ *
12
+ * Fallbacks, in order:
13
+ * 1. No prebuilt binary for this platform, but ocamlopt is available:
14
+ * compile from the bundled ppx.ml source (build.sh).
15
+ * 2. Otherwise: print instructions and exit 0. The install never fails —
16
+ * the ppx is only needed by projects that list it in their own
17
+ * ppx-flags, and Xote's published library sources compile without it.
18
+ */
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { spawnSync } = require('child_process');
24
+
25
+ const ppxDir = __dirname;
26
+ const isWindows = process.platform === 'win32';
27
+ const target = `${process.platform}-${process.arch}`;
28
+ const prebuilt = path.join(ppxDir, 'bin', `ppx-${target}.exe`);
29
+ const dest = path.join(ppxDir, isWindows ? 'ppx.exe' : 'ppx');
30
+
31
+ function installPrebuilt() {
32
+ if (!fs.existsSync(prebuilt)) return false;
33
+ fs.copyFileSync(prebuilt, dest);
34
+ if (!isWindows) fs.chmodSync(dest, 0o755);
35
+ console.log(`xote: installed prebuilt ppx binary for ${target}`);
36
+ return true;
37
+ }
38
+
39
+ function buildFromSource() {
40
+ const probe = spawnSync('ocamlopt', ['-version'], { stdio: 'ignore', shell: isWindows });
41
+ if (probe.error || probe.status !== 0) return false;
42
+ console.log(`xote: no prebuilt ppx binary for ${target}, compiling from source...`);
43
+ const build = spawnSync('sh', [path.join(ppxDir, 'build.sh')], { stdio: 'inherit' });
44
+ return build.status === 0 && fs.existsSync(dest);
45
+ }
46
+
47
+ if (!installPrebuilt() && !buildFromSource()) {
48
+ console.warn(
49
+ [
50
+ `xote: no ppx binary available for ${target}.`,
51
+ 'The @xote.component annotation needs it; the rest of Xote does not.',
52
+ 'To use it, install an OCaml compiler (ocamlopt) and run:',
53
+ ' sh node_modules/xote/ppx/build.sh',
54
+ 'Supported prebuilt platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64.',
55
+ ].join('\n'),
56
+ );
57
+ }
package/ppx/ppx.ml CHANGED
@@ -1117,6 +1117,14 @@ let jsx_parts (e : expression) =
1117
1117
  | Pexp_apply (f, args) when has_jsx e -> Some (f, args)
1118
1118
  | _ -> None
1119
1119
 
1120
+ (* A JSX fragment `<>…</>` is a `::`/`[]` list carrying the JSX attribute (not a
1121
+ Pexp_apply, so jsx_parts misses it). Its children are node position. *)
1122
+ let is_jsx_fragment (e : expression) : bool =
1123
+ has_jsx e
1124
+ && (match e.pexp_desc with
1125
+ | Pexp_construct ({ txt = Longident.Lident ("::" | "[]"); _ }, _) -> true
1126
+ | _ -> false)
1127
+
1120
1128
  (* Lowercase leading char => intrinsic HTML/SVG element (children are nodes). *)
1121
1129
  let is_element (f : expression) : bool =
1122
1130
  match f.pexp_desc with
@@ -1151,6 +1159,15 @@ let rec fine_node (env : env) (e : expression) : expression =
1151
1159
  | Some (f, args) ->
1152
1160
  (* element or user component: attrs are value position, children nodes *)
1153
1161
  { e with pexp_desc = Pexp_apply (f, List.map (element_arg env) args) }
1162
+ | None when is_jsx_fragment e ->
1163
+ (* A fragment `<>…</>` is a JSX-tagged `::`/`[]` list, not a Pexp_apply, so
1164
+ jsx_parts misses it. Recurse fine_node into each child exactly like an
1165
+ element's children (map_children preserves the outer @JSX attribute), so
1166
+ nested reactive regions stay *independent*. Without this the whole fragment
1167
+ would be wrapped in one coarse thunk and any nested `if`/`switch` inside it
1168
+ would collapse into that single tracked scope — rebuilding every sibling on
1169
+ one signal change. *)
1170
+ map_children (fine_node env) e
1154
1171
  | None ->
1155
1172
  (match e.pexp_desc with
1156
1173
  | Pexp_ifthenelse _ | Pexp_match _ ->