serialize-function 1.2.6 → 2.0.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/README.md CHANGED
@@ -14,6 +14,15 @@
14
14
  ![npm](https://nodei.co/npm/serialize-function.png)
15
15
  ](https://www.npmjs.com/package/serialize-function)
16
16
 
17
+ ---
18
+
19
+ - [Quickstart](#quickstart)
20
+ - [Deep serialization](#deep-serialization)
21
+ - [Hashing](#hashing)
22
+ - [Whitespace and comments](#whitespace-and-comments)
23
+ - [Function type support](#function-type-support)
24
+ - [Changelog](#changelog)
25
+
17
26
 
18
27
  ## Quickstart
19
28
 
@@ -30,7 +39,7 @@ Serializes javascript functions to a JSON-encodable object suitable for storage
30
39
  ```js
31
40
  function doTheThing(a,b,c,d,e) { return a + b * c / d % e; }
32
41
 
33
- const obj = serialize(doTheThing);
42
+ const obj = await serialize(doTheThing);
34
43
  console.log(obj);
35
44
  // {
36
45
  // params: [ 'a', 'b', 'c', 'd', 'e' ],
@@ -42,17 +51,70 @@ console.log(obj);
42
51
  Deserializes back into an invokable function:
43
52
 
44
53
  ```js
45
- const func = deserialize(obj);
54
+ const func = await deserialize(obj);
46
55
  console.log( func(1, 2, 3, 4, 5) );
47
56
  // 2.5
48
57
  ```
49
58
 
59
+
60
+ ## Deep serialization
61
+
62
+ You may want to deeply serialize _any_ functions nested at arbitrary levels of your data structures. The provided convenience functions will traverse and selectively clone any containing objects, while serializing any functions found:
63
+
64
+ ```js
65
+ const { deepSerialize, deepDeserialize } = require('serialize-function');
66
+
67
+ const original = {
68
+ foo: () => 'something',
69
+ bar: [
70
+ function* (seed = 0) { let n = seed; while(true) { n = n * 2; yield n; } }
71
+ ],
72
+ baz: new Date('2026-01-01')
73
+ }
74
+
75
+ const clone = await deepSerialize(original);
76
+ // {
77
+ // foo: {
78
+ // params: [],
79
+ // body: "return ('something');",
80
+ // type: 'ArrowFunction'
81
+ // },
82
+ // bar: [
83
+ // {
84
+ // params: [ 'seed = 0' ],
85
+ // body: 'let n = seed; while(true) { n = n * 2; yield n; }',
86
+ // type: 'Generator'
87
+ // }
88
+ // ],
89
+ // baz: 2026-01-01T00:00:00.000Z
90
+ // }
91
+
92
+ // original container and functions remain unmodified
93
+ original.foo(); // 'something'
94
+ const gen1 = original.bar[0](3.14);
95
+ gen1.next().value; // 6.28
96
+ gen1.next().value; // 12.56
97
+
98
+ const restored = await deepDeserialize(clone);
99
+ // {
100
+ // foo: [Function: anonymous],
101
+ // bar: [ [GeneratorFunction: anonymous] ],
102
+ // baz: 2026-01-01T00:00:00.000Z
103
+ // }
104
+
105
+ // deserialized functions remain invokable
106
+ restored.foo(); // 'something'
107
+ const gen2 = restored.bar[0](901364);
108
+ gen2.next().value; // 1802728
109
+ gen2.next().value; // 3605456
110
+ ```
111
+
112
+
50
113
  ## Hashing
51
114
 
52
115
  Optionally supports SHA256 checksum hashing to prevent MITM tampering:
53
116
 
54
117
  ```js
55
- // note: use of hashing returns a promise
56
118
  const hashedObj = await serialize(doTheThing, { hash: true });
57
119
  console.log(hashedObj);
58
120
  // {
@@ -69,6 +131,7 @@ const tamperedFunc = await deserialize(hashedObj, { hash: true });
69
131
 
70
132
  > Under the hood, hashing uses the [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) API.
71
133
 
134
+
72
135
  ## Whitespace and comments
73
136
 
74
137
  Line breaks within the function body are preserved and normalized, but all other padding whitespace is removed from the function by default, along with any comments.
@@ -97,7 +160,7 @@ function thingNumberTwo(
97
160
  return remainder;
98
161
  }
99
162
 
100
- const commentedObj = serialize(thingNumberTwo, { whitespace: true, comments: true });
163
+ const commentedObj = await serialize(thingNumberTwo, { whitespace: true, comments: true });
101
164
  console.log(commentedObj);
102
165
  // {
103
166
  // params: [ '\n /* marco */\n a', ' b', '\tc', '\n d', 'e/* polo */\n' ],
@@ -120,12 +183,13 @@ console.log(commentedObj);
120
183
  // }
121
184
  ```
122
185
 
186
+
123
187
  ## Function type support
124
188
 
125
189
  Arrow functions, generators, and all async variants are supported (contingent on _browser support_ where relevant):
126
190
 
127
191
  ```js
128
- serialize(
192
+ await serialize(
129
193
  (i,j,k) => ({ i, j, k })
130
194
  );
131
195
  // {
@@ -134,7 +198,7 @@ serialize(
134
198
  // type: 'ArrowFunction'
135
199
  // }
136
200
 
137
- serialize(
201
+ await serialize(
138
202
  function* (x,y,z) {
139
203
  yield x;
140
204
  yield y;
@@ -147,7 +211,7 @@ serialize(
147
211
  // type: 'Generator'
148
212
  // }
149
213
 
150
- serialize(
214
+ await serialize(
151
215
  async (ms) => new Promise(
152
216
  resolve => setTimeout(resolve, ms)
153
217
  )
@@ -158,3 +222,22 @@ serialize(
158
222
  // type: 'AsyncArrowFunction'
159
223
  // }
160
224
  ```
225
+
226
+ > [!NOTE]
227
+ > As there is no global `Class` object constructor, there is no way to safely deserialize [ES6 classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
228
+ >
229
+ > As such, ES6 classes are _not_ currently supported for serialization.
230
+ >
231
+ > Alternatively, you can rewrite your classes as functions, or transpile them with tools like [Babel](https://babeljs.io/docs/babel-plugin-transform-classes/).
232
+
233
+
234
+ ## Changelog
235
+
236
+ Any potentially breaking changes will be documented here.
237
+
238
+ - 1.1.0 - Standardized both node and web builds on SubtleCrypto API
239
+ - 1.2.0 - Refactored comment stripping, to address potential regex DOS
240
+ - 2.0.0
241
+ - Made all exported functions fully async
242
+ - Implemented named captures for format patterns
243
+ - Implemented deep de/serialization
package/dist/import.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  // ESM support
2
- import { serialize, deserialize } from './main.js';
3
-
4
- export { serialize, deserialize }
5
- export default { serialize, deserialize }
2
+ import * as everything from './main.js';
3
+ export default everything;
4
+ export * from './main.js';
package/dist/main.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.deserialize=deserialize;exports.serialize=serialize;class JsonError extends Error{};class CryptoError extends Error{};class SerializeError extends Error{}class DeserializeError extends Error{}class ChecksumError extends Error{}class ConstructError extends Error{}const AsyncFunction=async function(){}.constructor;const Generator=function*(){}.constructor;const AsyncGenerator=async function*(){}.constructor;const formatPatterns={"Generator":/^(async\s+)?function\*\s*[^()]*\(([^)]*)\)\s*{([\s\S]*)}$/,"Function":/^(async\s+)?function\s*[^()]*\(([^)]*)\)\s*{([\s\S]*)}$/,"ArrowFunction":/^(async\s+)?(?:\(([^)]*)\)|([^=\s(]+))\s*=>\s*(?:{([\s\S]*)}|([\s\S]+))$/};async function hasher(obj){let json,hashed;try{json=JSON.stringify(obj)}catch(cause){throw new JsonError("Failed to stringify serialized function structure",{cause})}try{const hashBuffer=await(globalThis?.crypto?.subtle??window?.crypto?.subtle).digest("SHA-256",new TextEncoder().encode(json));hashed=Array.from(new Uint8Array(hashBuffer)).map(item=>item.toString(16).padStart(2,"0")).join("")}catch(cause){throw new CryptoError("Failed to generate hash digest",{cause})}return hashed}function getConstructor(type){switch(type){case"Function":case"ArrowFunction":return Function;case"AsyncFunction":case"AsyncArrowFunction":return AsyncFunction;case"Generator":return Generator;case"AsyncGenerator":return AsyncGenerator;default:throw new ConstructError(`Unexpected type ${type}`)}}function removeComments(input){let[...output]=`__${input}__`;const mode={singleQuote:false,doubleQuote:false,regex:false,blockComment:false,lineComment:false,condComp:false};for(let i=0,l=output.length;i<l;i++){if(mode.regex){if(output[i]==="/"&&output[i-1]!=="\\")mode.regex=false;continue}if(mode.singleQuote){if(output[i]==="'"&&output[i-1]!=="\\")mode.singleQuote=false;continue}if(mode.doubleQuote){if(output[i]==="\""&&output[i-1]!=="\\")mode.doubleQuote=false;continue}if(mode.blockComment){if(output[i]==="*"&&output[i+1]==="/"){output[i+1]="";mode.blockComment=false}output[i]="";continue}if(mode.lineComment){if(output[i+1]==="\n"||output[i+1]==="\r")mode.lineComment=false;output[i]="";continue}if(mode.condComp){if(output[i-2]==="@"&&output[i-1]==="*"&&output[i]==="/")mode.condComp=false;continue}mode.doubleQuote=output[i]==="\"";mode.singleQuote=output[i]==="'";if(output[i]==="/"){if(output[i+1]==="*"&&output[i+2]==="@"){mode.condComp=true;continue}if(output[i+1]==="*"){output[i]="";mode.blockComment=true;continue}if(output[i+1]==="/"){output[i]="";mode.lineComment=true;continue}mode.regex=true}}return output.join("").slice(2,-2)}function serialize(func,opts){const def={hash:false,comments:false,whitespace:false};opts=typeof opts==="object"&&null!==opts?Object.assign({},def,opts):Object.assign({},def);const typed=typeof func;if(typed!=="function"){throw new SerializeError("Invalid argument type, must be a function",{cause:{"typeof":typed}})}let stringified=func.toString();if(!opts.comments){stringified=removeComments(stringified)}if(!opts.whitespace){stringified=stringified.split(/[\r\n]+/).map(line=>line.trim()).filter(line=>line!=="").join("\n")}let match,serialized;for(const[type,pattern]of Object.entries(formatPatterns)){try{match=stringified.match(pattern);if(match){let async=match[1]?"Async":"";let params=type==="ArrowFunction"?match[2]??match[3]:match[2];params=params.split(",").map(p=>opts.whitespace?p:p.trim()).filter(Boolean);let body=type==="ArrowFunction"?match[4]??`return (${match[5]});`:match[3];if(!opts.whitespace)body=body.trim();serialized={params,body,type:`${async}${type}`};break}}catch(cause){throw new SerializeError(`Unexpected error serializing ${type}`,{cause})}}if(!serialized){throw new SerializeError("Unsupported function format",{cause:stringified})}if(opts.hash){return hasher(serialized).then(hashed=>{serialized.hash=hashed;return serialized}).catch(cause=>{throw new SerializeError("Failure hashing serialized function",{cause})})}return serialized}function deserialize(struct,opts={hash:false}){if(opts?.hash){if(struct?.hash===undefined){throw new DeserializeError("Deserialized function missing hash")}const test=Object.assign({},struct);delete test.hash;return hasher(test).then(checksum=>{if(checksum!==struct.hash){throw new ChecksumError("Checksum failed",{cause:{a:checksum,b:struct.hash}})}return deserialize(struct,{hash:false})}).catch(cause=>{if(cause instanceof ChecksumError||cause instanceof DeserializeError||cause instanceof ConstructError){throw cause}throw new DeserializeError("Failure generating checksum",{cause})})}try{const constructor=getConstructor(struct.type);return new constructor(...struct.params,struct.body)}catch(cause){if(cause instanceof ConstructError)throw cause;throw new DeserializeError("Failure deserializing",{cause})}}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SerializeError=exports.JsonError=exports.DeserializeError=exports.CryptoError=exports.ConstructError=exports.ChecksumError=void 0;exports.deepDeserialize=deepDeserialize;exports.deepSerialize=deepSerialize;exports.deserialize=deserialize;exports.serialize=serialize;class JsonError extends Error{}exports.JsonError=JsonError;;class CryptoError extends Error{}exports.CryptoError=CryptoError;;class SerializeError extends Error{}exports.SerializeError=SerializeError;;class DeserializeError extends Error{}exports.DeserializeError=DeserializeError;;class ChecksumError extends Error{}exports.ChecksumError=ChecksumError;;class ConstructError extends Error{}exports.ConstructError=ConstructError;;const AsyncFunction=async function(){}.constructor;const Generator=function*(){}.constructor;const AsyncGenerator=async function*(){}.constructor;const formatPatterns={"Generator":/^(?<isAsync>async\s+)?function\*\s*[^()]*\((?<params>[^)]*)\)\s*{(?<body>[\s\S]*)}$/,"Function":/^(?<isAsync>async\s+)?function\s*[^()]*\((?<params>[^)]*)\)\s*{(?<body>[\s\S]*)}$/,"ArrowFunction":/^(?<isAsync>async\s+)?(?:\((?<params>[^)]*)\)|(?<singleParam>[^=\s(]+))\s*=>\s*(?:{(?<bracedBody>[\s\S]*)}|(?<bodyExpr>[\s\S]+))$/};async function hasher(obj){let json,hashed;try{json=JSON.stringify(obj)}catch(cause){throw new JsonError("Failed to stringify serialized function structure",{cause})}try{const hashBuffer=await(globalThis?.crypto?.subtle??window?.crypto?.subtle).digest("SHA-256",new TextEncoder().encode(json));hashed=Array.from(new Uint8Array(hashBuffer)).map(item=>item.toString(16).padStart(2,"0")).join("")}catch(cause){throw new CryptoError("Failed to generate hash digest",{cause})}return hashed}function getConstructor(type){switch(type){case"Function":case"ArrowFunction":return Function;case"AsyncFunction":case"AsyncArrowFunction":return AsyncFunction;case"Generator":return Generator;case"AsyncGenerator":return AsyncGenerator;default:throw new ConstructError(`Unexpected type ${type}`)}}function removeComments(input){let[...output]=`__${input}__`;const mode={singleQuote:false,doubleQuote:false,regex:false,blockComment:false,lineComment:false};for(let i=0,l=output.length;i<l;i++){if(mode.regex){if(output[i]==="/"&&output[i-1]!=="\\")mode.regex=false;continue}if(mode.singleQuote){if(output[i]==="'"&&output[i-1]!=="\\")mode.singleQuote=false;continue}if(mode.doubleQuote){if(output[i]==="\""&&output[i-1]!=="\\")mode.doubleQuote=false;continue}if(mode.blockComment){if(output[i]==="*"&&output[i+1]==="/"){output[i+1]="";mode.blockComment=false}output[i]="";continue}if(mode.lineComment){if(output[i+1]==="\n"||output[i+1]==="\r")mode.lineComment=false;output[i]="";continue}mode.doubleQuote=output[i]==="\"";mode.singleQuote=output[i]==="'";if(output[i]==="/"){if(output[i+1]==="*"){output[i]="";mode.blockComment=true;continue}if(output[i+1]==="/"){output[i]="";mode.lineComment=true;continue}mode.regex=true}}return output.join("").slice(2,-2)}async function serialize(func,opts){const def={hash:false,comments:false,whitespace:false};opts=typeof opts==="object"&&null!==opts?Object.assign({},def,opts):Object.assign({},def);const typed=typeof func;if(typed!=="function"){throw new SerializeError("Invalid argument type, must be a function",{cause:{"typeof":typed}})}let stringified=func.toString();if(!opts.comments){stringified=removeComments(stringified)}if(!opts.whitespace){stringified=stringified.split(/[\r\n]+/).map(line=>line.trim()).filter(line=>line!=="").join("\n")}let match,serialized;for(const[type,pattern]of Object.entries(formatPatterns)){try{match=stringified.match(pattern);if(match){let async=match.groups.isAsync?"Async":"";let params=type==="ArrowFunction"?match.groups.params??match.groups.singleParam:match.groups.params;params=params.split(",").map(p=>opts.whitespace?p:p.trim()).filter(Boolean);let body=type==="ArrowFunction"?match.groups.bracedBody??`return (${match.groups.bodyExpr});`:match.groups.body;if(!opts.whitespace)body=body.trim();serialized={params,body,type:`${async}${type}`};break}}catch(cause){throw new SerializeError(`Unexpected error serializing ${type}`,{cause})}}if(!serialized){throw new SerializeError("Unsupported function format",{cause:stringified})}if(opts.hash){try{const hashed=await hasher(serialized);serialized.hash=hashed}catch(cause){throw new SerializeError("Failure hashing serialized function",{cause})}}return serialized}async function deserialize(struct,opts={hash:false}){if(opts?.hash){if(struct?.hash===undefined){throw new DeserializeError("Deserialized function missing hash")}const test=Object.assign({},struct);delete test.hash;try{const checksum=await hasher(test);if(checksum!==struct.hash){throw new ChecksumError("Checksum failed",{cause:{a:checksum,b:struct.hash}})}}catch(cause){if(cause instanceof ChecksumError)throw cause;throw new DeserializeError("Failure generating checksum",{cause})}}try{const constructor=getConstructor(struct.type);return new constructor(...struct.params,struct.body)}catch(cause){if(cause instanceof ConstructError)throw cause;throw new DeserializeError("Failure deserializing",{cause})}}async function traverse(input,tester,converter){if(tester(input)){return await converter(input)}if(input===null||typeof input!=="object"){return input}const nonTraversables=[String,Boolean,Number,Date,RegExp];for(const constructor of nonTraversables){if(input instanceof constructor){return new constructor(input.valueOf())}}if(input instanceof Array){const cloned=[];for(let i=0;i<input.length;i++){if(tester(input[i]))cloned[i]=await converter(input[i]);else cloned[i]=await traverse(input[i],tester,converter)}return cloned}if(input instanceof Set){const cloned=new Set;for(const value of input){if(tester(value))cloned.add(await converter(value));else cloned.add(await traverse(value,tester,converter))}return cloned}if(input instanceof Map){const cloned=new Map;for(const[key,value]of input){if(tester(value))cloned.set(key,await converter(value));else cloned.set(key,await traverse(value,tester,converter))}return cloned}if(input instanceof Object){const cloned=Object.create(Object.getPrototypeOf(input));for(const key in input){if(Object.hasOwn(input,key)){if(tester(input[key]))cloned[key]=await converter(input[key]);else cloned[key]=await traverse(input[key],tester,converter)}}return cloned}return input}async function deepSerialize(value,options){try{return await traverse(value,input=>typeof input==="function",input=>serialize(input,options))}catch(cause){throw new SerializeError("Failure traversing and serializing",{cause})}}async function deepDeserialize(value,options){try{return await traverse(value,input=>typeof input==="object"&&Object.hasOwn(input,"params")&&Object.hasOwn(input,"body")&&Object.hasOwn(input,"type"),input=>deserialize(input,Object.assign({hash:Object.hasOwn(input,"hash")},options)))}catch(cause){throw new DeserializeError("Failure traversing and deserializing",{cause})}}
package/lib/main.js CHANGED
@@ -1 +1 @@
1
- class JsonError extends Error{};class CryptoError extends Error{};class SerializeError extends Error{}class DeserializeError extends Error{}class ChecksumError extends Error{}class ConstructError extends Error{}const AsyncFunction=async function(){}.constructor;const Generator=function*(){}.constructor;const AsyncGenerator=async function*(){}.constructor;const formatPatterns={"Generator":/^(async\s+)?function\*\s*[^()]*\(([^)]*)\)\s*{([\s\S]*)}$/,"Function":/^(async\s+)?function\s*[^()]*\(([^)]*)\)\s*{([\s\S]*)}$/,"ArrowFunction":/^(async\s+)?(?:\(([^)]*)\)|([^=\s(]+))\s*=>\s*(?:{([\s\S]*)}|([\s\S]+))$/};async function hasher(obj){let json,hashed;try{json=JSON.stringify(obj)}catch(cause){throw new JsonError("Failed to stringify serialized function structure",{cause})}try{const hashBuffer=await(globalThis?.crypto?.subtle??window?.crypto?.subtle).digest("SHA-256",new TextEncoder().encode(json));hashed=Array.from(new Uint8Array(hashBuffer)).map(item=>item.toString(16).padStart(2,"0")).join("")}catch(cause){throw new CryptoError("Failed to generate hash digest",{cause})}return hashed}function getConstructor(type){switch(type){case"Function":case"ArrowFunction":return Function;case"AsyncFunction":case"AsyncArrowFunction":return AsyncFunction;case"Generator":return Generator;case"AsyncGenerator":return AsyncGenerator;default:throw new ConstructError(`Unexpected type ${type}`)}}function removeComments(input){let[...output]=`__${input}__`;const mode={singleQuote:false,doubleQuote:false,regex:false,blockComment:false,lineComment:false,condComp:false};for(let i=0,l=output.length;i<l;i++){if(mode.regex){if(output[i]==="/"&&output[i-1]!=="\\")mode.regex=false;continue}if(mode.singleQuote){if(output[i]==="'"&&output[i-1]!=="\\")mode.singleQuote=false;continue}if(mode.doubleQuote){if(output[i]==="\""&&output[i-1]!=="\\")mode.doubleQuote=false;continue}if(mode.blockComment){if(output[i]==="*"&&output[i+1]==="/"){output[i+1]="";mode.blockComment=false}output[i]="";continue}if(mode.lineComment){if(output[i+1]==="\n"||output[i+1]==="\r")mode.lineComment=false;output[i]="";continue}if(mode.condComp){if(output[i-2]==="@"&&output[i-1]==="*"&&output[i]==="/")mode.condComp=false;continue}mode.doubleQuote=output[i]==="\"";mode.singleQuote=output[i]==="'";if(output[i]==="/"){if(output[i+1]==="*"&&output[i+2]==="@"){mode.condComp=true;continue}if(output[i+1]==="*"){output[i]="";mode.blockComment=true;continue}if(output[i+1]==="/"){output[i]="";mode.lineComment=true;continue}mode.regex=true}}return output.join("").slice(2,-2)}function serialize(func,opts){const def={hash:false,comments:false,whitespace:false};opts=typeof opts==="object"&&null!==opts?Object.assign({},def,opts):Object.assign({},def);const typed=typeof func;if(typed!=="function"){throw new SerializeError("Invalid argument type, must be a function",{cause:{"typeof":typed}})}let stringified=func.toString();if(!opts.comments){stringified=removeComments(stringified)}if(!opts.whitespace){stringified=stringified.split(/[\r\n]+/).map(line=>line.trim()).filter(line=>line!=="").join("\n")}let match,serialized;for(const[type,pattern]of Object.entries(formatPatterns)){try{match=stringified.match(pattern);if(match){let async=match[1]?"Async":"";let params=type==="ArrowFunction"?match[2]??match[3]:match[2];params=params.split(",").map(p=>opts.whitespace?p:p.trim()).filter(Boolean);let body=type==="ArrowFunction"?match[4]??`return (${match[5]});`:match[3];if(!opts.whitespace)body=body.trim();serialized={params,body,type:`${async}${type}`};break}}catch(cause){throw new SerializeError(`Unexpected error serializing ${type}`,{cause})}}if(!serialized){throw new SerializeError("Unsupported function format",{cause:stringified})}if(opts.hash){return hasher(serialized).then(hashed=>{serialized.hash=hashed;return serialized}).catch(cause=>{throw new SerializeError("Failure hashing serialized function",{cause})})}return serialized}function deserialize(struct){let opts=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{hash:false};if(opts?.hash){if(struct?.hash===undefined){throw new DeserializeError("Deserialized function missing hash")}const test=Object.assign({},struct);delete test.hash;return hasher(test).then(checksum=>{if(checksum!==struct.hash){throw new ChecksumError("Checksum failed",{cause:{a:checksum,b:struct.hash}})}return deserialize(struct,{hash:false})}).catch(cause=>{if(cause instanceof ChecksumError||cause instanceof DeserializeError||cause instanceof ConstructError){throw cause}throw new DeserializeError("Failure generating checksum",{cause})})}try{const constructor=getConstructor(struct.type);return new constructor(...struct.params,struct.body)}catch(cause){if(cause instanceof ConstructError)throw cause;throw new DeserializeError("Failure deserializing",{cause})}}export{serialize,deserialize};
1
+ class JsonError extends Error{};class CryptoError extends Error{};class SerializeError extends Error{};class DeserializeError extends Error{};class ChecksumError extends Error{};class ConstructError extends Error{};const AsyncFunction=async function(){}.constructor;const Generator=function*(){}.constructor;const AsyncGenerator=async function*(){}.constructor;const formatPatterns={"Generator":/^(?<isAsync>async\s+)?function\*\s*[^()]*\((?<params>[^)]*)\)\s*{(?<body>[\s\S]*)}$/,"Function":/^(?<isAsync>async\s+)?function\s*[^()]*\((?<params>[^)]*)\)\s*{(?<body>[\s\S]*)}$/,"ArrowFunction":/^(?<isAsync>async\s+)?(?:\((?<params>[^)]*)\)|(?<singleParam>[^=\s(]+))\s*=>\s*(?:{(?<bracedBody>[\s\S]*)}|(?<bodyExpr>[\s\S]+))$/};async function hasher(obj){let json,hashed;try{json=JSON.stringify(obj)}catch(cause){throw new JsonError("Failed to stringify serialized function structure",{cause})}try{const hashBuffer=await(globalThis?.crypto?.subtle??window?.crypto?.subtle).digest("SHA-256",new TextEncoder().encode(json));hashed=Array.from(new Uint8Array(hashBuffer)).map(item=>item.toString(16).padStart(2,"0")).join("")}catch(cause){throw new CryptoError("Failed to generate hash digest",{cause})}return hashed}function getConstructor(type){switch(type){case"Function":case"ArrowFunction":return Function;case"AsyncFunction":case"AsyncArrowFunction":return AsyncFunction;case"Generator":return Generator;case"AsyncGenerator":return AsyncGenerator;default:throw new ConstructError(`Unexpected type ${type}`)}}function removeComments(input){let[...output]=`__${input}__`;const mode={singleQuote:false,doubleQuote:false,regex:false,blockComment:false,lineComment:false};for(let i=0,l=output.length;i<l;i++){if(mode.regex){if(output[i]==="/"&&output[i-1]!=="\\")mode.regex=false;continue}if(mode.singleQuote){if(output[i]==="'"&&output[i-1]!=="\\")mode.singleQuote=false;continue}if(mode.doubleQuote){if(output[i]==="\""&&output[i-1]!=="\\")mode.doubleQuote=false;continue}if(mode.blockComment){if(output[i]==="*"&&output[i+1]==="/"){output[i+1]="";mode.blockComment=false}output[i]="";continue}if(mode.lineComment){if(output[i+1]==="\n"||output[i+1]==="\r")mode.lineComment=false;output[i]="";continue}mode.doubleQuote=output[i]==="\"";mode.singleQuote=output[i]==="'";if(output[i]==="/"){if(output[i+1]==="*"){output[i]="";mode.blockComment=true;continue}if(output[i+1]==="/"){output[i]="";mode.lineComment=true;continue}mode.regex=true}}return output.join("").slice(2,-2)}async function serialize(func,opts){const def={hash:false,comments:false,whitespace:false};opts=typeof opts==="object"&&null!==opts?Object.assign({},def,opts):Object.assign({},def);const typed=typeof func;if(typed!=="function"){throw new SerializeError("Invalid argument type, must be a function",{cause:{"typeof":typed}})}let stringified=func.toString();if(!opts.comments){stringified=removeComments(stringified)}if(!opts.whitespace){stringified=stringified.split(/[\r\n]+/).map(line=>line.trim()).filter(line=>line!=="").join("\n")}let match,serialized;for(const[type,pattern]of Object.entries(formatPatterns)){try{match=stringified.match(pattern);if(match){let async=match.groups.isAsync?"Async":"";let params=type==="ArrowFunction"?match.groups.params??match.groups.singleParam:match.groups.params;params=params.split(",").map(p=>opts.whitespace?p:p.trim()).filter(Boolean);let body=type==="ArrowFunction"?match.groups.bracedBody??`return (${match.groups.bodyExpr});`:match.groups.body;if(!opts.whitespace)body=body.trim();serialized={params,body,type:`${async}${type}`};break}}catch(cause){throw new SerializeError(`Unexpected error serializing ${type}`,{cause})}}if(!serialized){throw new SerializeError("Unsupported function format",{cause:stringified})}if(opts.hash){try{const hashed=await hasher(serialized);serialized.hash=hashed}catch(cause){throw new SerializeError("Failure hashing serialized function",{cause})}}return serialized}async function deserialize(struct){let opts=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{hash:false};if(opts?.hash){if(struct?.hash===undefined){throw new DeserializeError("Deserialized function missing hash")}const test=Object.assign({},struct);delete test.hash;try{const checksum=await hasher(test);if(checksum!==struct.hash){throw new ChecksumError("Checksum failed",{cause:{a:checksum,b:struct.hash}})}}catch(cause){if(cause instanceof ChecksumError)throw cause;throw new DeserializeError("Failure generating checksum",{cause})}}try{const constructor=getConstructor(struct.type);return new constructor(...struct.params,struct.body)}catch(cause){if(cause instanceof ConstructError)throw cause;throw new DeserializeError("Failure deserializing",{cause})}}async function traverse(input,tester,converter){if(tester(input)){return await converter(input)}if(input===null||typeof input!=="object"){return input}const nonTraversables=[String,Boolean,Number,Date,RegExp];for(const constructor of nonTraversables){if(input instanceof constructor){return new constructor(input.valueOf())}}if(input instanceof Array){const cloned=[];for(let i=0;i<input.length;i++){if(tester(input[i]))cloned[i]=await converter(input[i]);else cloned[i]=await traverse(input[i],tester,converter)}return cloned}if(input instanceof Set){const cloned=new Set;for(const value of input){if(tester(value))cloned.add(await converter(value));else cloned.add(await traverse(value,tester,converter))}return cloned}if(input instanceof Map){const cloned=new Map;for(const[key,value]of input){if(tester(value))cloned.set(key,await converter(value));else cloned.set(key,await traverse(value,tester,converter))}return cloned}if(input instanceof Object){const cloned=Object.create(Object.getPrototypeOf(input));for(const key in input){if(Object.hasOwn(input,key)){if(tester(input[key]))cloned[key]=await converter(input[key]);else cloned[key]=await traverse(input[key],tester,converter)}}return cloned}return input}async function deepSerialize(value,options){try{return await traverse(value,input=>typeof input==="function",input=>serialize(input,options))}catch(cause){throw new SerializeError("Failure traversing and serializing",{cause})}}async function deepDeserialize(value,options){try{return await traverse(value,input=>typeof input==="object"&&Object.hasOwn(input,"params")&&Object.hasOwn(input,"body")&&Object.hasOwn(input,"type"),input=>deserialize(input,Object.assign({hash:Object.hasOwn(input,"hash")},options)))}catch(cause){throw new DeserializeError("Failure traversing and deserializing",{cause})}}export{serialize,deserialize,deepSerialize,deepDeserialize,JsonError,CryptoError,SerializeError,DeserializeError,ChecksumError,ConstructError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serialize-function",
3
- "version": "1.2.6",
3
+ "version": "2.0.0",
4
4
  "description": "Serializes javascript functions to a JSON-friendly format",
5
5
  "author": "Evan Kaufman <evan@evanskaufman.com>",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@
36
36
  "build-node": "npm run transpile-node -- --minified --no-comments && cp src/import.mjs dist/",
37
37
  "dist-dev": "npm run build-browser-dev && npm run build-node-dev",
38
38
  "dist": "npm run build-browser && npm run build-node",
39
+ "docs-html": "jsdoc -c ./jsdoc.json ./src/main.mjs",
39
40
  "lint": "eslint .",
40
41
  "prepare-test-browser": "sed '/TEST_SPEC_END/e cat tests/general.spec.js' tests/003-browser.test.html > ./003-browser.test.html",
41
42
  "test-browser": "node ./tests/003-puppeteer.script.mjs",
@@ -52,10 +53,12 @@
52
53
  "@fastify/static": "^9.0.0",
53
54
  "@stylistic/eslint-plugin": "^5.10.0",
54
55
  "chai": "^6.2.2",
56
+ "classy-template": "^1.5.4",
55
57
  "eslint": "^10.0.3",
56
58
  "eslint-plugin-mocha": "^11.2.0",
57
59
  "fastify": "^5.8.2",
58
60
  "globals": "^17.4.0",
61
+ "jsdoc": "^4.0.5",
59
62
  "mocha": "^11.7.5",
60
63
  "proxyquire": "^2.1.3",
61
64
  "puppeteer": "^24.38.0",