sendscript 2.3.8 → 2.4.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/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.4.0](https://github.com/bas080/sendscript/compare/v2.3.9...v2.4.0)
8
+
9
+ - Update tap to 4.5.4 [`5249a85`](https://github.com/bas080/sendscript/commit/5249a85a08b10485f0d64058fd2c8bcaae84edda)
10
+ - Add fn and args support [`6110654`](https://github.com/bas080/sendscript/commit/6110654fa039d17cd9ba0e15f749756b615b5d09)
11
+ - Move transport agnostic section to socket example [`3a92257`](https://github.com/bas080/sendscript/commit/3a9225787df16a0fd83293bfa14d91b74a46d070)
12
+
13
+ #### [v2.3.9](https://github.com/bas080/sendscript/compare/v2.3.8...v2.3.9)
14
+
15
+ > 14 April 2026
16
+
17
+ - Remove schema description from reference and schema shape [`7126829`](https://github.com/bas080/sendscript/commit/7126829b5968588d7c9377ee643b88465f3fe16e)
18
+
7
19
  #### [v2.3.8](https://github.com/bas080/sendscript/compare/v2.3.7...v2.3.8)
8
20
 
21
+ > 14 April 2026
22
+
9
23
  - Move typedoc dep to dev deps [`a084b1d`](https://github.com/bas080/sendscript/commit/a084b1dad2f76ceb3b79fac390210f1ccfa368f8)
10
24
 
11
25
  #### [v2.3.7](https://github.com/bas080/sendscript/compare/v2.3.6...v2.3.7)
package/README.md CHANGED
@@ -25,6 +25,7 @@ Write JS code that you can run on servers, browsers or other clients.
25
25
  * [Server](#server)
26
26
  * [Client](#client)
27
27
  - [Repl](#repl)
28
+ - [Functions](#functions)
28
29
  - [Promises](#promises)
29
30
  * [.then / .catch](#then--catch)
30
31
  * [await](#await)
@@ -114,17 +115,14 @@ core or around it. Things like supporting more complex (de)serializers, errors
114
115
  and maybe mixing client functions with sendscript programs. Contact me if I have
115
116
  piqued your interest.
116
117
 
117
- ***
118
-
119
- SendScript leaves it up to you to choose HTTP, web-sockets or any other method
120
- of communication between servers and clients that best fits your needs.
121
-
122
118
  ## Reference
123
119
 
124
120
  <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
125
121
 
126
122
  ### defaultLeafParse
127
123
 
124
+ [parse.mjs:49-49](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/parse.mjs#L49-L49 "Source code on GitHub")
125
+
128
126
  Default deserializer for leaf nodes.
129
127
 
130
128
  #### Parameters
@@ -135,6 +133,8 @@ Returns **any**&#x20;
135
133
 
136
134
  ### Parse
137
135
 
136
+ [parse.mjs:68-215](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/parse.mjs#L68-L215 "Source code on GitHub")
137
+
138
138
  #### Parameters
139
139
 
140
140
  * `schema` **[Schema](#schema)**&#x20;
@@ -145,6 +145,8 @@ Returns **[parse](#parse)**&#x20;
145
145
 
146
146
  ### parse
147
147
 
148
+ [parse.mjs:78-214](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/parse.mjs#L78-L214 "Source code on GitHub")
149
+
148
150
  Parses and executes a serialized program.
149
151
 
150
152
  #### Parameters
@@ -155,17 +157,14 @@ Returns **(any | [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Ref
155
157
 
156
158
  ### References
157
159
 
158
- Builds a nested API structure from a schema definition.
159
-
160
- Schema supports:
160
+ [references.mjs:109-127](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/references.mjs#L109-L127 "Source code on GitHub")
161
161
 
162
- * string => leaf function node
163
- * \[name, children] => namespace with nested schema
162
+ Builds a nested API structure from a schema definition.
164
163
 
165
164
  #### Parameters
166
165
 
167
166
  * `schema` **[Schema](#schema)**&#x20;
168
- * `parentPath` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** (optional, default `[]`)
167
+ * `parentPath` (optional, default `[]`)
169
168
 
170
169
  <!---->
171
170
 
@@ -175,12 +174,16 @@ Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
175
174
 
176
175
  ### SchemaNode
177
176
 
177
+ [schema.mjs:1-19](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/schema.mjs#L1-L6 "Source code on GitHub")
178
+
178
179
  A single schema node.
179
180
 
180
181
  Type: ([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | \[[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), [Schema](#schema)])
181
182
 
182
183
  ### Schema
183
184
 
185
+ [schema.mjs:1-19](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/schema.mjs#L8-L18 "Source code on GitHub")
186
+
184
187
  A schema defines the structure of the runtime API tree.
185
188
 
186
189
  * string → leaf node
@@ -192,6 +195,8 @@ Type: [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global
192
195
 
193
196
  ### defaultLeafStringify
194
197
 
198
+ [stringify.mjs:37-47](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/stringify.mjs#L37-L47 "Source code on GitHub")
199
+
195
200
  Default strict serializer for leaf values.
196
201
 
197
202
  Rejects non-JSON-safe values.
@@ -204,6 +209,8 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
204
209
 
205
210
  ### Stringify
206
211
 
212
+ [stringify.mjs:56-157](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/stringify.mjs#L56-L157 "Source code on GitHub")
213
+
207
214
  Creates a stringify function for SendScript AST structures.
208
215
 
209
216
  #### Parameters
@@ -214,6 +221,8 @@ Returns **[stringify](#stringify)**&#x20;
214
221
 
215
222
  ### stringify
216
223
 
224
+ [stringify.mjs:64-156](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/stringify.mjs#L64-L156 "Source code on GitHub")
225
+
217
226
  Serializes a program into a JSON string representation.
218
227
 
219
228
  #### Parameters
@@ -224,6 +233,9 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
224
233
 
225
234
  ## Socket example
226
235
 
236
+ SendScript leaves it up to you to choose HTTP, web-sockets or any other method
237
+ of communication between servers and clients that best fits your needs.
238
+
227
239
  For this example we'll use [socket.io][socket.io].
228
240
 
229
241
  ### Module
@@ -330,6 +342,24 @@ run it by simply typing `sendscript` in their console.
330
342
  > Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for
331
343
  > printingonly sendscript logs.
332
344
 
345
+ ## Functions
346
+
347
+ SendScript as of `v2.4` supports functions to do basic templating. It does not
348
+ support async functions and will throw an error when you define one.
349
+
350
+ Under the hood the function is called when it is being parsed. Make sure you
351
+ understand what you are doing when mixing client and server functions.
352
+
353
+ ```js
354
+ map((a) => add(a, 1))([1, 2, 3]))
355
+ ```
356
+
357
+ It also supports nested functions.
358
+
359
+ ```js
360
+ map(call)(map((a) => () => add(a, 1))([1, 2, 3]))
361
+ ```
362
+
333
363
  ## Promises
334
364
 
335
365
  ### .then / .catch
package/README.mz CHANGED
@@ -74,17 +74,15 @@ core or around it. Things like supporting more complex (de)serializers, errors
74
74
  and maybe mixing client functions with sendscript programs. Contact me if I have
75
75
  piqued your interest.
76
76
 
77
- ---
78
-
79
- SendScript leaves it up to you to choose HTTP, web-sockets or any other method
80
- of communication between servers and clients that best fits your needs.
81
-
82
77
  ## Reference
83
78
 
84
79
  <!-- Reference -->
85
80
 
86
81
  ## Socket example
87
82
 
83
+ SendScript leaves it up to you to choose HTTP, web-sockets or any other method
84
+ of communication between servers and clients that best fits your needs.
85
+
88
86
  For this example we'll use [socket.io][socket.io].
89
87
 
90
88
  ### Module
@@ -189,6 +187,24 @@ run it by simply typing `sendscript` in their console.
189
187
  > Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for
190
188
  > printingonly sendscript logs.
191
189
 
190
+ ## Functions
191
+
192
+ SendScript as of `v2.4` supports functions to do basic templating. It does not
193
+ support async functions and will throw an error when you define one.
194
+
195
+ Under the hood the function is called when it is being parsed. Make sure you
196
+ understand what you are doing when mixing client and server functions.
197
+
198
+ ```js
199
+ map((a) => add(a, 1))([1, 2, 3]))
200
+ ```
201
+
202
+ It also supports nested functions.
203
+
204
+ ```js
205
+ map(call)(map((a) => () => add(a, 1))([1, 2, 3]))
206
+ ```
207
+
192
208
  ## Promises
193
209
 
194
210
  ### .then / .catch
@@ -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/a084b1dad2f76ceb3b79fac390210f1ccfa368f8/example/typescript/math.ts#L1)
11
+ Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/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/a084b1dad2f76ceb3b79fac390210f1ccfa368f8/example/typescript/math.ts#L2)
11
+ Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/5249a85a08b10485f0d64058fd2c8bcaae84edda/example/typescript/math.ts#L2)
12
12
 
13
13
  ## Parameters
14
14
 
package/index.test.mjs CHANGED
@@ -34,6 +34,7 @@ const myModuleOrig = {
34
34
  hello: 'world',
35
35
  noop: () => {},
36
36
  resolve: (x) => Promise.resolve(x),
37
+ call: (fn, ...args) => fn(...args),
37
38
  reject: x => Promise.reject(x),
38
39
  asyncFn: async () => 'my-async-function',
39
40
  instanceOf: (x, t) => x instanceof t,
@@ -85,15 +86,59 @@ test('should evaluate basic expressions correctly', async (t) => {
85
86
  always,
86
87
  multiply3,
87
88
  sayHello,
89
+ call,
88
90
  nested
89
91
  } = api
90
92
 
93
+ t.test('throws when async fn is provided', async t => {
94
+ t.throws(() => run(identity(async () => {})))
95
+ t.end()
96
+ })
97
+
98
+ t.test('supports fns with arguments', async t => {
99
+ t.same(run(map((a) => add(a, 1))([1, 2, 3])), [2, 3, 4])
100
+ t.end()
101
+ })
102
+
103
+ t.test('nested fns with arguments', async t => {
104
+ t.same(run(
105
+ map(call)(
106
+ map((a) => () => add(a, 1))([1, 2, 3])
107
+ )), [2, 3, 4])
108
+ t.end()
109
+ })
110
+
111
+ t.test('supports fns without arguments', async t => {
112
+ t.equal(run(() => add(1, 2))(), 3)
113
+ t.equal(run(always(() => 42)()()), 42)
114
+ t.equal(run(always(x => add(x, 42))()(2)), 44)
115
+ t.end()
116
+ })
117
+
118
+ t.test('supports binding reference functions on client', async t => {
119
+ const plusOne = add.bind(null, 1)
120
+
121
+ t.equal(run(plusOne(1)), 2)
122
+ t.equal(await run(resolve(1).then(plusOne)), 2)
123
+
124
+ // Even this works.
125
+ t.equal(await run(resolve(1).then(v => plusOne(v))), 2)
126
+
127
+ t.end()
128
+ })
129
+
91
130
  t.test('supports .then', async t => {
131
+ const something = 30
132
+
92
133
  t.rejects(run(reject('error')))
93
134
 
94
135
  t.equal(await run(reject('error').catch(sayHello)), 'hello error')
95
136
  t.equal(await run(reject('error').then(null, sayHello)), 'hello error')
96
137
 
138
+ // .then with fns just works and I do not know why.
139
+ t.equal(await run(resolve(32).then(() => add(something, 1))), 31)
140
+ t.equal(await run(resolve(32).then(value => add(something, value))), 62)
141
+
97
142
  t.equal(await run(resolve('value').then(sayHello)), 'hello value')
98
143
  t.equal(await run(resolve('value').then(sayHello, noop)), 'hello value')
99
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sendscript",
3
- "version": "2.3.8",
3
+ "version": "2.4.0",
4
4
  "description": "Blur the line between server and client code.",
5
5
  "module": true,
6
6
  "main": "index.mjs",
package/parse.mjs CHANGED
@@ -39,58 +39,6 @@ const spy = (type, fn) => (...args) => {
39
39
  return value
40
40
  }
41
41
 
42
- const evaluate = spy('eval', (value, awaits = []) => {
43
- if (value === undefinedSentinel) return undefined
44
-
45
- if (Array.isArray(value)) {
46
- const [operator, ...rest] = value
47
-
48
- if (operator === 'await') {
49
- const [index] = rest
50
- return awaits[index]
51
- }
52
-
53
- if (operator === 'then') {
54
- const [v, onResolve, onReject] = rest
55
- return evaluate(v, awaits).then(
56
- evaluate(onResolve, awaits),
57
- evaluate(onReject, awaits)
58
- )
59
- }
60
-
61
- if (operator === 'call') {
62
- let [fn, args] = rest
63
- fn = evaluate(fn, awaits)
64
-
65
- for (let i = 0; i < args.length; i++) {
66
- args[i] = evaluate(args[i], awaits)
67
- }
68
-
69
- return fn(...args)
70
- }
71
-
72
- if (operator === 'quote') {
73
- const [quoted] = rest
74
- return quoted
75
- }
76
-
77
- for (let i = 0; i < value.length; i++) {
78
- value[i] = evaluate(value[i], awaits)
79
- }
80
-
81
- return value
82
- }
83
-
84
- if (isPlainObject(value)) {
85
- for (const key of Object.keys(value)) {
86
- value[key] = evaluate(value[key], awaits)
87
- }
88
- return value
89
- }
90
-
91
- return value
92
- })
93
-
94
42
  /**
95
43
  * Default deserializer for leaf nodes.
96
44
  *
@@ -131,6 +79,76 @@ export default function Parse (schema, env, leafParse = defaultLeafParse) {
131
79
  debug('program', program)
132
80
  const awaits = []
133
81
 
82
+ const evaluateOuter = spy('eval', (env, value) => {
83
+ const evaluate = evaluateOuter.bind(null, env)
84
+
85
+ if (value === undefinedSentinel) return undefined
86
+
87
+ if (Array.isArray(value)) {
88
+ const [operator, ...rest] = value
89
+
90
+ if (operator === 'await') {
91
+ const [index] = rest
92
+ return awaits[index]
93
+ }
94
+
95
+ if (operator === 'then') {
96
+ const [v, onResolve, onReject] = rest
97
+ return evaluate(v).then(
98
+ evaluate(onResolve),
99
+ evaluate(onReject)
100
+ )
101
+ }
102
+
103
+ if (operator === 'fn') {
104
+ const [argIds, body] = [...rest]
105
+
106
+ return (...args) => {
107
+ const newEnv = Object.create(env)
108
+
109
+ argIds.forEach((id, index) => {
110
+ newEnv[id] = args[index]
111
+ })
112
+
113
+ return evaluateOuter(newEnv, body)
114
+ }
115
+ }
116
+
117
+ if (operator === 'arg') {
118
+ const [id] = rest
119
+
120
+ return env[id]
121
+ }
122
+
123
+ if (operator === 'call') {
124
+ let [fn, args] = rest
125
+ fn = evaluate(fn)
126
+
127
+ return fn(...args.map(evaluate))
128
+ }
129
+
130
+ if (operator === 'quote') {
131
+ const [quoted] = rest
132
+ return quoted
133
+ }
134
+
135
+ for (let i = 0; i < value.length; i++) {
136
+ value[i] = evaluate(value[i])
137
+ }
138
+
139
+ return value
140
+ }
141
+
142
+ if (isPlainObject(value)) {
143
+ for (const key of Object.keys(value)) {
144
+ value[key] = evaluate(value[key])
145
+ }
146
+ return value
147
+ }
148
+
149
+ return value
150
+ })
151
+
134
152
  /**
135
153
  * JSON reviver used during parsing.
136
154
  * Converts encoded operators into runtime structures.
@@ -184,14 +202,14 @@ export default function Parse (schema, env, leafParse = defaultLeafParse) {
184
202
 
185
203
  return (async function () {
186
204
  for (let i = 0; i < awaits.length; i++) {
187
- awaits[i] = await evaluate(awaits[i], awaits)
205
+ awaits[i] = await evaluateOuter({}, awaits[i])
188
206
  }
189
207
 
190
208
  debug('awaits(awaited)', awaits)
191
- return evaluate(parsed, awaits)
209
+ return evaluateOuter({}, parsed)
192
210
  })()
193
211
  }
194
212
 
195
- return evaluate(parsed)
213
+ return evaluateOuter({}, parsed)
196
214
  }
197
215
  }
package/references.mjs CHANGED
@@ -100,12 +100,7 @@ function instrument (path) {
100
100
  /**
101
101
  * Builds a nested API structure from a schema definition.
102
102
  *
103
- * Schema supports:
104
- * - string => leaf function node
105
- * - [name, children] => namespace with nested schema
106
- *
107
103
  * @param {Schema} schema
108
- * @param {Array<string>} [parentPath=[]]
109
104
  * @returns {Object} Nested instrumented API object
110
105
  *
111
106
  * @throws {Error} If schema format is invalid
package/stringify.mjs CHANGED
@@ -2,14 +2,20 @@ import Debug from './debug.mjs'
2
2
  import { SendScriptSerializationError } from './error.mjs'
3
3
  import {
4
4
  awaitSymbol,
5
+ referenceSymbol,
5
6
  then,
6
7
  call,
7
8
  ref
8
9
  } from './symbol.mjs'
9
10
 
11
+ const asyncFunction = async function () {}
12
+ asyncFunction()
13
+ const AsyncFunction = (asyncFunction).constructor
14
+
10
15
  const debug = Debug.extend('stringify')
11
16
 
12
- const keywords = ['ref', 'call', 'quote', 'await', 'leaf']
17
+ const keywords = ['ref', 'call', 'quote', 'await', 'leaf', 'arg', 'fn']
18
+ const argSymbol = Symbol('arg')
13
19
 
14
20
  const isKeyword = (v) => keywords.includes(v)
15
21
 
@@ -19,67 +25,6 @@ const isPlainObject = (value) => {
19
25
  return proto === Object.prototype || proto === null
20
26
  }
21
27
 
22
- function transformValue (value, leafSerializer) {
23
- debug(value)
24
-
25
- if (value === null) return null
26
-
27
- // unwrap function wrappers (instrumented nodes)
28
- if (typeof value === 'function' && typeof value.toJSON === 'function') {
29
- return transformValue(value.toJSON(), leafSerializer)
30
- }
31
-
32
- if (value && value[ref]) {
33
- return ['ref', ...value.path]
34
- }
35
-
36
- if (value && value[call]) {
37
- return [
38
- 'call',
39
- transformValue(value.ref, leafSerializer),
40
- transformValue(value.args, leafSerializer)
41
- ]
42
- }
43
-
44
- if (value && value[awaitSymbol]) {
45
- return ['await', transformValue(value.ref, leafSerializer)]
46
- }
47
-
48
- if (value && value[then]) {
49
- return [
50
- 'then',
51
- transformValue(value.ref, leafSerializer),
52
- transformValue(value.resolve || null, leafSerializer),
53
- transformValue(value.reject || null, leafSerializer)
54
- ]
55
- }
56
-
57
- if (Array.isArray(value)) {
58
- const [operator, ...rest] = value
59
-
60
- if (isKeyword(operator)) {
61
- return [
62
- ['quote', operator],
63
- ...rest.map((item) => transformValue(item, leafSerializer))
64
- ]
65
- }
66
-
67
- return value.map((item) => transformValue(item, leafSerializer))
68
- }
69
-
70
- if (isPlainObject(value)) {
71
- const result = {}
72
-
73
- for (const key of Object.keys(value)) {
74
- result[key] = transformValue(value[key], leafSerializer)
75
- }
76
-
77
- return result
78
- }
79
-
80
- return ['leaf', leafSerializer(value)]
81
- }
82
-
83
28
  /**
84
29
  * Default strict serializer for leaf values.
85
30
  *
@@ -117,6 +62,96 @@ export default function Stringify (leafStringify = defaultLeafStringify) {
117
62
  * @public
118
63
  */
119
64
  return function stringify (program) {
120
- return JSON.stringify(transformValue(program, leafStringify))
65
+ // Reset the argId
66
+ let argId = 0
67
+
68
+ function transformValue (value) {
69
+ debug(value)
70
+
71
+ if (value === null) return null
72
+
73
+ // unwrap function wrappers (instrumented nodes)
74
+ if (value && value[referenceSymbol]) {
75
+ return transformValue(value.toJSON())
76
+ }
77
+
78
+ // Is a simple function. We call it to get the template.
79
+ if (typeof value === 'function') {
80
+ if (value instanceof AsyncFunction) {
81
+ throw new SendScriptSerializationError('Sendscript does not support async functions as of yet.')
82
+ }
83
+
84
+ const args = []
85
+ const argIds = []
86
+
87
+ for (let i = 0; i < value.length; i++) {
88
+ const arg = argId++
89
+ const placeholder = ['arg', arg]
90
+
91
+ placeholder[argSymbol] = argSymbol
92
+
93
+ args.push(placeholder)
94
+ argIds.push(arg)
95
+ }
96
+
97
+ return ['fn', argIds, transformValue(value(...args))]
98
+ }
99
+
100
+ if (value && value[argSymbol]) {
101
+ return value
102
+ }
103
+
104
+ if (value && value[ref]) {
105
+ return ['ref', ...value.path]
106
+ }
107
+
108
+ if (value && value[call]) {
109
+ return [
110
+ 'call',
111
+ transformValue(value.ref),
112
+ transformValue(value.args)
113
+ ]
114
+ }
115
+
116
+ if (value && value[awaitSymbol]) {
117
+ return ['await', transformValue(value.ref)]
118
+ }
119
+
120
+ if (value && value[then]) {
121
+ return [
122
+ 'then',
123
+ transformValue(value.ref),
124
+ transformValue(value.resolve || null),
125
+ transformValue(value.reject || null)
126
+ ]
127
+ }
128
+
129
+ if (Array.isArray(value)) {
130
+ const [operator, ...rest] = value
131
+
132
+ if (isKeyword(operator)) {
133
+ return [
134
+ ['quote', operator],
135
+ ...rest.map((item) => transformValue(item))
136
+ ]
137
+ }
138
+
139
+ return value.map((item) => transformValue(item))
140
+ }
141
+
142
+ if (isPlainObject(value)) {
143
+ const result = {}
144
+
145
+ for (const key of Object.keys(value)) {
146
+ result[key] = transformValue(value[key])
147
+ }
148
+
149
+ return result
150
+ }
151
+
152
+ return ['leaf', leafStringify(value)]
153
+ }
154
+
155
+ return JSON.stringify(transformValue(program))
121
156
  }
122
157
  }