sendscript 1.1.0 → 2.0.1
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 +2 -2
- package/.github/workflows/release.yml +45 -0
- package/CHANGELOG.md +14 -0
- package/README.md +57 -69
- package/README.mz +45 -46
- 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 +97 -25
- package/leaf-serializer.test.mjs +10 -15
- package/package.json +4 -2
- package/parse.mjs +31 -3
- package/{module.mjs → references.mjs} +14 -15
- package/references.test.mjs +8 -0
- package/stringify.mjs +6 -2
- package/index.mjs +0 -11
|
@@ -3,7 +3,7 @@ name: Publish Package
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
|
-
- '
|
|
6
|
+
- '*'
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
9
|
id-token: write # Required for OIDC
|
|
@@ -22,4 +22,4 @@ jobs:
|
|
|
22
22
|
- run: npm ci
|
|
23
23
|
- run: npm run build --if-present
|
|
24
24
|
- run: npm test
|
|
25
|
-
- run: npm publish
|
|
25
|
+
- run: npm publish
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Release on tag
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
node: ['lts/*', 'latest']
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: ${{ matrix.node }}
|
|
24
|
+
cache: npm
|
|
25
|
+
|
|
26
|
+
- run: npm ci
|
|
27
|
+
- run: npm test
|
|
28
|
+
|
|
29
|
+
release:
|
|
30
|
+
needs: test
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- uses: actions/setup-node@v4
|
|
37
|
+
with:
|
|
38
|
+
node-version: 'latest'
|
|
39
|
+
cache: npm
|
|
40
|
+
|
|
41
|
+
- run: npm ci
|
|
42
|
+
- name: Create GitHub Release
|
|
43
|
+
uses: softprops/action-gh-release@v2
|
|
44
|
+
with:
|
|
45
|
+
generate_release_notes: true
|
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,22 @@ 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.0.1](https://github.com/bas080/sendscript/compare/v2.0.0...v2.0.1)
|
|
8
|
+
|
|
9
|
+
- Publish to npm on tag push [`357ac0e`](https://github.com/bas080/sendscript/commit/357ac0ef3384f294e4f9bb179d5a675afda5fd19)
|
|
10
|
+
|
|
11
|
+
### [v2.0.0](https://github.com/bas080/sendscript/compare/v1.1.0...v2.0.0)
|
|
12
|
+
|
|
13
|
+
> 8 April 2026
|
|
14
|
+
|
|
15
|
+
- Rewrite to have parse take schema to prevent accessing refs [`df16f85`](https://github.com/bas080/sendscript/commit/df16f85c79299aa8bcd3e44b6cc127bd7198eace)
|
|
16
|
+
- Run tests before release and publish on release [`10442d6`](https://github.com/bas080/sendscript/commit/10442d6766a098926ee98daee08dba23eea0fb14)
|
|
17
|
+
- Create release on tag with github workflows [`4ec222b`](https://github.com/bas080/sendscript/commit/4ec222bf51fc1d3cce9fc54fdd1e9639b5d259cd)
|
|
18
|
+
|
|
7
19
|
#### [v1.1.0](https://github.com/bas080/sendscript/compare/v1.0.6...v1.1.0)
|
|
8
20
|
|
|
21
|
+
> 7 April 2026
|
|
22
|
+
|
|
9
23
|
- Support custom serialization of leaf nodes [`d56f1b7`](https://github.com/bas080/sendscript/commit/d56f1b750e2d7faf72d69f9ba8e9bb62ef5ebce7)
|
|
10
24
|
- Support nested modules [`88e69e5`](https://github.com/bas080/sendscript/commit/88e69e5c05ddb85aac1797a020e92519c986e89f)
|
|
11
25
|
- 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
|
|
|
@@ -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.0.1 test
|
|
445
433
|
> tap -R silent
|
|
446
434
|
|
|
447
435
|
|
|
448
|
-
> sendscript@
|
|
436
|
+
> sendscript@2.0.1 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% ( 348/348 )
|
|
442
|
+
Branches : 100% ( 145/145 )
|
|
443
|
+
Functions : 100% ( 24/24 )
|
|
444
|
+
Lines : 100% ( 348/348 )
|
|
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
|
|
|
@@ -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.
|
|
@@ -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/357ac0ef3384f294e4f9bb179d5a675afda5fd19/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/357ac0ef3384f294e4f9bb179d5a675afda5fd19/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,7 @@
|
|
|
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
5
|
|
|
6
6
|
const myModule = {
|
|
7
7
|
nested: {
|
|
@@ -30,27 +30,19 @@ const myModule = {
|
|
|
30
30
|
obj.b = 'c'
|
|
31
31
|
return obj
|
|
32
32
|
},
|
|
33
|
-
Function,
|
|
34
33
|
Promise
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
const schema = Object.keys(myModule)
|
|
38
|
-
|
|
36
|
+
const schema = Object.keys(myModule)
|
|
37
|
+
schema.push(['nested', [
|
|
38
|
+
['again', ['T']]
|
|
39
|
+
]])
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const api = references(schema)
|
|
42
|
+
const stringify = Stringify()
|
|
43
|
+
const parse = Parse(schema, myModule)
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const sendscript = {
|
|
46
|
-
stringify,
|
|
47
|
-
parse: ssparse(myModule),
|
|
48
|
-
module: module(schema)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const { parse } = sendscript
|
|
52
|
-
|
|
53
|
-
const run = (program) => sendscript.parse(sendscript.stringify(program))
|
|
45
|
+
const run = (program) => parse(stringify(program))
|
|
54
46
|
|
|
55
47
|
const RealPromise = Promise
|
|
56
48
|
|
|
@@ -61,7 +53,6 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
61
53
|
resolve,
|
|
62
54
|
delayedIdentity,
|
|
63
55
|
noop,
|
|
64
|
-
Function,
|
|
65
56
|
Promise,
|
|
66
57
|
instanceOf,
|
|
67
58
|
asyncFn,
|
|
@@ -74,7 +65,16 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
74
65
|
always,
|
|
75
66
|
multiply3,
|
|
76
67
|
nested
|
|
77
|
-
} =
|
|
68
|
+
} = api
|
|
69
|
+
|
|
70
|
+
t.test('mix await without await', async t => {
|
|
71
|
+
const [one, two] = await run([await resolve(1), resolve(2)])
|
|
72
|
+
|
|
73
|
+
t.equal(one, 1)
|
|
74
|
+
t.ok(two instanceof RealPromise)
|
|
75
|
+
|
|
76
|
+
t.end()
|
|
77
|
+
})
|
|
78
78
|
|
|
79
79
|
t.test('calling nested function works', t => {
|
|
80
80
|
t.equal(run(nested.again.T()), true)
|
|
@@ -143,9 +143,6 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
143
143
|
t.strictSame(await run(asyncFn()), 'my-async-function')
|
|
144
144
|
t.strictSame(await run(await resolve('my-promise')), 'my-promise')
|
|
145
145
|
t.strictSame(run(instanceOf(resolve(asyncFn), Promise)), true)
|
|
146
|
-
t.strictSame(
|
|
147
|
-
await run(instanceOf(await resolve(asyncFn), Function)), true
|
|
148
|
-
)
|
|
149
146
|
t.strictSame(
|
|
150
147
|
await run({ a: await resolve('b') }),
|
|
151
148
|
{ a: 'b' }
|
|
@@ -218,7 +215,7 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
218
215
|
})
|
|
219
216
|
|
|
220
217
|
t.test('null-prototype object traversal', (t) => {
|
|
221
|
-
const { nullProto } =
|
|
218
|
+
const { nullProto } = api
|
|
222
219
|
t.strictSame(run({ a: nullProto() }), { a: { b: 'c' } })
|
|
223
220
|
t.end()
|
|
224
221
|
})
|
|
@@ -238,3 +235,78 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
238
235
|
t.end()
|
|
239
236
|
})
|
|
240
237
|
})
|
|
238
|
+
|
|
239
|
+
test('stringify: invalid children throws', t => {
|
|
240
|
+
const invalidSchema = [
|
|
241
|
+
['math', 'add']
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
t.throws(() => Parse(invalidSchema, {}))
|
|
245
|
+
t.throws(() => Parse([Symbol('no-allowed')], {}))
|
|
246
|
+
t.end()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('forbidden/reflection access should be blocked', async (t) => {
|
|
250
|
+
function run (program) {
|
|
251
|
+
return parse(JSON.stringify(program))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
t.test('cannot access constructor via ref', (t) => {
|
|
255
|
+
// try to reach the constructor of nested.again.T
|
|
256
|
+
t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor']))
|
|
257
|
+
t.end()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
t.test('cannot access prototype property', (t) => {
|
|
261
|
+
// direct prototype traversal attempt
|
|
262
|
+
t.throws(() => run(['ref', 'nested', '__proto__']))
|
|
263
|
+
t.throws(() => run(['ref', 'nested', 'again', '__proto__']))
|
|
264
|
+
t.end()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
t.test('cannot reach Function constructor', (t) => {
|
|
268
|
+
// many modules may accidentally expose Function; this should not be available via ref
|
|
269
|
+
t.throws(() => run(['ref', 'Function']))
|
|
270
|
+
t.end()
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
t.test('cannot call Function to execute code', (t) => {
|
|
274
|
+
// If Function were reachable, this would execute arbitrary code
|
|
275
|
+
const program = ['call', ['ref', 'Function'], [['leaf', '"return 1 + 2"']]]
|
|
276
|
+
t.throws(() => run(program))
|
|
277
|
+
t.end()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
t.test('cannot access global Promise constructor via ref', (t) => {
|
|
281
|
+
t.throws(() => run(['ref', 'Promise', 'prototype', 'then']))
|
|
282
|
+
t.end()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
t.test('cannot access Object.prototype methods', (t) => {
|
|
286
|
+
// toString is a common vector for prototype access
|
|
287
|
+
t.throws(() => run(['ref', 'toString']))
|
|
288
|
+
t.throws(() => run(['ref', 'nested', 'again', 'T', 'toString']))
|
|
289
|
+
t.end()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
t.test('cannot reach process or global (if accidentally exposed)', (t) => {
|
|
293
|
+
// attempt common global names; parser should not resolve them
|
|
294
|
+
t.throws(() => run(['ref', 'process']))
|
|
295
|
+
t.throws(() => run(['ref', 'global']))
|
|
296
|
+
t.end()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
t.test('cannot use constructor.constructor to reach Function', (t) => {
|
|
300
|
+
// attempt: nested.again.T.constructor.constructor
|
|
301
|
+
t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor', 'constructor']))
|
|
302
|
+
t.end()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
t.test('throws when trying to get something that does not exist', (t) => {
|
|
306
|
+
// Ensure array that contains a ref-like array stays as data if not a SendScript ref wrapper:
|
|
307
|
+
t.throws(() => run(['ref', 'doesNotExist']))
|
|
308
|
+
t.end()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
t.end()
|
|
312
|
+
})
|
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.0.1",
|
|
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,6 +1,29 @@
|
|
|
1
1
|
import Debug from './debug.mjs'
|
|
2
2
|
import { SendScriptReferenceError } from './error.mjs'
|
|
3
3
|
|
|
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
|
+
// TODO: Test this
|
|
14
|
+
if (!Array.isArray(children)) {
|
|
15
|
+
throw new Error(`Expected children array for namespace "${name}"`)
|
|
16
|
+
}
|
|
17
|
+
obj[name] = flattenSchema(children)
|
|
18
|
+
// TODO: Test this also
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error('Schema items must be strings or [name, children] arrays')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return obj
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
const debug = Debug.extend('parse')
|
|
5
28
|
|
|
6
29
|
const isThenable = (value) => (
|
|
@@ -89,8 +112,10 @@ const spy = (fn) => (...args) => {
|
|
|
89
112
|
|
|
90
113
|
const defaultLeafDeserializer = (text) => JSON.parse(text)
|
|
91
114
|
|
|
92
|
-
export default (env) =>
|
|
93
|
-
|
|
115
|
+
export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
|
|
116
|
+
const schema = flattenSchema(schemaArg)
|
|
117
|
+
|
|
118
|
+
return function parse (program) {
|
|
94
119
|
debug('program', program)
|
|
95
120
|
|
|
96
121
|
const reviver = spy((key, value) => {
|
|
@@ -140,10 +165,12 @@ export default (env) =>
|
|
|
140
165
|
if (operator === 'ref') {
|
|
141
166
|
const path = rest // e.g., ["math","add"]
|
|
142
167
|
let current = env
|
|
168
|
+
let schemaCurrent = schema
|
|
143
169
|
|
|
144
170
|
for (const segment of path) {
|
|
145
|
-
if (
|
|
171
|
+
if (schemaCurrent && Object.hasOwn(schemaCurrent, segment)) {
|
|
146
172
|
current = current[segment]
|
|
173
|
+
schemaCurrent = schemaCurrent[segment]
|
|
147
174
|
} else {
|
|
148
175
|
throw new SendScriptReferenceError({ key, value })
|
|
149
176
|
}
|
|
@@ -160,3 +187,4 @@ export default (env) =>
|
|
|
160
187
|
|
|
161
188
|
return result
|
|
162
189
|
}
|
|
190
|
+
}
|
|
@@ -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/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