@reventlessdev/rescript-node 2.0.0-alpha.1 → 2.0.0-alpha.10
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/CHANGELOG.md +64 -0
- package/README.md +243 -0
- package/package.json +1 -1
- package/rescript.json +2 -0
- package/src/NodeBuffer.res +30 -0
- package/src/NodeBuffer.res.mjs +2 -0
- package/src/NodeChildProcess.res +29 -0
- package/src/NodeFs.res +43 -0
- package/src/NodeImportMeta.res +20 -0
- package/src/NodeImportMeta.res.mjs +2 -0
- package/src/NodeModule.res +5 -0
- package/src/NodeNet.res +45 -0
- package/src/NodeNet.res.mjs +2 -0
- package/src/NodeProcess.res +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,70 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 2.0.0-alpha.10 (2026-09-08)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **seed:** a run says which identity its token carries, not which one it asked for ([063da61](https://github.com/ReventlessDev/reventless-core/commit/063da6110a2d89333bd3d99f4c7cb1bc579d78f0))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 2.0.0-alpha.9 (2026-09-04)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **local:** serve the event tap over a socket, on by default ([1babf96](https://github.com/ReventlessDev/reventless-core/commit/1babf960f032805418e3ac8f25d03ceb3dcd8d20))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 2.0.0-alpha.8 (2026-08-18)
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **core:** read a command's lifecycle guard off the GWT corpus, and check [@transition](https://github.com/transition) against it ([057c898](https://github.com/ReventlessDev/reventless-core/commit/057c898761917bd6fc5eb9ac6867c8125709eab4))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# 2.0.0-alpha.7 (2026-08-15)
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
* **local:** serve ui-hints.json edits without restarting the platform ([b29b104](https://github.com/ReventlessDev/reventless-core/commit/b29b1044f31f620071423c618486daef9692c614))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# 2.0.0-alpha.6 (2026-08-14)
|
|
35
|
+
|
|
36
|
+
### Features
|
|
37
|
+
|
|
38
|
+
* **local:** let the seed tools address a platform, not a guessed file ([98862ad](https://github.com/ReventlessDev/reventless-core/commit/98862adaa4111f605553e4a78bc52a8a488a4ef3))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# 2.0.0-alpha.5 (2026-08-13)
|
|
42
|
+
|
|
43
|
+
### Features
|
|
44
|
+
|
|
45
|
+
* **rescript:** bind import.meta so no module reaches for %raw ([c7bd00c](https://github.com/ReventlessDev/reventless-core/commit/c7bd00c0d49ca06cb26a0891a85921219eb87b12))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# 2.0.0-alpha.4 (2026-08-11)
|
|
49
|
+
|
|
50
|
+
**Note:** Version bump only for package @reventlessdev/rescript-node
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# 2.0.0-alpha.3 (2026-08-09)
|
|
57
|
+
|
|
58
|
+
### Features
|
|
59
|
+
|
|
60
|
+
* **core,aws:** bundle a runtime extension's companion packages, guard imports at deploy ([e975175](https://github.com/ReventlessDev/reventless-core/commit/e9751758f51582a8e46db362219f725bb5f1bcde))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# 2.0.0-alpha.2 (2026-08-05)
|
|
64
|
+
|
|
65
|
+
### Features
|
|
66
|
+
|
|
67
|
+
* **local:** persist the object store beside the SQLite database ([f37e4a1](https://github.com/ReventlessDev/reventless-core/commit/f37e4a1ad3191a97f14f3db7e2ead0e2b27b46c2))
|
|
68
|
+
|
|
69
|
+
|
|
6
70
|
# 2.0.0-alpha.1 (2026-08-02)
|
|
7
71
|
|
|
8
72
|
### Features
|
package/README.md
CHANGED
|
@@ -34,9 +34,252 @@ Add it to your `rescript.json` dependencies:
|
|
|
34
34
|
|
|
35
35
|
| Module | Binds |
|
|
36
36
|
|---|---|
|
|
37
|
+
| `NodeBuffer` | the global `Buffer` |
|
|
38
|
+
| `NodeChildProcess` | `node:child_process` |
|
|
39
|
+
| `NodeCrypto` | `node:crypto` |
|
|
40
|
+
| `NodeFs` | `node:fs`, plus `node:fs/promises` as `NodeFs.Promises` |
|
|
41
|
+
| `NodeImportMeta` | `import.meta` |
|
|
42
|
+
| `NodeModule` | `node:module` |
|
|
43
|
+
| `NodeOs` | `node:os` |
|
|
44
|
+
| `NodePath` | `node:path` |
|
|
45
|
+
| `NodeProcess` | the `process` global |
|
|
37
46
|
| `NodeStreams` | `node:stream`, plus the stream-shaped parts of `node:fs` and `node:readline` |
|
|
47
|
+
| `NodeUrl` | `node:url` |
|
|
38
48
|
| `NodeZlib` | `node:zlib` |
|
|
39
49
|
|
|
50
|
+
Every module specifier is `node:`-prefixed. Bare `"fs"` is what bundlers alias to a browser polyfill
|
|
51
|
+
shim, and this repository bundles Lambda code archives; `node:fs` is unambiguous to Node and to every
|
|
52
|
+
bundler.
|
|
53
|
+
|
|
54
|
+
Coverage is demand-driven: these bind what the Reventless packages actually call, not the whole of
|
|
55
|
+
each Node module. Where a Node API has an encoding or option argument that is only ever passed one
|
|
56
|
+
way here, the binding bakes it in rather than accepting it — `NodeFs.readFileSync` is UTF-8 by
|
|
57
|
+
construction, so no call site can pass `"utf-8"` and get a different, silently wrong, encoding.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## `NodeBuffer`
|
|
62
|
+
|
|
63
|
+
`t` aliases `Uint8Array.t` rather than being abstract: a Node `Buffer` *is* a `Uint8Array` subclass,
|
|
64
|
+
so stream chunks, `NodeFs.readFileSyncBuffer` results and `NodeFs.writeFileSyncBuffer` arguments are
|
|
65
|
+
all the same values and flow between those calls without a cast.
|
|
66
|
+
|
|
67
|
+
```rescript
|
|
68
|
+
type t = Uint8Array.t
|
|
69
|
+
|
|
70
|
+
let concat: array<t> => t // assemble a body from its `data` chunks
|
|
71
|
+
let fromStringUtf8: string => t
|
|
72
|
+
let toStringUtf8: t => string
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## `NodeChildProcess`
|
|
78
|
+
|
|
79
|
+
```rescript
|
|
80
|
+
type execOptions = {cwd?: string, encoding?: string, env?: dict<string>, stdio?: array<string>, maxBuffer?: int}
|
|
81
|
+
|
|
82
|
+
let execSync: (string, execOptions) => string
|
|
83
|
+
let execFileSync: (string, array<string>, execOptions) => string
|
|
84
|
+
|
|
85
|
+
type childProcess
|
|
86
|
+
type spawnOptions = {cwd?: string, env?: dict<string>, stdio?: array<string>}
|
|
87
|
+
|
|
88
|
+
let spawn: (string, array<string>, spawnOptions) => childProcess
|
|
89
|
+
let exitCode: childProcess => Nullable.t<int>
|
|
90
|
+
let kill: (childProcess, string) => bool
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Prefer `execFileSync` over `execSync`: it takes the arguments as an array rather than interpolating
|
|
94
|
+
them into a shell string, so an argument containing shell metacharacters stays one argument.
|
|
95
|
+
|
|
96
|
+
`spawn` returns while the child is still alive, which is the reason to reach for it over
|
|
97
|
+
`execFileSync`. `exitCode` is `null` until the child exits — the one honest way to ask "is it still
|
|
98
|
+
alive?" without holding an event listener, so a caller polling for readiness can tell a slow start
|
|
99
|
+
from a process that already died. `kill` returns whether the signal was delivered; `false` means the
|
|
100
|
+
process was already gone, which is not an error.
|
|
101
|
+
|
|
102
|
+
`spawnOptions.env` **replaces** the child's environment rather than extending it. Pass a copy of
|
|
103
|
+
`NodeProcess.env` with the additions applied when the child still needs `PATH` and friends.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## `NodeCrypto`
|
|
108
|
+
|
|
109
|
+
```rescript
|
|
110
|
+
type buffer // abstract: only ever re-keyed or stringified
|
|
111
|
+
let bufferToString: (buffer, string) => string
|
|
112
|
+
|
|
113
|
+
type hash
|
|
114
|
+
let createHash: string => hash
|
|
115
|
+
let hashUpdate: (hash, string) => hash
|
|
116
|
+
let hashUpdateBuffer: (hash, Uint8Array.t) => hash
|
|
117
|
+
let hashDigest: (hash, string) => string
|
|
118
|
+
let sha256Hex: string => string // SHA-256 of a UTF-8 string, hex-encoded
|
|
119
|
+
|
|
120
|
+
type hmac
|
|
121
|
+
let createHmac: (string, string) => hmac
|
|
122
|
+
let createHmacFromBuffer: (string, buffer) => hmac
|
|
123
|
+
let hmacUpdate: (hmac, string) => hmac
|
|
124
|
+
let hmacDigest: (hmac, string) => string
|
|
125
|
+
let hmacDigestBuffer: hmac => buffer
|
|
126
|
+
|
|
127
|
+
let randomBytes: int => buffer
|
|
128
|
+
let randomUUID: unit => string
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`sha256Hex` is the content hash content-addressed stores key their objects on (`sha256/<hash>`): the
|
|
132
|
+
same bytes always yield the same digest, so an upload is idempotent and deduplicating.
|
|
133
|
+
`createHmacFromBuffer` is the chained form — each round of an AWS SigV4 signing key takes the
|
|
134
|
+
previous round's raw digest as its key.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## `NodeFs`
|
|
139
|
+
|
|
140
|
+
```rescript
|
|
141
|
+
let existsSync: string => bool
|
|
142
|
+
let realpathSync: string => string
|
|
143
|
+
|
|
144
|
+
let readFileSync: string => string // UTF-8 baked in
|
|
145
|
+
let readFileSyncBuffer: string => Uint8Array.t
|
|
146
|
+
let writeFileSync: (string, string) => unit // UTF-8 baked in
|
|
147
|
+
let writeFileSyncBuffer: (string, Uint8Array.t) => unit
|
|
148
|
+
|
|
149
|
+
type dirent
|
|
150
|
+
let isDirectory: dirent => bool
|
|
151
|
+
let isFile: dirent => bool
|
|
152
|
+
let direntName: dirent => string
|
|
153
|
+
|
|
154
|
+
type readdirOptions = {withFileTypes: bool}
|
|
155
|
+
type mkdirOptions = {recursive?: bool}
|
|
156
|
+
type rmOptions = {recursive?: bool, force?: bool}
|
|
157
|
+
type cpOptions = {recursive?: bool}
|
|
158
|
+
|
|
159
|
+
let readdirSync: (string, readdirOptions) => array<dirent>
|
|
160
|
+
let mkdirSync: (string, mkdirOptions) => unit
|
|
161
|
+
let mkdtempSync: string => string
|
|
162
|
+
let unlinkSync: string => unit
|
|
163
|
+
let rmSync: (string, rmOptions) => unit
|
|
164
|
+
let cpSync: (string, string, cpOptions) => unit
|
|
165
|
+
|
|
166
|
+
module Promises = {
|
|
167
|
+
let writeFile: (string, string) => promise<unit>
|
|
168
|
+
let mkdir: (string, mkdirOptions) => promise<Nullable.t<string>>
|
|
169
|
+
let rm: (string, rmOptions) => promise<unit>
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `*Buffer` variants are the same Node calls without an encoding, which is what makes Node return
|
|
174
|
+
raw bytes rather than a decoded string. `Promises` is a separate module because `node:fs/promises` is
|
|
175
|
+
a separate specifier — not a wrapper this package adds over the sync calls.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## `NodeImportMeta`
|
|
180
|
+
|
|
181
|
+
```rescript
|
|
182
|
+
let url: string // import.meta.url
|
|
183
|
+
let dirname: string // import.meta.dirname
|
|
184
|
+
let filename: string // import.meta.filename
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
These resolve to the location of the module that *reads* them, which is what makes binding a
|
|
188
|
+
per-module value in a shared package sound at all: `@val` externals are inlined at the use site
|
|
189
|
+
rather than re-exported by this one. The corollary is that there are no helpers here — a function
|
|
190
|
+
defined in this module would report *this* file's location, so compute from the values at the call
|
|
191
|
+
site instead.
|
|
192
|
+
|
|
193
|
+
```rescript
|
|
194
|
+
// The file sitting next to the module that asks for it.
|
|
195
|
+
let hintsFile = NodePath.resolve([NodeImportMeta.dirname, "../ui-hints.json"])
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`dirname` and `filename` are Node's own additions to `import.meta` and are defined only for `file:`
|
|
199
|
+
URLs; a bundler that rewrites modules to CommonJS drops them. `url` is the portable form and the one
|
|
200
|
+
to reach for when either could apply.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## `NodeModule`
|
|
205
|
+
|
|
206
|
+
```rescript
|
|
207
|
+
type require
|
|
208
|
+
let createRequire: string => require
|
|
209
|
+
let requireResolve: (require, string) => string
|
|
210
|
+
let builtinModules: array<string>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`createRequire` is how an ESM module gets at CommonJS resolution, which is what `require.resolve` is
|
|
214
|
+
wanted for: locating a dependency's on-disk path without importing it. `builtinModules` lists Node's
|
|
215
|
+
built-ins, both bare and `node:`-only entries such as `node:test`, plus subpath forms like
|
|
216
|
+
`fs/promises`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## `NodeOs`
|
|
221
|
+
|
|
222
|
+
```rescript
|
|
223
|
+
let tmpdir: unit => string
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## `NodePath`
|
|
229
|
+
|
|
230
|
+
```rescript
|
|
231
|
+
let join: array<string> => string // variadic
|
|
232
|
+
let resolve: array<string> => string // variadic
|
|
233
|
+
let dirname: string => string
|
|
234
|
+
let basename: string => string
|
|
235
|
+
let relative: (string, string) => string
|
|
236
|
+
let sep: string
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`join` and `resolve` are variadic, a strict superset of the two-argument forms they replace, so no
|
|
240
|
+
call site loses expressiveness. A two-argument `join` and a variadic one are not duplicates — they
|
|
241
|
+
are different functions with the same name, and which one a call site got used to depend on which
|
|
242
|
+
inline binding block it happened to sit near.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## `NodeProcess`
|
|
247
|
+
|
|
248
|
+
```rescript
|
|
249
|
+
let argv: array<string>
|
|
250
|
+
let env: dict<string>
|
|
251
|
+
let cwd: unit => string
|
|
252
|
+
let chdir: string => unit
|
|
253
|
+
let exit: int => unit
|
|
254
|
+
|
|
255
|
+
type stream
|
|
256
|
+
let stdin: stream
|
|
257
|
+
let stdout: stream
|
|
258
|
+
let write: (stream, string) => unit
|
|
259
|
+
let pause: stream => unit
|
|
260
|
+
let unref: stream => unit
|
|
261
|
+
let isTTY: stream => option<bool>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`process` is a global rather than a module specifier, so these are `@val` bindings under
|
|
265
|
+
`@scope("process")`.
|
|
266
|
+
|
|
267
|
+
`isTTY` is `option<bool>`, not `bool`: Node sets it to `true` on an interactive stream and leaves it
|
|
268
|
+
**undefined** otherwise — it is never `false`. A `bool`-typed binding reads that undefined as a valid
|
|
269
|
+
`false`, which happens to work and is still lying about the value.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## `NodeUrl`
|
|
274
|
+
|
|
275
|
+
```rescript
|
|
276
|
+
let fileURLToPath: string => string
|
|
277
|
+
let pathToFileURL: string => {"href": string}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Only the two path/URL converters. The `URL` class itself is a WHATWG global rather than a `node:`
|
|
281
|
+
import, so it belongs to `rescript-web`, not here.
|
|
282
|
+
|
|
40
283
|
---
|
|
41
284
|
|
|
42
285
|
## `NodeStreams`
|
package/package.json
CHANGED
package/rescript.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Bindings for Node's global [`Buffer`](https://nodejs.org/api/buffer.html).
|
|
2
|
+
|
|
3
|
+
`t` aliases `Uint8Array.t` rather than being abstract: a Node `Buffer` *is* a
|
|
4
|
+
`Uint8Array` subclass, so the bytes a stream hands over, the bytes
|
|
5
|
+
{!NodeFs.readFileSyncBuffer} returns, and the bytes {!NodeFs.writeFileSyncBuffer}
|
|
6
|
+
takes are all the same values. Aliasing lets them flow between those calls
|
|
7
|
+
without a cast that would exist only to satisfy the type checker. */
|
|
8
|
+
type t = Uint8Array.t
|
|
9
|
+
|
|
10
|
+
/** Joins byte chunks into one buffer — how a request body is assembled from the
|
|
11
|
+
chunks its `data` events deliver. */
|
|
12
|
+
@val @scope("Buffer")
|
|
13
|
+
external concat: array<t> => t = "concat"
|
|
14
|
+
|
|
15
|
+
/** Bakes the encoding in for the same reason {!NodeFs.readFileSync} does: the
|
|
16
|
+
`(string, string)` form permits a different, silently wrong, encoding name. */
|
|
17
|
+
@val @scope("Buffer")
|
|
18
|
+
external fromStringUtf8: (string, @as("utf8") _) => t = "from"
|
|
19
|
+
|
|
20
|
+
/** Base64url, the alphabet a bearer token's segments are encoded in. Decoding
|
|
21
|
+
never throws — a segment that is not base64url yields whatever bytes it can,
|
|
22
|
+
so callers judge the result by whether it parses. */
|
|
23
|
+
@val @scope("Buffer")
|
|
24
|
+
external fromStringBase64Url: (string, @as("base64url") _) => t = "from"
|
|
25
|
+
|
|
26
|
+
@send
|
|
27
|
+
external toStringUtf8: (t, @as("utf8") _) => string = "toString"
|
|
28
|
+
|
|
29
|
+
@send
|
|
30
|
+
external toStringBase64Url: (t, @as("base64url") _) => string = "toString"
|
package/src/NodeChildProcess.res
CHANGED
|
@@ -16,3 +16,32 @@ external execSync: (string, execOptions) => string = "execSync"
|
|
|
16
16
|
string, so an argument containing shell metacharacters stays one argument. */
|
|
17
17
|
@module("node:child_process")
|
|
18
18
|
external execFileSync: (string, array<string>, execOptions) => string = "execFileSync"
|
|
19
|
+
|
|
20
|
+
/** A running child process. Unlike the `*Sync` calls above, `spawn` returns
|
|
21
|
+
while the child is still alive, so the caller keeps working alongside it —
|
|
22
|
+
which is the whole reason to reach for this over `execFileSync`. */
|
|
23
|
+
type childProcess
|
|
24
|
+
|
|
25
|
+
type spawnOptions = {
|
|
26
|
+
cwd?: string,
|
|
27
|
+
/** Replaces the child's environment entirely rather than extending it. Pass
|
|
28
|
+
a copy of `NodeProcess.env` with the additions applied when the child
|
|
29
|
+
still needs PATH and friends. */
|
|
30
|
+
env?: dict<string>,
|
|
31
|
+
/** Per-descriptor disposition: `"ignore"`, `"inherit"`, or `"pipe"`, in
|
|
32
|
+
stdin/stdout/stderr order. */
|
|
33
|
+
stdio?: array<string>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@module("node:child_process")
|
|
37
|
+
external spawn: (string, array<string>, spawnOptions) => childProcess = "spawn"
|
|
38
|
+
|
|
39
|
+
/** `null` while the child is running, its exit code once it has exited. The
|
|
40
|
+
one honest way to ask "is it still alive?" without holding an event
|
|
41
|
+
listener — a caller polling for readiness checks this to tell a slow start
|
|
42
|
+
from a process that already died. */
|
|
43
|
+
@get external exitCode: childProcess => Nullable.t<int> = "exitCode"
|
|
44
|
+
|
|
45
|
+
/** Signal the child. Returns whether the signal was delivered — `false` once
|
|
46
|
+
the process is already gone, which is not an error. */
|
|
47
|
+
@send external kill: (childProcess, string) => bool = "kill"
|
package/src/NodeFs.res
CHANGED
|
@@ -17,6 +17,15 @@ external existsSync: string => bool = "existsSync"
|
|
|
17
17
|
@module("node:fs")
|
|
18
18
|
external realpathSync: string => string = "realpathSync"
|
|
19
19
|
|
|
20
|
+
/** Set a file's access and modification times, in seconds since the epoch.
|
|
21
|
+
|
|
22
|
+
The reason this exists here rather than as a shell-out to `touch`: a build
|
|
23
|
+
whose compiler caches on source mtime cannot be made to re-run a side effect
|
|
24
|
+
of compilation — emitting a sidecar, say — by any argument passed to it. The
|
|
25
|
+
input has to look newer. */
|
|
26
|
+
@module("node:fs")
|
|
27
|
+
external utimesSync: (string, float, float) => unit = "utimesSync"
|
|
28
|
+
|
|
20
29
|
// ── Reading ──────────────────────────────────────────────────────────────────
|
|
21
30
|
|
|
22
31
|
@module("node:fs")
|
|
@@ -32,6 +41,40 @@ external readFileSyncBuffer: string => Uint8Array.t = "readFileSync"
|
|
|
32
41
|
@module("node:fs")
|
|
33
42
|
external writeFileSync: (string, string, @as("utf8") _) => unit = "writeFileSync"
|
|
34
43
|
|
|
44
|
+
/** The byte-oriented companion to {!writeFileSync}, mirroring
|
|
45
|
+
{!readFileSyncBuffer}: no encoding to bake in, because the payload is
|
|
46
|
+
already bytes. */
|
|
47
|
+
@module("node:fs")
|
|
48
|
+
external writeFileSyncBuffer: (string, Uint8Array.t) => unit = "writeFileSync"
|
|
49
|
+
|
|
50
|
+
// ── Watching ─────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** A handle from {!watch}. Held so it can be closed, and so it can be `unref`ed
|
|
53
|
+
— an active watcher keeps the event loop alive, which turns a stray watch in
|
|
54
|
+
a test into a run that never exits. */
|
|
55
|
+
type watcher
|
|
56
|
+
|
|
57
|
+
/** Stop watching. */
|
|
58
|
+
@send external watcherClose: watcher => unit = "close"
|
|
59
|
+
|
|
60
|
+
/** Take the watcher off the event loop's reference count, so it never by itself
|
|
61
|
+
keeps the process running. Returns the same watcher, as Node does. */
|
|
62
|
+
@send external watcherUnref: watcher => watcher = "unref"
|
|
63
|
+
|
|
64
|
+
/** [`fs.watch`](https://nodejs.org/api/fs.html#fswatchfilename-options-listener).
|
|
65
|
+
|
|
66
|
+
The listener takes the event type (`"rename"` or `"change"`) and the
|
|
67
|
+
basename, which Node may report as null on some platforms — hence
|
|
68
|
+
`Nullable.t`.
|
|
69
|
+
|
|
70
|
+
**Watch the directory, not the file**, when the file is one an editor
|
|
71
|
+
writes: a save that replaces rather than rewrites (write-temp-then-rename,
|
|
72
|
+
which is how most editors save atomically) gives the path a new inode, and a
|
|
73
|
+
watcher registered on the old one goes silent with no error. Watching the
|
|
74
|
+
containing directory and filtering on the basename survives that. */
|
|
75
|
+
@module("node:fs")
|
|
76
|
+
external watch: (string, (string, Nullable.t<string>) => unit) => watcher = "watch"
|
|
77
|
+
|
|
35
78
|
// ── Directories ──────────────────────────────────────────────────────────────
|
|
36
79
|
|
|
37
80
|
type dirent
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Bindings for [`import.meta`](https://nodejs.org/api/esm.html#importmeta).
|
|
2
|
+
|
|
3
|
+
`@val` externals are inlined at the use site rather than re-exported by this
|
|
4
|
+
module, so each of these resolves to the location of the module that *reads*
|
|
5
|
+
it — which is the only reason binding a per-module value from a shared
|
|
6
|
+
package is sound. Reading one through a wrapper function defined here would
|
|
7
|
+
return this file's location instead, so there are no helpers below.
|
|
8
|
+
|
|
9
|
+
`dirname` and `filename` are Node's own additions and are defined only for
|
|
10
|
+
`file:` URLs; a bundler that rewrites modules to CommonJS drops them. `url`
|
|
11
|
+
is the portable form and the one to reach for when either could apply. */
|
|
12
|
+
|
|
13
|
+
@val @scope(("import", "meta"))
|
|
14
|
+
external url: string = "url"
|
|
15
|
+
|
|
16
|
+
@val @scope(("import", "meta"))
|
|
17
|
+
external dirname: string = "dirname"
|
|
18
|
+
|
|
19
|
+
@val @scope(("import", "meta"))
|
|
20
|
+
external filename: string = "filename"
|
package/src/NodeModule.res
CHANGED
|
@@ -10,3 +10,8 @@ type require
|
|
|
10
10
|
external createRequire: string => require = "createRequire"
|
|
11
11
|
|
|
12
12
|
@send external requireResolve: (require, string) => string = "resolve"
|
|
13
|
+
|
|
14
|
+
/** The names of Node's built-in modules (both bare and some `node:`-only
|
|
15
|
+
entries like `node:test`), plus subpath forms such as `fs/promises`. */
|
|
16
|
+
@module("node:module")
|
|
17
|
+
external builtinModules: array<string> = "builtinModules"
|
package/src/NodeNet.res
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Bindings for [`node:net`](https://nodejs.org/api/net.html) — TCP servers and
|
|
2
|
+
the sockets they accept. */
|
|
3
|
+
|
|
4
|
+
type socket
|
|
5
|
+
type server
|
|
6
|
+
|
|
7
|
+
/** A connected client. `write` returns the backpressure flag, which a caller
|
|
8
|
+
that only ever sends short lines can ignore. */
|
|
9
|
+
@send external write: (socket, string) => bool = "write"
|
|
10
|
+
@send external destroySocket: socket => unit = "destroy"
|
|
11
|
+
|
|
12
|
+
/** `setEncoding` makes the `data` frames arrive as strings rather than Buffers,
|
|
13
|
+
which is what a line-oriented consumer wants. */
|
|
14
|
+
@send external setEncoding: (socket, string) => unit = "setEncoding"
|
|
15
|
+
|
|
16
|
+
@send external onSocketData: (socket, @as("data") _, string => unit) => unit = "on"
|
|
17
|
+
@send external onSocketClose: (socket, @as("close") _, unit => unit) => unit = "on"
|
|
18
|
+
|
|
19
|
+
/** A socket that errors is gone; without a listener the error is thrown and
|
|
20
|
+
takes the process with it, so every accepted socket needs one. */
|
|
21
|
+
@send external onSocketError: (socket, @as("error") _, JsExn.t => unit) => unit = "on"
|
|
22
|
+
|
|
23
|
+
/** `keepAlive` is deliberately absent: a caller that wants it sets it per
|
|
24
|
+
socket, and defaulting it changes the behaviour of every consumer. */
|
|
25
|
+
@module("node:net")
|
|
26
|
+
external createServer: (socket => unit) => server = "createServer"
|
|
27
|
+
|
|
28
|
+
/** Binding to an explicit host is what keeps a server off every interface —
|
|
29
|
+
pass `"127.0.0.1"` for loopback-only. */
|
|
30
|
+
@send external listen: (server, int, string, unit => unit) => unit = "listen"
|
|
31
|
+
|
|
32
|
+
@send external onServerError: (server, @as("error") _, JsExn.t => unit) => unit = "on"
|
|
33
|
+
@send external closeServer: (server, unit => unit) => unit = "close"
|
|
34
|
+
|
|
35
|
+
/** The port actually bound — the only way to learn it after listening on `0`.
|
|
36
|
+
`Nullable`, because a server that is not listening reports `null`. */
|
|
37
|
+
@send external address: server => Nullable.t<{"port": int}> = "address"
|
|
38
|
+
|
|
39
|
+
/** Lets the process exit while the server is still listening — a diagnostic
|
|
40
|
+
listener must not be the reason a CLI hangs at the end of its work. */
|
|
41
|
+
@send external unref: server => unit = "unref"
|
|
42
|
+
|
|
43
|
+
/** Connect to a listening server. The callback fires once the connection is up. */
|
|
44
|
+
@module("node:net")
|
|
45
|
+
external connect: (int, string, unit => unit) => socket = "createConnection"
|
package/src/NodeProcess.res
CHANGED
|
@@ -21,6 +21,30 @@ external chdir: string => unit = "chdir"
|
|
|
21
21
|
@val @scope("process")
|
|
22
22
|
external exit: int => unit = "exit"
|
|
23
23
|
|
|
24
|
+
@val @scope("process")
|
|
25
|
+
external pid: int = "pid"
|
|
26
|
+
|
|
27
|
+
/** Signalling another process — or, with signal `0`, asking whether it is still
|
|
28
|
+
there without disturbing it. Throws when the pid is gone (`ESRCH`) or not
|
|
29
|
+
ours to signal (`EPERM`), so a liveness check is a `try`. */
|
|
30
|
+
@val @scope("process")
|
|
31
|
+
external kill: (int, int) => unit = "kill"
|
|
32
|
+
|
|
33
|
+
/** `process.on` for the two shutdown paths a CLI has to clean up on.
|
|
34
|
+
|
|
35
|
+
Split into an `exit` binding and a signal binding because the handlers are
|
|
36
|
+
not interchangeable: `exit` fires with the exit code and may only do
|
|
37
|
+
synchronous work, while a signal handler receives the signal name and fires
|
|
38
|
+
*before* the process is committed to leaving — code that registers one and
|
|
39
|
+
expects the other's timing gets a cleanup that never runs. Signals are a
|
|
40
|
+
polyvariant so a typo is a compile error rather than a handler that is never
|
|
41
|
+
called. */
|
|
42
|
+
@val @scope("process")
|
|
43
|
+
external onExit: (@as("exit") _, int => unit) => unit = "on"
|
|
44
|
+
|
|
45
|
+
@val @scope("process")
|
|
46
|
+
external onSignal: ([#SIGINT | #SIGTERM | #SIGHUP], unit => unit) => unit = "on"
|
|
47
|
+
|
|
24
48
|
/** The standard streams. `write`, `isTTY`, `pause` and `unref` are what the
|
|
25
49
|
interactive prompts in this repository reach for; the type is abstract so it
|
|
26
50
|
can also be handed to `readline.createInterface`. */
|