@reventlessdev/rescript-node 2.0.0-alpha.3 → 2.0.0-alpha.5

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 CHANGED
@@ -3,6 +3,21 @@
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.5 (2026-08-13)
7
+
8
+ ### Features
9
+
10
+ * **rescript:** bind import.meta so no module reaches for %raw ([c7bd00c](https://github.com/ReventlessDev/reventless-core/commit/c7bd00c0d49ca06cb26a0891a85921219eb87b12))
11
+
12
+
13
+ # 2.0.0-alpha.4 (2026-08-11)
14
+
15
+ **Note:** Version bump only for package @reventlessdev/rescript-node
16
+
17
+
18
+
19
+
20
+
6
21
  # 2.0.0-alpha.3 (2026-08-09)
7
22
 
8
23
  ### 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/rescript-node",
3
- "version": "2.0.0-alpha.3",
3
+ "version": "2.0.0-alpha.5",
4
4
  "description": "ReScript bindings for the Node.js standard library",
5
5
  "license": "Apache-2.0",
6
6
  "devDependencies": {
package/rescript.json CHANGED
@@ -15,9 +15,11 @@
15
15
  "dir": "src",
16
16
  "subdirs": true,
17
17
  "public": [
18
+ "NodeBuffer",
18
19
  "NodeChildProcess",
19
20
  "NodeCrypto",
20
21
  "NodeFs",
22
+ "NodeImportMeta",
21
23
  "NodeModule",
22
24
  "NodeOs",
23
25
  "NodePath",
@@ -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"
@@ -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"
@@ -0,0 +1,2 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+ /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */