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 +4 -1
- package/ppx/README.md +51 -30
- package/ppx/build.sh +13 -6
- package/ppx/postinstall.js +57 -0
- package/ppx/ppx.ml +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xote",
|
|
3
|
-
"version": "7.1.0-beta.
|
|
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:**
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
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
|
|
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
|
|
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
|
-
##
|
|
213
|
+
## Distribution
|
|
210
214
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
```
|
|
215
|
+
The npm package ships the PPX **prebuilt** for the common platforms, under
|
|
216
|
+
`ppx/bin/`:
|
|
214
217
|
|
|
215
|
-
|
|
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
|
|
234
|
+
{ "ppx-flags": ["xote/ppx/ppx"] }
|
|
219
235
|
```
|
|
220
236
|
|
|
221
|
-
|
|
237
|
+
That one line in your `rescript.json` is the whole setup — no OCaml toolchain
|
|
238
|
+
required. Two edge cases:
|
|
222
239
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
`
|
|
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
|
-
|
|
235
|
-
npm install xote
|
|
236
|
-
sh node_modules/xote/ppx/build.sh # needs ocamlopt
|
|
237
|
-
```
|
|
256
|
+
## Build from source
|
|
238
257
|
|
|
239
|
-
```
|
|
240
|
-
|
|
258
|
+
```sh
|
|
259
|
+
sh build.sh # produces ./ppx (needs ocamlopt; any recent OCaml, tested 4.14)
|
|
241
260
|
```
|
|
242
261
|
|
|
243
|
-
|
|
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 (
|
|
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 @
|
|
3
|
-
# OCaml compiler. ReScript 12 hands a ppx an OCaml 4.06 parsetree;
|
|
4
|
-
# vendors those exact AST types, so the only build dependency is
|
|
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
|
-
|
|
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)
|
|
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 _ ->
|