sendscript 1.1.0 → 2.1.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.
- package/.github/workflows/publish.yml +42 -5
- package/CHANGELOG.md +22 -0
- package/README.md +58 -70
- package/README.mz +46 -47
- package/cli.mjs +3 -2
- package/example/client.socket.io.mjs +5 -5
- package/example/server.socket.io.mjs +2 -1
- package/example/typescript/client.ts +6 -6
- package/example/typescript/docs/functions/add.md +1 -1
- package/example/typescript/docs/functions/square.md +1 -1
- package/example/typescript/math.client.ts +3 -7
- package/index.test.mjs +154 -26
- package/leaf-serializer.test.mjs +10 -15
- package/package.json +4 -2
- package/parse.mjs +90 -84
- package/{module.mjs → references.mjs} +14 -15
- package/references.test.mjs +8 -0
- package/repl.mjs +2 -2
- package/stringify.mjs +6 -2
- package/index.mjs +0 -11
|
@@ -1,25 +1,62 @@
|
|
|
1
|
-
name: Publish
|
|
1
|
+
name: Release and Publish
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
|
-
- '
|
|
6
|
+
- '*'
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
|
+
contents: write
|
|
9
10
|
id-token: write # Required for OIDC
|
|
10
|
-
contents: read
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
node: ['lts/*', 'latest']
|
|
18
|
+
fail-fast: true
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-node@v6
|
|
24
|
+
with:
|
|
25
|
+
node-version: ${{ matrix.node }}
|
|
26
|
+
cache: npm
|
|
27
|
+
|
|
28
|
+
- run: npm ci
|
|
29
|
+
- run: npm test
|
|
30
|
+
|
|
31
|
+
release:
|
|
32
|
+
needs: test
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v6
|
|
36
|
+
|
|
37
|
+
- uses: actions/setup-node@v6
|
|
38
|
+
with:
|
|
39
|
+
node-version: 'latest'
|
|
40
|
+
cache: npm
|
|
41
|
+
|
|
42
|
+
- run: npm ci
|
|
43
|
+
- name: Create GitHub Release
|
|
44
|
+
uses: softprops/action-gh-release@v2
|
|
45
|
+
with:
|
|
46
|
+
generate_release_notes: true
|
|
47
|
+
|
|
13
48
|
publish:
|
|
49
|
+
needs: test
|
|
14
50
|
runs-on: ubuntu-latest
|
|
15
51
|
steps:
|
|
16
52
|
- uses: actions/checkout@v6
|
|
17
53
|
|
|
18
54
|
- uses: actions/setup-node@v6
|
|
19
55
|
with:
|
|
20
|
-
node-version: '
|
|
56
|
+
node-version: 'latest'
|
|
57
|
+
cache: npm
|
|
21
58
|
registry-url: 'https://registry.npmjs.org'
|
|
59
|
+
|
|
22
60
|
- run: npm ci
|
|
23
|
-
- run: npm run build --if-present
|
|
24
61
|
- run: npm test
|
|
25
62
|
- run: npm publish
|
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,30 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v2.1.0](https://github.com/bas080/sendscript/compare/v2.0.1...v2.1.0)
|
|
8
|
+
|
|
9
|
+
- Make eval order when using await inline with JS [`00db8aa`](https://github.com/bas080/sendscript/commit/00db8aaa563378db814035e3e550269593ed82e2)
|
|
10
|
+
- Merge release workflow into publish [`6a5db12`](https://github.com/bas080/sendscript/commit/6a5db128b659dd295390505b2e470c5913617abf)
|
|
11
|
+
- Fix the cli repl [`3cf8b0d`](https://github.com/bas080/sendscript/commit/3cf8b0d9b4531989fb01a53fa9ab4cb6a011c183)
|
|
12
|
+
|
|
13
|
+
#### [v2.0.1](https://github.com/bas080/sendscript/compare/v2.0.0...v2.0.1)
|
|
14
|
+
|
|
15
|
+
> 8 April 2026
|
|
16
|
+
|
|
17
|
+
- Publish to npm on tag push [`357ac0e`](https://github.com/bas080/sendscript/commit/357ac0ef3384f294e4f9bb179d5a675afda5fd19)
|
|
18
|
+
|
|
19
|
+
### [v2.0.0](https://github.com/bas080/sendscript/compare/v1.1.0...v2.0.0)
|
|
20
|
+
|
|
21
|
+
> 8 April 2026
|
|
22
|
+
|
|
23
|
+
- Rewrite to have parse take schema to prevent accessing refs [`df16f85`](https://github.com/bas080/sendscript/commit/df16f85c79299aa8bcd3e44b6cc127bd7198eace)
|
|
24
|
+
- Run tests before release and publish on release [`10442d6`](https://github.com/bas080/sendscript/commit/10442d6766a098926ee98daee08dba23eea0fb14)
|
|
25
|
+
- Create release on tag with github workflows [`4ec222b`](https://github.com/bas080/sendscript/commit/4ec222bf51fc1d3cce9fc54fdd1e9639b5d259cd)
|
|
26
|
+
|
|
7
27
|
#### [v1.1.0](https://github.com/bas080/sendscript/compare/v1.0.6...v1.1.0)
|
|
8
28
|
|
|
29
|
+
> 7 April 2026
|
|
30
|
+
|
|
9
31
|
- Support custom serialization of leaf nodes [`d56f1b7`](https://github.com/bas080/sendscript/commit/d56f1b750e2d7faf72d69f9ba8e9bb62ef5ebce7)
|
|
10
32
|
- Support nested modules [`88e69e5`](https://github.com/bas080/sendscript/commit/88e69e5c05ddb85aac1797a020e92519c986e89f)
|
|
11
33
|
- Document new nesting and leaf serialization feature [`b3ee9f6`](https://github.com/bas080/sendscript/commit/b3ee9f693dc6137b01c9f0072aa2dd1e940f07c9)
|
package/README.md
CHANGED
|
@@ -53,10 +53,11 @@ using more advanced (de)serialization libraries.
|
|
|
53
53
|
SendScript produces an intermediate JSON representation of the program. Let's see what that looks like.
|
|
54
54
|
|
|
55
55
|
```js
|
|
56
|
-
import
|
|
57
|
-
import
|
|
56
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
57
|
+
import references from 'sendscript/references.mjs'
|
|
58
58
|
|
|
59
|
-
const { add } =
|
|
59
|
+
const { add } = references(['add'])
|
|
60
|
+
const stringify = Stringify()
|
|
60
61
|
|
|
61
62
|
console.log(stringify(add(1,2)))
|
|
62
63
|
```
|
|
@@ -75,7 +76,7 @@ const module = {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
const parse = Parse(module)
|
|
79
|
+
const parse = Parse(['add'], module)
|
|
79
80
|
|
|
80
81
|
const program = '["call",["ref","add"],[1,2]]'
|
|
81
82
|
|
|
@@ -91,7 +92,7 @@ composition and even await.
|
|
|
91
92
|
This package is nothing more than the absolute core of sendscript. It
|
|
92
93
|
includes:
|
|
93
94
|
|
|
94
|
-
- The `
|
|
95
|
+
- The `references` function to create stubs to write the programs.
|
|
95
96
|
- `stringify` which takes the program and returns a JSON string.
|
|
96
97
|
- `parse` which takes the `stringify` JSON string and a real module and returns the result.
|
|
97
98
|
|
|
@@ -131,7 +132,8 @@ import { Server } from 'socket.io'
|
|
|
131
132
|
import Parse from 'sendscript/parse.mjs'
|
|
132
133
|
import * as math from './math.mjs'
|
|
133
134
|
|
|
134
|
-
const
|
|
135
|
+
const schema = Object.keys(math)
|
|
136
|
+
const parse = Parse(schema, math)
|
|
135
137
|
const server = new Server()
|
|
136
138
|
const port = process.env.PORT || 3000
|
|
137
139
|
|
|
@@ -158,11 +160,13 @@ Now for a client that sends a program to the server.
|
|
|
158
160
|
// ./example/client.socket.io.mjs
|
|
159
161
|
|
|
160
162
|
import socketClient from 'socket.io-client'
|
|
161
|
-
import
|
|
162
|
-
import
|
|
163
|
-
import * as math from './math.mjs'
|
|
163
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
164
|
+
import references from 'sendscript/references.mjs'
|
|
164
165
|
import assert from 'node:assert'
|
|
165
166
|
|
|
167
|
+
const { add, square } = references(['add', 'square'])
|
|
168
|
+
const stringify = Stringify()
|
|
169
|
+
|
|
166
170
|
const port = process.env.PORT || 3000
|
|
167
171
|
const client = socketClient(`http://localhost:${port}`)
|
|
168
172
|
|
|
@@ -176,8 +180,6 @@ const send = program => {
|
|
|
176
180
|
})
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
const { add, square } = module(math)
|
|
180
|
-
|
|
181
183
|
// The program to be sent over the wire
|
|
182
184
|
const program = square(add(1, add(add(2, 3), 4)))
|
|
183
185
|
|
|
@@ -253,52 +255,31 @@ export const add = (a: number, b: number) => a + b
|
|
|
253
255
|
export const square = (a: number) => a * a
|
|
254
256
|
```
|
|
255
257
|
|
|
256
|
-
We
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
cat ./example/typescript/math.client.ts
|
|
260
|
-
```
|
|
261
|
-
```ts
|
|
262
|
-
import module from 'sendscript/module.mjs'
|
|
263
|
-
import type * as mathTypes from './math.ts'
|
|
264
|
-
|
|
265
|
-
const math = module([
|
|
266
|
-
'add',
|
|
267
|
-
'square'
|
|
268
|
-
]) as typeof mathTypes
|
|
269
|
-
|
|
270
|
-
export default math
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
We now use the client version of this module.
|
|
258
|
+
We can then coerce the types of the instrumented stubs.
|
|
274
259
|
|
|
275
260
|
```bash
|
|
276
261
|
cat ./example/typescript/client.ts
|
|
277
262
|
```
|
|
278
263
|
```ts
|
|
279
|
-
import
|
|
264
|
+
import math from './math.client.ts'
|
|
265
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
266
|
+
|
|
267
|
+
const stringify = Stringify()
|
|
280
268
|
|
|
281
|
-
|
|
269
|
+
// The return type of this function matches the type passed as the return of the program.
|
|
270
|
+
async function send<T>(program: T): Promise<T> {
|
|
282
271
|
return (await fetch('/api', {
|
|
283
272
|
method: 'POST',
|
|
284
273
|
body: stringify(program)
|
|
285
274
|
})).json()
|
|
286
275
|
}
|
|
287
276
|
|
|
288
|
-
import math from './math.client.ts'
|
|
289
|
-
|
|
290
|
-
const { add, square } = math
|
|
291
|
-
|
|
292
277
|
send(square(add(1, 2)))
|
|
293
278
|
```
|
|
294
279
|
|
|
295
280
|
We'll also generate the docs for this module.
|
|
296
281
|
|
|
297
282
|
```bash
|
|
298
|
-
npm install --no-save \
|
|
299
|
-
typedoc \
|
|
300
|
-
typedoc-plugin-markdown
|
|
301
|
-
|
|
302
283
|
npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./example/typescript/math.ts
|
|
303
284
|
```
|
|
304
285
|
|
|
@@ -316,32 +297,33 @@ Sendscript allows you to define your API as a **nested object of functions**, ma
|
|
|
316
297
|
|
|
317
298
|
### Defining a Nested Module
|
|
318
299
|
|
|
319
|
-
You can define a schema as
|
|
320
|
-
|
|
321
|
-
1. **An object with nested objects** – submodules.
|
|
322
|
-
2. **An array of function names** – automatically instrumented.
|
|
300
|
+
You can define a schema as **an array of function names**
|
|
323
301
|
|
|
324
302
|
```js
|
|
325
|
-
import module from 'sendscript/module.mjs'
|
|
326
|
-
|
|
327
|
-
const myModule = module({
|
|
328
|
-
math: ['add', 'sub'],
|
|
329
|
-
|
|
330
|
-
// Use an object with keys and true value
|
|
331
|
-
vector: {
|
|
332
|
-
add: true,
|
|
333
|
-
multiply: true
|
|
334
|
-
},
|
|
335
303
|
|
|
336
|
-
|
|
337
|
-
|
|
304
|
+
const schema = [
|
|
305
|
+
'help',
|
|
306
|
+
'version',
|
|
307
|
+
|
|
308
|
+
['math', [
|
|
309
|
+
'add',
|
|
310
|
+
'sub'
|
|
311
|
+
]],
|
|
312
|
+
['vector', [
|
|
313
|
+
'add',
|
|
314
|
+
'multiply'
|
|
315
|
+
]],
|
|
316
|
+
['utils', [
|
|
317
|
+
'identity',
|
|
318
|
+
'always'
|
|
319
|
+
]],
|
|
338
320
|
})
|
|
339
321
|
```
|
|
340
322
|
|
|
341
323
|
Functions are referenced via their **path in the module tree**:
|
|
342
324
|
|
|
343
325
|
```js
|
|
344
|
-
const { math, vector } =
|
|
326
|
+
const { math, vector } = references(schema)
|
|
345
327
|
|
|
346
328
|
math.add(
|
|
347
329
|
1,
|
|
@@ -389,9 +371,9 @@ Here's how to use [superjson](https://github.com/blitz-js/superjson) to support
|
|
|
389
371
|
|
|
390
372
|
```js
|
|
391
373
|
import SuperJSON from 'superjson'
|
|
392
|
-
import
|
|
374
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
375
|
+
import references from 'sendscript/references.mjs'
|
|
393
376
|
import Parse from 'sendscript/parse.mjs'
|
|
394
|
-
import module from 'sendscript/module.mjs'
|
|
395
377
|
|
|
396
378
|
const leafSerializer = (value) => {
|
|
397
379
|
if (value === undefined) return JSON.stringify({ __undefined__: true })
|
|
@@ -404,7 +386,10 @@ const leafDeserializer = (text) => {
|
|
|
404
386
|
return SuperJSON.deserialize(parsed)
|
|
405
387
|
}
|
|
406
388
|
|
|
407
|
-
const
|
|
389
|
+
const schema = ['processData']
|
|
390
|
+
|
|
391
|
+
const { processData } = references(schema)
|
|
392
|
+
const stringify = Stringify(leafSerializer)
|
|
408
393
|
|
|
409
394
|
// Program with Date, RegExp, and other types
|
|
410
395
|
const program = {
|
|
@@ -416,17 +401,20 @@ const program = {
|
|
|
416
401
|
}
|
|
417
402
|
|
|
418
403
|
// Serialize with custom leaf serializer
|
|
419
|
-
const json = stringify(processData(program)
|
|
404
|
+
const json = stringify(processData(program))
|
|
420
405
|
|
|
421
|
-
//
|
|
422
|
-
const
|
|
406
|
+
// The environment
|
|
407
|
+
const env = {
|
|
423
408
|
processData: (data) => ({
|
|
424
409
|
success: true,
|
|
425
410
|
received: data
|
|
426
411
|
})
|
|
427
|
-
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Parse with custom leaf deserializer
|
|
415
|
+
const parse = Parse(schema, env, leadDeserializer)
|
|
428
416
|
|
|
429
|
-
const result = parse(json
|
|
417
|
+
const result = parse(json)
|
|
430
418
|
```
|
|
431
419
|
|
|
432
420
|
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous and safe from colliding with SendScript operators.
|
|
@@ -441,19 +429,19 @@ npm t -- report text-summary
|
|
|
441
429
|
```
|
|
442
430
|
```
|
|
443
431
|
|
|
444
|
-
> sendscript@
|
|
432
|
+
> sendscript@2.1.0 test
|
|
445
433
|
> tap -R silent
|
|
446
434
|
|
|
447
435
|
|
|
448
|
-
> sendscript@
|
|
436
|
+
> sendscript@2.1.0 test
|
|
449
437
|
> tap report text-summary
|
|
450
438
|
|
|
451
439
|
|
|
452
440
|
=============================== Coverage summary ===============================
|
|
453
|
-
Statements : 100% (
|
|
454
|
-
Branches : 100% (
|
|
455
|
-
Functions : 100% (
|
|
456
|
-
Lines : 100% (
|
|
441
|
+
Statements : 100% ( 326/326 )
|
|
442
|
+
Branches : 100% ( 118/118 )
|
|
443
|
+
Functions : 100% ( 21/21 )
|
|
444
|
+
Lines : 100% ( 326/326 )
|
|
457
445
|
================================================================================
|
|
458
446
|
```
|
|
459
447
|
|
package/README.mz
CHANGED
|
@@ -30,10 +30,11 @@ using more advanced (de)serialization libraries.
|
|
|
30
30
|
SendScript produces an intermediate JSON representation of the program. Let's see what that looks like.
|
|
31
31
|
|
|
32
32
|
```js|json node --input-type=module | tee /tmp/sendscript.json
|
|
33
|
-
import
|
|
34
|
-
import
|
|
33
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
34
|
+
import references from 'sendscript/references.mjs'
|
|
35
35
|
|
|
36
|
-
const { add } =
|
|
36
|
+
const { add } = references(['add'])
|
|
37
|
+
const stringify = Stringify()
|
|
37
38
|
|
|
38
39
|
console.log(stringify(add(1,2)))
|
|
39
40
|
```
|
|
@@ -49,7 +50,7 @@ const module = {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const parse = Parse(module)
|
|
53
|
+
const parse = Parse(['add'], module)
|
|
53
54
|
|
|
54
55
|
const program = '["call",["ref","add"],[1,2]]'
|
|
55
56
|
|
|
@@ -62,7 +63,7 @@ composition and even await.
|
|
|
62
63
|
This package is nothing more than the absolute core of sendscript. It
|
|
63
64
|
includes:
|
|
64
65
|
|
|
65
|
-
- The `
|
|
66
|
+
- The `references` function to create stubs to write the programs.
|
|
66
67
|
- `stringify` which takes the program and returns a JSON string.
|
|
67
68
|
- `parse` which takes the `stringify` JSON string and a real module and returns the result.
|
|
68
69
|
|
|
@@ -102,7 +103,8 @@ import { Server } from 'socket.io'
|
|
|
102
103
|
import Parse from 'sendscript/parse.mjs'
|
|
103
104
|
import * as math from './math.mjs'
|
|
104
105
|
|
|
105
|
-
const
|
|
106
|
+
const schema = Object.keys(math)
|
|
107
|
+
const parse = Parse(schema, math)
|
|
106
108
|
const server = new Server()
|
|
107
109
|
const port = process.env.PORT || 3000
|
|
108
110
|
|
|
@@ -129,11 +131,13 @@ Now for a client that sends a program to the server.
|
|
|
129
131
|
// ./example/client.socket.io.mjs
|
|
130
132
|
|
|
131
133
|
import socketClient from 'socket.io-client'
|
|
132
|
-
import
|
|
133
|
-
import
|
|
134
|
-
import * as math from './math.mjs'
|
|
134
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
135
|
+
import references from 'sendscript/references.mjs'
|
|
135
136
|
import assert from 'node:assert'
|
|
136
137
|
|
|
138
|
+
const { add, square } = references(['add', 'square'])
|
|
139
|
+
const stringify = Stringify()
|
|
140
|
+
|
|
137
141
|
const port = process.env.PORT || 3000
|
|
138
142
|
const client = socketClient(`http://localhost:${port}`)
|
|
139
143
|
|
|
@@ -147,8 +151,6 @@ const send = program => {
|
|
|
147
151
|
})
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
const { add, square } = module(math)
|
|
151
|
-
|
|
152
154
|
// The program to be sent over the wire
|
|
153
155
|
const program = square(add(1, add(add(2, 3), 4)))
|
|
154
156
|
|
|
@@ -217,13 +219,7 @@ Let's say we have this module which we use on the server.
|
|
|
217
219
|
cat ./example/typescript/math.ts
|
|
218
220
|
```
|
|
219
221
|
|
|
220
|
-
We
|
|
221
|
-
|
|
222
|
-
```bash|ts bash
|
|
223
|
-
cat ./example/typescript/math.client.ts
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
We now use the client version of this module.
|
|
222
|
+
We can then coerce the types of the instrumented stubs.
|
|
227
223
|
|
|
228
224
|
```bash|ts bash
|
|
229
225
|
cat ./example/typescript/client.ts
|
|
@@ -232,10 +228,6 @@ cat ./example/typescript/client.ts
|
|
|
232
228
|
We'll also generate the docs for this module.
|
|
233
229
|
|
|
234
230
|
```bash bash 1>&2
|
|
235
|
-
npm install --no-save \
|
|
236
|
-
typedoc \
|
|
237
|
-
typedoc-plugin-markdown
|
|
238
|
-
|
|
239
231
|
npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./example/typescript/math.ts
|
|
240
232
|
```
|
|
241
233
|
|
|
@@ -253,32 +245,33 @@ Sendscript allows you to define your API as a **nested object of functions**, ma
|
|
|
253
245
|
|
|
254
246
|
### Defining a Nested Module
|
|
255
247
|
|
|
256
|
-
You can define a schema as
|
|
257
|
-
|
|
258
|
-
1. **An object with nested objects** – submodules.
|
|
259
|
-
2. **An array of function names** – automatically instrumented.
|
|
248
|
+
You can define a schema as **an array of function names**
|
|
260
249
|
|
|
261
250
|
```js
|
|
262
|
-
import module from 'sendscript/module.mjs'
|
|
263
251
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
add
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
252
|
+
const schema = [
|
|
253
|
+
'help',
|
|
254
|
+
'version',
|
|
255
|
+
|
|
256
|
+
['math', [
|
|
257
|
+
'add',
|
|
258
|
+
'sub'
|
|
259
|
+
]],
|
|
260
|
+
['vector', [
|
|
261
|
+
'add',
|
|
262
|
+
'multiply'
|
|
263
|
+
]],
|
|
264
|
+
['utils', [
|
|
265
|
+
'identity',
|
|
266
|
+
'always'
|
|
267
|
+
]],
|
|
275
268
|
})
|
|
276
269
|
```
|
|
277
270
|
|
|
278
271
|
Functions are referenced via their **path in the module tree**:
|
|
279
272
|
|
|
280
273
|
```js
|
|
281
|
-
const { math, vector } =
|
|
274
|
+
const { math, vector } = references(schema)
|
|
282
275
|
|
|
283
276
|
math.add(
|
|
284
277
|
1,
|
|
@@ -326,9 +319,9 @@ Here's how to use [superjson](https://github.com/blitz-js/superjson) to support
|
|
|
326
319
|
|
|
327
320
|
```js
|
|
328
321
|
import SuperJSON from 'superjson'
|
|
329
|
-
import
|
|
322
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
323
|
+
import references from 'sendscript/references.mjs'
|
|
330
324
|
import Parse from 'sendscript/parse.mjs'
|
|
331
|
-
import module from 'sendscript/module.mjs'
|
|
332
325
|
|
|
333
326
|
const leafSerializer = (value) => {
|
|
334
327
|
if (value === undefined) return JSON.stringify({ __undefined__: true })
|
|
@@ -341,7 +334,10 @@ const leafDeserializer = (text) => {
|
|
|
341
334
|
return SuperJSON.deserialize(parsed)
|
|
342
335
|
}
|
|
343
336
|
|
|
344
|
-
const
|
|
337
|
+
const schema = ['processData']
|
|
338
|
+
|
|
339
|
+
const { processData } = references(schema)
|
|
340
|
+
const stringify = Stringify(leafSerializer)
|
|
345
341
|
|
|
346
342
|
// Program with Date, RegExp, and other types
|
|
347
343
|
const program = {
|
|
@@ -353,17 +349,20 @@ const program = {
|
|
|
353
349
|
}
|
|
354
350
|
|
|
355
351
|
// Serialize with custom leaf serializer
|
|
356
|
-
const json = stringify(processData(program)
|
|
352
|
+
const json = stringify(processData(program))
|
|
357
353
|
|
|
358
|
-
//
|
|
359
|
-
const
|
|
354
|
+
// The environment
|
|
355
|
+
const env = {
|
|
360
356
|
processData: (data) => ({
|
|
361
357
|
success: true,
|
|
362
358
|
received: data
|
|
363
359
|
})
|
|
364
|
-
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Parse with custom leaf deserializer
|
|
363
|
+
const parse = Parse(schema, env, leadDeserializer)
|
|
365
364
|
|
|
366
|
-
const result = parse(json
|
|
365
|
+
const result = parse(json)
|
|
367
366
|
```
|
|
368
367
|
|
|
369
368
|
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous and safe from colliding with SendScript operators.
|
package/cli.mjs
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { pathToFileURL } from 'url'
|
|
5
|
-
import
|
|
5
|
+
import Stringify from './stringify.mjs'
|
|
6
6
|
import repl from './repl.mjs'
|
|
7
7
|
import Parse from './parse.mjs'
|
|
8
8
|
|
|
9
|
+
const stringify = Stringify()
|
|
9
10
|
const modulePath = pathToFileURL(path.resolve(process.cwd(), process.argv[2])).href
|
|
10
11
|
const mod = await import(modulePath)
|
|
11
|
-
const parse = Parse(mod)
|
|
12
|
+
const parse = Parse(Object.keys(mod), mod)
|
|
12
13
|
|
|
13
14
|
const send = program => parse(stringify(program))
|
|
14
15
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// ./example/client.socket.io.mjs
|
|
2
2
|
|
|
3
3
|
import socketClient from 'socket.io-client'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import * as math from './math.mjs'
|
|
4
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
5
|
+
import references from 'sendscript/references.mjs'
|
|
7
6
|
import assert from 'node:assert'
|
|
8
7
|
|
|
8
|
+
const { add, square } = references(['add', 'square'])
|
|
9
|
+
const stringify = Stringify()
|
|
10
|
+
|
|
9
11
|
const port = process.env.PORT || 3000
|
|
10
12
|
const client = socketClient(`http://localhost:${port}`)
|
|
11
13
|
|
|
@@ -19,8 +21,6 @@ const send = program => {
|
|
|
19
21
|
})
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
const { add, square } = module(math)
|
|
23
|
-
|
|
24
24
|
// The program to be sent over the wire
|
|
25
25
|
const program = square(add(1, add(add(2, 3), 4)))
|
|
26
26
|
|
|
@@ -4,7 +4,8 @@ import { Server } from 'socket.io'
|
|
|
4
4
|
import Parse from 'sendscript/parse.mjs'
|
|
5
5
|
import * as math from './math.mjs'
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const schema = Object.keys(math)
|
|
8
|
+
const parse = Parse(schema, math)
|
|
8
9
|
const server = new Server()
|
|
9
10
|
const port = process.env.PORT || 3000
|
|
10
11
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import math from './math.client.ts'
|
|
2
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const stringify = Stringify()
|
|
5
|
+
|
|
6
|
+
// The return type of this function matches the type passed as the return of the program.
|
|
7
|
+
async function send<T>(program: T): Promise<T> {
|
|
4
8
|
return (await fetch('/api', {
|
|
5
9
|
method: 'POST',
|
|
6
10
|
body: stringify(program)
|
|
7
11
|
})).json()
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
import math from './math.client.ts'
|
|
11
|
-
|
|
12
|
-
const { add, square } = math
|
|
13
|
-
|
|
14
14
|
send(square(add(1, 2)))
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> **add**(`a`, `b`): `number`
|
|
10
10
|
|
|
11
|
-
Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/
|
|
11
|
+
Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/3d843187a56e23f0e70f11e904832efb1abd9cf1/example/typescript/math.ts#L1)
|
|
12
12
|
|
|
13
13
|
## Parameters
|
|
14
14
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> **square**(`a`): `number`
|
|
10
10
|
|
|
11
|
-
Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/
|
|
11
|
+
Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/3d843187a56e23f0e70f11e904832efb1abd9cf1/example/typescript/math.ts#L2)
|
|
12
12
|
|
|
13
13
|
## Parameters
|
|
14
14
|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import module from 'sendscript/module.mjs'
|
|
2
1
|
import type * as mathTypes from './math.ts'
|
|
2
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
3
|
+
import references from 'sendscript/references.mjs'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
'add',
|
|
6
|
-
'square'
|
|
7
|
-
]) as typeof mathTypes
|
|
8
|
-
|
|
9
|
-
export default math
|
|
5
|
+
export default references(['add', 'square']) as typeof mathTypes
|
package/index.test.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { test } from 'tap'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import Stringify from './stringify.mjs'
|
|
3
|
+
import references from './references.mjs'
|
|
4
|
+
import Parse from './parse.mjs'
|
|
5
|
+
|
|
6
|
+
const order = []
|
|
5
7
|
|
|
6
8
|
const myModule = {
|
|
7
9
|
nested: {
|
|
@@ -12,6 +14,16 @@ const myModule = {
|
|
|
12
14
|
add: (a, b) => a + b,
|
|
13
15
|
identity: (x) => x,
|
|
14
16
|
concat: (a, b) => a.concat(b),
|
|
17
|
+
delay: (value, ms) => {
|
|
18
|
+
return new Promise(resolve => {
|
|
19
|
+
order.push(`${value} start`)
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
order.push(`${value} end`)
|
|
22
|
+
|
|
23
|
+
resolve(value)
|
|
24
|
+
}, ms)
|
|
25
|
+
})
|
|
26
|
+
},
|
|
15
27
|
toArray: (...array) => array,
|
|
16
28
|
always: (x) => () => x,
|
|
17
29
|
multiply3: (a) => (b) => (c) => a * b * c,
|
|
@@ -30,27 +42,19 @@ const myModule = {
|
|
|
30
42
|
obj.b = 'c'
|
|
31
43
|
return obj
|
|
32
44
|
},
|
|
33
|
-
Function,
|
|
34
45
|
Promise
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
const schema = Object.keys(myModule)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}, {})
|
|
42
|
-
|
|
43
|
-
schema.nested = { again: ['T'] }
|
|
44
|
-
|
|
45
|
-
const sendscript = {
|
|
46
|
-
stringify,
|
|
47
|
-
parse: ssparse(myModule),
|
|
48
|
-
module: module(schema)
|
|
49
|
-
}
|
|
48
|
+
const schema = Object.keys(myModule)
|
|
49
|
+
schema.push(['nested', [
|
|
50
|
+
['again', ['T']]
|
|
51
|
+
]])
|
|
50
52
|
|
|
51
|
-
const
|
|
53
|
+
const api = references(schema)
|
|
54
|
+
const stringify = Stringify()
|
|
55
|
+
const parse = Parse(schema, myModule)
|
|
52
56
|
|
|
53
|
-
const run = (program) =>
|
|
57
|
+
const run = (program) => parse(stringify(program))
|
|
54
58
|
|
|
55
59
|
const RealPromise = Promise
|
|
56
60
|
|
|
@@ -59,9 +63,9 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
59
63
|
aPromise,
|
|
60
64
|
asyncAdd,
|
|
61
65
|
resolve,
|
|
66
|
+
delay,
|
|
62
67
|
delayedIdentity,
|
|
63
68
|
noop,
|
|
64
|
-
Function,
|
|
65
69
|
Promise,
|
|
66
70
|
instanceOf,
|
|
67
71
|
asyncFn,
|
|
@@ -74,7 +78,58 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
74
78
|
always,
|
|
75
79
|
multiply3,
|
|
76
80
|
nested
|
|
77
|
-
} =
|
|
81
|
+
} = api
|
|
82
|
+
|
|
83
|
+
t.test('await evaluation order matches JS semantics', async (t) => {
|
|
84
|
+
const lorder = []
|
|
85
|
+
|
|
86
|
+
const ldelay = (label, ms) => new RealPromise((resolve) => {
|
|
87
|
+
lorder.push(label + ' start')
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
lorder.push(label + ' end')
|
|
90
|
+
resolve(label)
|
|
91
|
+
}, ms)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const lresult = [
|
|
95
|
+
await ldelay('a', 30),
|
|
96
|
+
await ldelay('b', 10)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
t.same(lresult, ['a', 'b'])
|
|
100
|
+
t.same(lorder, [
|
|
101
|
+
'a start',
|
|
102
|
+
'a end',
|
|
103
|
+
'b start',
|
|
104
|
+
'b end'
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
// TODO: Write it what ss touches
|
|
108
|
+
|
|
109
|
+
const result = await run([
|
|
110
|
+
await delay('a', 30),
|
|
111
|
+
await delay('b', 10)
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
t.same(result, ['a', 'b'])
|
|
115
|
+
t.same(order, [
|
|
116
|
+
'a start',
|
|
117
|
+
'a end',
|
|
118
|
+
'b start',
|
|
119
|
+
'b end'
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
t.end()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
t.test('mix await without await', async t => {
|
|
126
|
+
const [one, two] = await run([await resolve(1), resolve(2)])
|
|
127
|
+
|
|
128
|
+
t.equal(one, 1)
|
|
129
|
+
t.ok(two instanceof RealPromise)
|
|
130
|
+
|
|
131
|
+
t.end()
|
|
132
|
+
})
|
|
78
133
|
|
|
79
134
|
t.test('calling nested function works', t => {
|
|
80
135
|
t.equal(run(nested.again.T()), true)
|
|
@@ -143,9 +198,6 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
143
198
|
t.strictSame(await run(asyncFn()), 'my-async-function')
|
|
144
199
|
t.strictSame(await run(await resolve('my-promise')), 'my-promise')
|
|
145
200
|
t.strictSame(run(instanceOf(resolve(asyncFn), Promise)), true)
|
|
146
|
-
t.strictSame(
|
|
147
|
-
await run(instanceOf(await resolve(asyncFn), Function)), true
|
|
148
|
-
)
|
|
149
201
|
t.strictSame(
|
|
150
202
|
await run({ a: await resolve('b') }),
|
|
151
203
|
{ a: 'b' }
|
|
@@ -218,8 +270,9 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
218
270
|
})
|
|
219
271
|
|
|
220
272
|
t.test('null-prototype object traversal', (t) => {
|
|
221
|
-
const { nullProto } =
|
|
222
|
-
|
|
273
|
+
const { nullProto } = api
|
|
274
|
+
const other = myModule.nullProto()
|
|
275
|
+
t.strictSame(run({ a: nullProto() }), { a: other })
|
|
223
276
|
t.end()
|
|
224
277
|
})
|
|
225
278
|
|
|
@@ -238,3 +291,78 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
238
291
|
t.end()
|
|
239
292
|
})
|
|
240
293
|
})
|
|
294
|
+
|
|
295
|
+
test('stringify: invalid children throws', t => {
|
|
296
|
+
const invalidSchema = [
|
|
297
|
+
['math', 'add']
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
t.throws(() => Parse(invalidSchema, {}))
|
|
301
|
+
t.throws(() => Parse([Symbol('no-allowed')], {}))
|
|
302
|
+
t.end()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('forbidden/reflection access should be blocked', async (t) => {
|
|
306
|
+
function run (program) {
|
|
307
|
+
return parse(JSON.stringify(program))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
t.test('cannot access constructor via ref', (t) => {
|
|
311
|
+
// try to reach the constructor of nested.again.T
|
|
312
|
+
t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor']))
|
|
313
|
+
t.end()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
t.test('cannot access prototype property', (t) => {
|
|
317
|
+
// direct prototype traversal attempt
|
|
318
|
+
t.throws(() => run(['ref', 'nested', '__proto__']))
|
|
319
|
+
t.throws(() => run(['ref', 'nested', 'again', '__proto__']))
|
|
320
|
+
t.end()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
t.test('cannot reach Function constructor', (t) => {
|
|
324
|
+
// many modules may accidentally expose Function; this should not be available via ref
|
|
325
|
+
t.throws(() => run(['ref', 'Function']))
|
|
326
|
+
t.end()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
t.test('cannot call Function to execute code', (t) => {
|
|
330
|
+
// If Function were reachable, this would execute arbitrary code
|
|
331
|
+
const program = ['call', ['ref', 'Function'], [['leaf', '"return 1 + 2"']]]
|
|
332
|
+
t.throws(() => run(program))
|
|
333
|
+
t.end()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
t.test('cannot access global Promise constructor via ref', (t) => {
|
|
337
|
+
t.throws(() => run(['ref', 'Promise', 'prototype', 'then']))
|
|
338
|
+
t.end()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
t.test('cannot access Object.prototype methods', (t) => {
|
|
342
|
+
// toString is a common vector for prototype access
|
|
343
|
+
t.throws(() => run(['ref', 'toString']))
|
|
344
|
+
t.throws(() => run(['ref', 'nested', 'again', 'T', 'toString']))
|
|
345
|
+
t.end()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
t.test('cannot reach process or global (if accidentally exposed)', (t) => {
|
|
349
|
+
// attempt common global names; parser should not resolve them
|
|
350
|
+
t.throws(() => run(['ref', 'process']))
|
|
351
|
+
t.throws(() => run(['ref', 'global']))
|
|
352
|
+
t.end()
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
t.test('cannot use constructor.constructor to reach Function', (t) => {
|
|
356
|
+
// attempt: nested.again.T.constructor.constructor
|
|
357
|
+
t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor', 'constructor']))
|
|
358
|
+
t.end()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
t.test('throws when trying to get something that does not exist', (t) => {
|
|
362
|
+
// Ensure array that contains a ref-like array stays as data if not a SendScript ref wrapper:
|
|
363
|
+
t.throws(() => run(['ref', 'doesNotExist']))
|
|
364
|
+
t.end()
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
t.end()
|
|
368
|
+
})
|
package/leaf-serializer.test.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test } from 'tap'
|
|
2
2
|
import SuperJSON from 'superjson'
|
|
3
|
-
import
|
|
3
|
+
import Parse from './parse.mjs'
|
|
4
|
+
import Stringify from './stringify.mjs'
|
|
4
5
|
|
|
5
6
|
const leafSerializer = (value) => {
|
|
6
7
|
if (value === undefined) return JSON.stringify({ __sendscript_undefined__: true })
|
|
@@ -17,10 +18,14 @@ const module = {
|
|
|
17
18
|
identity: (x) => x
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const run = (program, serializer, deserializer) =>
|
|
23
|
-
parse(
|
|
21
|
+
const schema = Object.keys(module)
|
|
22
|
+
|
|
23
|
+
const run = (program, serializer, deserializer) => {
|
|
24
|
+
const parse = Parse(schema, module, deserializer)
|
|
25
|
+
const stringify = Stringify(serializer)
|
|
26
|
+
|
|
27
|
+
return parse(stringify(program))
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
test('custom leaf serializer/deserializer using superjson', async (t) => {
|
|
26
31
|
const value = {
|
|
@@ -64,13 +69,3 @@ test('default leaf deserializer when not provided', async (t) => {
|
|
|
64
69
|
t.strictSame(result, value)
|
|
65
70
|
t.end()
|
|
66
71
|
})
|
|
67
|
-
|
|
68
|
-
test('default leaf deserializer handles undefined parameter', (t) => {
|
|
69
|
-
const parse = Sendscript({}).parse
|
|
70
|
-
// Create a simple JSON with a leaf then parse using default deserializer
|
|
71
|
-
// The reviver will never pass undefined to deserializer, but we test it defensively
|
|
72
|
-
const json = '["leaf","{\\"test\\":1}"]'
|
|
73
|
-
const result = parse(json)
|
|
74
|
-
t.strictSame(result, { test: 1 })
|
|
75
|
-
t.end()
|
|
76
|
-
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sendscript",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Blur the line between server and client code.",
|
|
5
5
|
"module": true,
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"tape-check": "^1.0.0-rc.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"debug": "^4.4.3"
|
|
32
|
+
"debug": "^4.4.3",
|
|
33
|
+
"typedoc": "^0.28.18",
|
|
34
|
+
"typedoc-plugin-markdown": "^4.11.0"
|
|
33
35
|
}
|
|
34
36
|
}
|
package/parse.mjs
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import Debug from './debug.mjs'
|
|
2
2
|
import { SendScriptReferenceError } from './error.mjs'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
function flattenSchema (schema) {
|
|
5
|
+
const obj = {}
|
|
6
|
+
|
|
7
|
+
for (const item of schema) {
|
|
8
|
+
if (typeof item === 'string') {
|
|
9
|
+
// leaf function
|
|
10
|
+
obj[item] = true
|
|
11
|
+
} else if (Array.isArray(item)) {
|
|
12
|
+
const [name, children] = item
|
|
13
|
+
if (!Array.isArray(children)) {
|
|
14
|
+
throw new Error(`Expected children array for namespace "${name}"`)
|
|
15
|
+
}
|
|
16
|
+
obj[name] = flattenSchema(children)
|
|
17
|
+
} else {
|
|
18
|
+
throw new Error('Schema items must be strings or [name, children] arrays')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return obj
|
|
23
|
+
}
|
|
5
24
|
|
|
6
|
-
const
|
|
7
|
-
value != null && typeof value.then === 'function'
|
|
8
|
-
)
|
|
25
|
+
const debug = Debug.extend('parse')
|
|
9
26
|
|
|
10
|
-
const isAwaitPromise = Symbol('sendscript-await')
|
|
11
27
|
const undefinedSentinel = Symbol('sendscript-undefined')
|
|
12
28
|
|
|
13
29
|
const isPlainObject = (value) => {
|
|
@@ -16,66 +32,57 @@ const isPlainObject = (value) => {
|
|
|
16
32
|
return proto === Object.prototype || proto === null
|
|
17
33
|
}
|
|
18
34
|
|
|
19
|
-
const markAwait = (promise) => {
|
|
20
|
-
promise[isAwaitPromise] = true
|
|
21
|
-
return promise
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const isExplicitAwait = (value) => (
|
|
25
|
-
isThenable(value) && value[isAwaitPromise] === true
|
|
26
|
-
)
|
|
27
|
-
|
|
28
35
|
// Recursively resolve awaited values in a parsed tree
|
|
29
|
-
const
|
|
36
|
+
const evaluate = (value, awaits = []) => {
|
|
30
37
|
if (value === undefinedSentinel) return undefined
|
|
31
38
|
|
|
32
|
-
if (isThenable(value)) {
|
|
33
|
-
return isExplicitAwait(value)
|
|
34
|
-
? markAwait(value.then(resolveAwaitedValues))
|
|
35
|
-
: value
|
|
36
|
-
}
|
|
37
|
-
|
|
38
39
|
if (Array.isArray(value)) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const [operator, ...rest] = value
|
|
41
|
+
|
|
42
|
+
if (operator === 'await') {
|
|
43
|
+
const [index] = rest
|
|
44
|
+
// No need to check index. It is closely tied to the program.
|
|
45
|
+
// if (typeof index !== 'number' || index < 0 || index >= awaitsResolved.length) {
|
|
46
|
+
// throw new Error(`Invalid await index: ${index}`);
|
|
47
|
+
// }
|
|
48
|
+
return awaits[index]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (operator === 'call') {
|
|
52
|
+
// Step 1: evaluate the function itself
|
|
53
|
+
let [fn, args] = rest
|
|
54
|
+
fn = evaluate(fn, awaits)
|
|
55
|
+
|
|
56
|
+
// Step 2: evaluate each argument AFTER fn is ready
|
|
57
|
+
for (let i = 0; i < args.length; i++) {
|
|
58
|
+
args[i] = evaluate(args[i], awaits)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 3: call the function
|
|
62
|
+
return fn(...args)
|
|
63
|
+
}
|
|
45
64
|
|
|
46
|
-
if (
|
|
65
|
+
if (operator === 'quote') {
|
|
66
|
+
const [quoted] = rest
|
|
67
|
+
return quoted // return as-is without evaluating
|
|
68
|
+
}
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
? Promise.resolve(item).then((resolved) => {
|
|
51
|
-
result[index] = resolved
|
|
52
|
-
return resolved
|
|
53
|
-
})
|
|
54
|
-
: item
|
|
55
|
-
)
|
|
70
|
+
// fallback: evaluate each element
|
|
71
|
+
// re-uses the array again.
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
for (let index = 0; index < value.length; index++) {
|
|
74
|
+
const item = value[index]
|
|
75
|
+
value[index] = evaluate(item, awaits)
|
|
76
|
+
}
|
|
77
|
+
return value
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
if (isPlainObject(value)) {
|
|
61
|
-
|
|
62
|
-
const promises = []
|
|
63
|
-
|
|
81
|
+
// We muatate the object itself. No need to make a new one.
|
|
64
82
|
for (const key of Object.keys(value)) {
|
|
65
|
-
|
|
66
|
-
if (isExplicitAwait(resolved)) {
|
|
67
|
-
promises.push(
|
|
68
|
-
Promise.resolve(resolved).then((resolvedValue) => {
|
|
69
|
-
result[key] = resolvedValue
|
|
70
|
-
})
|
|
71
|
-
)
|
|
72
|
-
} else {
|
|
73
|
-
result[key] = resolved
|
|
74
|
-
}
|
|
83
|
+
value[key] = evaluate(value[key], awaits)
|
|
75
84
|
}
|
|
76
|
-
|
|
77
|
-
if (!promises.length) return result
|
|
78
|
-
return markAwait(Promise.all(promises).then(() => result))
|
|
85
|
+
return value
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
return value
|
|
@@ -89,10 +96,15 @@ const spy = (fn) => (...args) => {
|
|
|
89
96
|
|
|
90
97
|
const defaultLeafDeserializer = (text) => JSON.parse(text)
|
|
91
98
|
|
|
92
|
-
export default (env) =>
|
|
93
|
-
|
|
99
|
+
export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
|
|
100
|
+
const schema = flattenSchema(schemaArg)
|
|
101
|
+
|
|
102
|
+
return function parse (program) {
|
|
94
103
|
debug('program', program)
|
|
104
|
+
const awaits = []
|
|
95
105
|
|
|
106
|
+
// Creates the list of awaits that will resolve in order
|
|
107
|
+
// and also deserializes the leaves.
|
|
96
108
|
const reviver = spy((key, value) => {
|
|
97
109
|
if (value === null) return value
|
|
98
110
|
|
|
@@ -109,41 +121,19 @@ export default (env) =>
|
|
|
109
121
|
|
|
110
122
|
if (operator === 'await') {
|
|
111
123
|
const [program] = rest
|
|
112
|
-
return markAwait(Promise.resolve(program))
|
|
113
|
-
}
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
const [, quoted] = operator
|
|
117
|
-
return [quoted, ...rest]
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (operator === 'call') {
|
|
121
|
-
const [fn, args] = rest
|
|
122
|
-
const resolvedFn = isExplicitAwait(fn) ? Promise.resolve(fn) : fn
|
|
123
|
-
const resolvedArgs = resolveAwaitedValues(args)
|
|
124
|
-
|
|
125
|
-
if (isExplicitAwait(resolvedFn) || isExplicitAwait(resolvedArgs)) {
|
|
126
|
-
const promiseFn = isExplicitAwait(resolvedFn)
|
|
127
|
-
? Promise.resolve(resolvedFn)
|
|
128
|
-
: resolvedFn
|
|
129
|
-
const promiseArgs = isExplicitAwait(resolvedArgs)
|
|
130
|
-
? Promise.resolve(resolvedArgs)
|
|
131
|
-
: resolvedArgs
|
|
132
|
-
|
|
133
|
-
return Promise.all([promiseFn, promiseArgs])
|
|
134
|
-
.then(([resolvedFnValue, resolvedArgsValue]) => resolvedFnValue(...resolvedArgsValue))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return fn(...resolvedArgs)
|
|
125
|
+
return ['await', awaits.push(program) - 1]
|
|
138
126
|
}
|
|
139
127
|
|
|
140
128
|
if (operator === 'ref') {
|
|
141
|
-
const path = rest
|
|
129
|
+
const path = rest
|
|
142
130
|
let current = env
|
|
131
|
+
let schemaCurrent = schema
|
|
143
132
|
|
|
144
133
|
for (const segment of path) {
|
|
145
|
-
if (
|
|
134
|
+
if (schemaCurrent && Object.hasOwn(schemaCurrent, segment)) {
|
|
146
135
|
current = current[segment]
|
|
136
|
+
schemaCurrent = schemaCurrent[segment]
|
|
147
137
|
} else {
|
|
148
138
|
throw new SendScriptReferenceError({ key, value })
|
|
149
139
|
}
|
|
@@ -156,7 +146,23 @@ export default (env) =>
|
|
|
156
146
|
})
|
|
157
147
|
|
|
158
148
|
const parsed = JSON.parse(program, reviver)
|
|
159
|
-
const result = resolveAwaitedValues(parsed)
|
|
160
149
|
|
|
161
|
-
|
|
150
|
+
debug('parsed', parsed)
|
|
151
|
+
|
|
152
|
+
if (awaits.length) {
|
|
153
|
+
debug('awaits', awaits)
|
|
154
|
+
|
|
155
|
+
return (async function () {
|
|
156
|
+
for (let index = 0; index < awaits.length; index++) {
|
|
157
|
+
awaits[index] = await evaluate(awaits[index], awaits)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
debug('awaits(awaited)', awaits)
|
|
161
|
+
|
|
162
|
+
return evaluate(parsed, awaits)
|
|
163
|
+
})()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return evaluate(parsed)
|
|
162
167
|
}
|
|
168
|
+
}
|
|
@@ -37,22 +37,21 @@ function instrument (path) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export default function module (schema, parentPath = []) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
acc[key] = module(value, [...parentPath, key])
|
|
40
|
+
return schema.reduce((acc, item) => {
|
|
41
|
+
if (typeof item === 'string') {
|
|
42
|
+
// leaf function
|
|
43
|
+
acc[item] = instrument([...parentPath, item])
|
|
44
|
+
} else if (Array.isArray(item)) {
|
|
45
|
+
const [name, children] = item
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(children)) {
|
|
48
|
+
// recurse: children can be strings or [name, children] arrays
|
|
49
|
+
acc[name] = module(children, [...parentPath, name])
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error(`Expected children array for namespace "${name}"`)
|
|
52
|
+
}
|
|
54
53
|
} else {
|
|
55
|
-
|
|
54
|
+
throw new Error('Schema items must be strings or [name, children] arrays')
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
return acc
|
package/repl.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import references from './references.mjs'
|
|
2
2
|
import repl from 'node:repl'
|
|
3
3
|
|
|
4
4
|
export default async function sendscriptRepl (send, module) {
|
|
5
|
-
Object.assign(global,
|
|
5
|
+
Object.assign(global, references(Object.keys(module)))
|
|
6
6
|
|
|
7
7
|
async function cb (cmd, context, filename, callback) {
|
|
8
8
|
callback(null, await send(eval(cmd))) // eslint-disable-line no-eval
|
package/stringify.mjs
CHANGED
|
@@ -69,6 +69,10 @@ function transformValue (value, leafSerializer) {
|
|
|
69
69
|
return ['leaf', leafSerializer(value)]
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export default function stringify (
|
|
73
|
-
|
|
72
|
+
export default function stringify (leafSerializer = JSON.stringify) {
|
|
73
|
+
function stringify (program) {
|
|
74
|
+
return JSON.stringify(transformValue(program, leafSerializer))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return stringify
|
|
74
78
|
}
|
package/index.mjs
DELETED