sendscript 1.0.6 → 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 +22 -0
- package/README.md +161 -45
- package/README.mz +142 -21
- 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 +126 -11
- package/leaf-serializer-property.test.mjs +149 -0
- package/leaf-serializer.test.mjs +71 -0
- package/package.json +8 -3
- package/parse.mjs +147 -49
- package/references.mjs +59 -0
- package/references.test.mjs +8 -0
- package/stringify.mjs +47 -35
- package/index.mjs +0 -11
- package/module.mjs +0 -52
|
@@ -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,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.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
|
+
|
|
19
|
+
#### [v1.1.0](https://github.com/bas080/sendscript/compare/v1.0.6...v1.1.0)
|
|
20
|
+
|
|
21
|
+
> 7 April 2026
|
|
22
|
+
|
|
23
|
+
- Support custom serialization of leaf nodes [`d56f1b7`](https://github.com/bas080/sendscript/commit/d56f1b750e2d7faf72d69f9ba8e9bb62ef5ebce7)
|
|
24
|
+
- Support nested modules [`88e69e5`](https://github.com/bas080/sendscript/commit/88e69e5c05ddb85aac1797a020e92519c986e89f)
|
|
25
|
+
- Document new nesting and leaf serialization feature [`b3ee9f6`](https://github.com/bas080/sendscript/commit/b3ee9f693dc6137b01c9f0072aa2dd1e940f07c9)
|
|
26
|
+
|
|
7
27
|
#### [v1.0.6](https://github.com/bas080/sendscript/compare/v1.0.5...v1.0.6)
|
|
8
28
|
|
|
29
|
+
> 6 April 2026
|
|
30
|
+
|
|
9
31
|
- Update tap to 21.6.3 [`c1769d9`](https://github.com/bas080/sendscript/commit/c1769d9a24148e63d27b0e5a7456213bec64763e)
|
|
10
32
|
- Add introduction to readme [`a21a494`](https://github.com/bas080/sendscript/commit/a21a494616d5ad8c99f9a2904ef529ea85104a99)
|
|
11
33
|
|
package/README.md
CHANGED
|
@@ -17,6 +17,12 @@ Write JS code that you can run on servers, browsers or other clients.
|
|
|
17
17
|
- [Repl](#repl)
|
|
18
18
|
- [Async/Await](#asyncawait)
|
|
19
19
|
- [TypeScript](#typescript)
|
|
20
|
+
- [Schema and Nested Modules](#schema-and-nested-modules)
|
|
21
|
+
* [Defining a Nested Module](#defining-a-nested-module)
|
|
22
|
+
- [Validation (using Zod)](#validation-using-zod)
|
|
23
|
+
* [Validating structured input](#validating-structured-input)
|
|
24
|
+
- [Leaf Serializer](#leaf-serializer)
|
|
25
|
+
* [Example with superjson](#example-with-superjson)
|
|
20
26
|
- [Tests](#tests)
|
|
21
27
|
- [Formatting](#formatting)
|
|
22
28
|
- [Changelog](#changelog)
|
|
@@ -47,15 +53,16 @@ using more advanced (de)serialization libraries.
|
|
|
47
53
|
SendScript produces an intermediate JSON representation of the program. Let's see what that looks like.
|
|
48
54
|
|
|
49
55
|
```js
|
|
50
|
-
import
|
|
51
|
-
import
|
|
56
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
57
|
+
import references from 'sendscript/references.mjs'
|
|
52
58
|
|
|
53
|
-
const { add } =
|
|
59
|
+
const { add } = references(['add'])
|
|
60
|
+
const stringify = Stringify()
|
|
54
61
|
|
|
55
62
|
console.log(stringify(add(1,2)))
|
|
56
63
|
```
|
|
57
64
|
```json
|
|
58
|
-
["call",["ref","add"],[1,2]]
|
|
65
|
+
["call",["ref","add"],[["leaf","1"],["leaf","2"]]]
|
|
59
66
|
```
|
|
60
67
|
|
|
61
68
|
We can then parse that JSON and it will evaluate down to a value.
|
|
@@ -69,7 +76,7 @@ const module = {
|
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
const parse = Parse(module)
|
|
79
|
+
const parse = Parse(['add'], module)
|
|
73
80
|
|
|
74
81
|
const program = '["call",["ref","add"],[1,2]]'
|
|
75
82
|
|
|
@@ -125,7 +132,8 @@ import { Server } from 'socket.io'
|
|
|
125
132
|
import Parse from 'sendscript/parse.mjs'
|
|
126
133
|
import * as math from './math.mjs'
|
|
127
134
|
|
|
128
|
-
const
|
|
135
|
+
const schema = Object.keys(math)
|
|
136
|
+
const parse = Parse(schema, math)
|
|
129
137
|
const server = new Server()
|
|
130
138
|
const port = process.env.PORT || 3000
|
|
131
139
|
|
|
@@ -152,11 +160,13 @@ Now for a client that sends a program to the server.
|
|
|
152
160
|
// ./example/client.socket.io.mjs
|
|
153
161
|
|
|
154
162
|
import socketClient from 'socket.io-client'
|
|
155
|
-
import
|
|
156
|
-
import
|
|
157
|
-
import * as math from './math.mjs'
|
|
163
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
164
|
+
import references from 'sendscript/references.mjs'
|
|
158
165
|
import assert from 'node:assert'
|
|
159
166
|
|
|
167
|
+
const { add, square } = references(['add', 'square'])
|
|
168
|
+
const stringify = Stringify()
|
|
169
|
+
|
|
160
170
|
const port = process.env.PORT || 3000
|
|
161
171
|
const client = socketClient(`http://localhost:${port}`)
|
|
162
172
|
|
|
@@ -170,8 +180,6 @@ const send = program => {
|
|
|
170
180
|
})
|
|
171
181
|
}
|
|
172
182
|
|
|
173
|
-
const { add, square } = module(math)
|
|
174
|
-
|
|
175
183
|
// The program to be sent over the wire
|
|
176
184
|
const program = square(add(1, add(add(2, 3), 4)))
|
|
177
185
|
|
|
@@ -247,52 +255,31 @@ export const add = (a: number, b: number) => a + b
|
|
|
247
255
|
export const square = (a: number) => a * a
|
|
248
256
|
```
|
|
249
257
|
|
|
250
|
-
We
|
|
251
|
-
|
|
252
|
-
```bash
|
|
253
|
-
cat ./example/typescript/math.client.ts
|
|
254
|
-
```
|
|
255
|
-
```ts
|
|
256
|
-
import module from 'sendscript/module.mjs'
|
|
257
|
-
import type * as mathTypes from './math.ts'
|
|
258
|
-
|
|
259
|
-
const math = module([
|
|
260
|
-
'add',
|
|
261
|
-
'square'
|
|
262
|
-
]) as typeof mathTypes
|
|
263
|
-
|
|
264
|
-
export default math
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
We now use the client version of this module.
|
|
258
|
+
We can then coerce the types of the instrumented stubs.
|
|
268
259
|
|
|
269
260
|
```bash
|
|
270
261
|
cat ./example/typescript/client.ts
|
|
271
262
|
```
|
|
272
263
|
```ts
|
|
273
|
-
import
|
|
264
|
+
import math from './math.client.ts'
|
|
265
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
274
266
|
|
|
275
|
-
|
|
267
|
+
const stringify = Stringify()
|
|
268
|
+
|
|
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> {
|
|
276
271
|
return (await fetch('/api', {
|
|
277
272
|
method: 'POST',
|
|
278
273
|
body: stringify(program)
|
|
279
274
|
})).json()
|
|
280
275
|
}
|
|
281
276
|
|
|
282
|
-
import math from './math.client.ts'
|
|
283
|
-
|
|
284
|
-
const { add, square } = math
|
|
285
|
-
|
|
286
277
|
send(square(add(1, 2)))
|
|
287
278
|
```
|
|
288
279
|
|
|
289
280
|
We'll also generate the docs for this module.
|
|
290
281
|
|
|
291
282
|
```bash
|
|
292
|
-
npm install --no-save \
|
|
293
|
-
typedoc \
|
|
294
|
-
typedoc-plugin-markdown
|
|
295
|
-
|
|
296
283
|
npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./example/typescript/math.ts
|
|
297
284
|
```
|
|
298
285
|
|
|
@@ -303,6 +290,135 @@ You can see the docs [here](./example/typescript/docs/globals.md)
|
|
|
303
290
|
> experience, it does not represent the actual type.
|
|
304
291
|
> Values are subject to serialization and deserialization.
|
|
305
292
|
|
|
293
|
+
|
|
294
|
+
## Schema and Nested Modules
|
|
295
|
+
|
|
296
|
+
Sendscript allows you to define your API as a **nested object of functions**, making it easy to organize your DSL into modules and submodules. Each function is instrumented so that when serialized, it produces a structured reference that can be safely sent and executed elsewhere.
|
|
297
|
+
|
|
298
|
+
### Defining a Nested Module
|
|
299
|
+
|
|
300
|
+
You can define a schema as **an array of function names**
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
|
|
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
|
+
]],
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Functions are referenced via their **path in the module tree**:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
const { math, vector } = references(schema)
|
|
327
|
+
|
|
328
|
+
math.add(
|
|
329
|
+
1,
|
|
330
|
+
vector.length(
|
|
331
|
+
vector.multiply([1,2], 3)
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Validation (using Zod)
|
|
337
|
+
|
|
338
|
+
SendScript focuses on program serialization and execution. For runtime input validation, you can use [Zod](https://zod.dev).
|
|
339
|
+
|
|
340
|
+
### Validating structured input
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
const userSchema = z.object({
|
|
344
|
+
id: z.string().uuid(),
|
|
345
|
+
name: z.string(),
|
|
346
|
+
roles: z.array(z.string())
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
export function createUser(user) {
|
|
350
|
+
userSchema.parse(user)
|
|
351
|
+
|
|
352
|
+
return { success: true }
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Benefits**:
|
|
357
|
+
|
|
358
|
+
- Ensures arguments match expected types and shapes.
|
|
359
|
+
- Throws structured errors that can be propagated to clients.
|
|
360
|
+
- Works with TypeScript for automatic type inference.
|
|
361
|
+
|
|
362
|
+
## Leaf Serializer
|
|
363
|
+
|
|
364
|
+
By default, SendScript uses JSON for serialization, which limits support to primitives and plain objects/arrays. To support richer JavaScript types like `Date`, `RegExp`, `BigInt`, `Map`, `Set`, and `undefined`, you can provide custom serialization functions.
|
|
365
|
+
|
|
366
|
+
The `stringify` function accepts an optional `leafSerializer` parameter, and `parse` accepts an optional `leafDeserializer` parameter. These functions control how non-SendScript values (leaves) are encoded and decoded.
|
|
367
|
+
|
|
368
|
+
### Example with superjson
|
|
369
|
+
|
|
370
|
+
Here's how to use [superjson](https://github.com/blitz-js/superjson) to support extended types:
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
import SuperJSON from 'superjson'
|
|
374
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
375
|
+
import references from 'sendscript/references.mjs'
|
|
376
|
+
import Parse from 'sendscript/parse.mjs'
|
|
377
|
+
|
|
378
|
+
const leafSerializer = (value) => {
|
|
379
|
+
if (value === undefined) return JSON.stringify({ __undefined__: true })
|
|
380
|
+
return JSON.stringify(SuperJSON.serialize(value))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const leafDeserializer = (text) => {
|
|
384
|
+
const parsed = JSON.parse(text)
|
|
385
|
+
if (parsed && parsed.__undefined__ === true) return undefined
|
|
386
|
+
return SuperJSON.deserialize(parsed)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const schema = ['processData']
|
|
390
|
+
|
|
391
|
+
const { processData } = references(schema)
|
|
392
|
+
const stringify = Stringify(leafSerializer)
|
|
393
|
+
|
|
394
|
+
// Program with Date, RegExp, and other types
|
|
395
|
+
const program = {
|
|
396
|
+
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
|
397
|
+
pattern: /foo/gi,
|
|
398
|
+
count: BigInt('9007199254740992'),
|
|
399
|
+
items: new Set([1, 2, 3]),
|
|
400
|
+
mapping: new Map([['a', 1], ['b', 2]])
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Serialize with custom leaf serializer
|
|
404
|
+
const json = stringify(processData(program))
|
|
405
|
+
|
|
406
|
+
// The environment
|
|
407
|
+
const env = {
|
|
408
|
+
processData: (data) => ({
|
|
409
|
+
success: true,
|
|
410
|
+
received: data
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Parse with custom leaf deserializer
|
|
415
|
+
const parse = Parse(schema, env, leadDeserializer)
|
|
416
|
+
|
|
417
|
+
const result = parse(json)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous and safe from colliding with SendScript operators.
|
|
421
|
+
|
|
306
422
|
## Tests
|
|
307
423
|
|
|
308
424
|
Tests with 100% code coverage.
|
|
@@ -313,19 +429,19 @@ npm t -- report text-summary
|
|
|
313
429
|
```
|
|
314
430
|
```
|
|
315
431
|
|
|
316
|
-
> sendscript@
|
|
432
|
+
> sendscript@2.0.1 test
|
|
317
433
|
> tap -R silent
|
|
318
434
|
|
|
319
435
|
|
|
320
|
-
> sendscript@
|
|
436
|
+
> sendscript@2.0.1 test
|
|
321
437
|
> tap report text-summary
|
|
322
438
|
|
|
323
439
|
|
|
324
440
|
=============================== Coverage summary ===============================
|
|
325
|
-
Statements : 100% (
|
|
326
|
-
Branches : 100% (
|
|
327
|
-
Functions : 100% (
|
|
328
|
-
Lines : 100% (
|
|
441
|
+
Statements : 100% ( 348/348 )
|
|
442
|
+
Branches : 100% ( 145/145 )
|
|
443
|
+
Functions : 100% ( 24/24 )
|
|
444
|
+
Lines : 100% ( 348/348 )
|
|
329
445
|
================================================================================
|
|
330
446
|
```
|
|
331
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
|
|
|
@@ -246,6 +238,135 @@ You can see the docs [here](./example/typescript/docs/globals.md)
|
|
|
246
238
|
> experience, it does not represent the actual type.
|
|
247
239
|
> Values are subject to serialization and deserialization.
|
|
248
240
|
|
|
241
|
+
|
|
242
|
+
## Schema and Nested Modules
|
|
243
|
+
|
|
244
|
+
Sendscript allows you to define your API as a **nested object of functions**, making it easy to organize your DSL into modules and submodules. Each function is instrumented so that when serialized, it produces a structured reference that can be safely sent and executed elsewhere.
|
|
245
|
+
|
|
246
|
+
### Defining a Nested Module
|
|
247
|
+
|
|
248
|
+
You can define a schema as **an array of function names**
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
|
|
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
|
+
]],
|
|
268
|
+
})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Functions are referenced via their **path in the module tree**:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
const { math, vector } = references(schema)
|
|
275
|
+
|
|
276
|
+
math.add(
|
|
277
|
+
1,
|
|
278
|
+
vector.length(
|
|
279
|
+
vector.multiply([1,2], 3)
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Validation (using Zod)
|
|
285
|
+
|
|
286
|
+
SendScript focuses on program serialization and execution. For runtime input validation, you can use [Zod](https://zod.dev).
|
|
287
|
+
|
|
288
|
+
### Validating structured input
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
const userSchema = z.object({
|
|
292
|
+
id: z.string().uuid(),
|
|
293
|
+
name: z.string(),
|
|
294
|
+
roles: z.array(z.string())
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
export function createUser(user) {
|
|
298
|
+
userSchema.parse(user)
|
|
299
|
+
|
|
300
|
+
return { success: true }
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Benefits**:
|
|
305
|
+
|
|
306
|
+
- Ensures arguments match expected types and shapes.
|
|
307
|
+
- Throws structured errors that can be propagated to clients.
|
|
308
|
+
- Works with TypeScript for automatic type inference.
|
|
309
|
+
|
|
310
|
+
## Leaf Serializer
|
|
311
|
+
|
|
312
|
+
By default, SendScript uses JSON for serialization, which limits support to primitives and plain objects/arrays. To support richer JavaScript types like `Date`, `RegExp`, `BigInt`, `Map`, `Set`, and `undefined`, you can provide custom serialization functions.
|
|
313
|
+
|
|
314
|
+
The `stringify` function accepts an optional `leafSerializer` parameter, and `parse` accepts an optional `leafDeserializer` parameter. These functions control how non-SendScript values (leaves) are encoded and decoded.
|
|
315
|
+
|
|
316
|
+
### Example with superjson
|
|
317
|
+
|
|
318
|
+
Here's how to use [superjson](https://github.com/blitz-js/superjson) to support extended types:
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
import SuperJSON from 'superjson'
|
|
322
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
323
|
+
import references from 'sendscript/references.mjs'
|
|
324
|
+
import Parse from 'sendscript/parse.mjs'
|
|
325
|
+
|
|
326
|
+
const leafSerializer = (value) => {
|
|
327
|
+
if (value === undefined) return JSON.stringify({ __undefined__: true })
|
|
328
|
+
return JSON.stringify(SuperJSON.serialize(value))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const leafDeserializer = (text) => {
|
|
332
|
+
const parsed = JSON.parse(text)
|
|
333
|
+
if (parsed && parsed.__undefined__ === true) return undefined
|
|
334
|
+
return SuperJSON.deserialize(parsed)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const schema = ['processData']
|
|
338
|
+
|
|
339
|
+
const { processData } = references(schema)
|
|
340
|
+
const stringify = Stringify(leafSerializer)
|
|
341
|
+
|
|
342
|
+
// Program with Date, RegExp, and other types
|
|
343
|
+
const program = {
|
|
344
|
+
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
|
345
|
+
pattern: /foo/gi,
|
|
346
|
+
count: BigInt('9007199254740992'),
|
|
347
|
+
items: new Set([1, 2, 3]),
|
|
348
|
+
mapping: new Map([['a', 1], ['b', 2]])
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Serialize with custom leaf serializer
|
|
352
|
+
const json = stringify(processData(program))
|
|
353
|
+
|
|
354
|
+
// The environment
|
|
355
|
+
const env = {
|
|
356
|
+
processData: (data) => ({
|
|
357
|
+
success: true,
|
|
358
|
+
received: data
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Parse with custom leaf deserializer
|
|
363
|
+
const parse = Parse(schema, env, leadDeserializer)
|
|
364
|
+
|
|
365
|
+
const result = parse(json)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous and safe from colliding with SendScript operators.
|
|
369
|
+
|
|
249
370
|
## Tests
|
|
250
371
|
|
|
251
372
|
Tests with 100% code coverage.
|
|
@@ -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
|
|