serialize-function 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Evan Kaufman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # serialize-function
2
+
3
+ ![ci status](https://github.com/EvanK/npm-serialize-function/actions/workflows/ci.yaml/badge.svg)
4
+ ![node.js supported as of v20](https://img.shields.io/badge/Node.js-v20-green)
5
+ ![ECMAScript standard supported as of ES2023](https://img.shields.io/badge/ES-2023-yellow)
6
+
7
+ ## Quickstart
8
+
9
+ Supports both CommonJS and ES Modules:
10
+
11
+ ```js
12
+ const { serialize, deserialize } = require('serialize-function');
13
+ // or
14
+ import { serialize, deserialize } from 'serialize-function';
15
+ ```
16
+
17
+ Serializes javascript functions to a JSON-encodable object suitable for storage to file or transfer over the wire:
18
+
19
+ ```js
20
+ function doTheThing(a,b,c,d,e) { return a + b * c / d % e; }
21
+
22
+ const obj = serialize(doTheThing);
23
+ console.log(obj);
24
+ // {
25
+ // params: [ 'a', 'b', 'c', 'd', 'e' ],
26
+ // body: 'return a + b * c / d % e;',
27
+ // type: 'Function'
28
+ // }
29
+ ```
30
+
31
+ Deserializes back into an invokable function:
32
+
33
+ ```js
34
+ const func = deserialize(obj);
35
+ console.log( func(1, 2, 3, 4, 5) );
36
+ // 2.5
37
+ ```
38
+
39
+ ## Hashing
40
+
41
+ Optionally supports SHA256 checksum hashing to prevent MITM tampering:
42
+
43
+ ```js
44
+ // note: use of hashing returns a promise
45
+ const hashedObj = await serialize(doTheThing, { hash: true });
46
+ console.log(hashedObj);
47
+ // {
48
+ // params: [ 'a', 'b', 'c', 'd', 'e' ],
49
+ // body: 'return a + b * c / d % e;',
50
+ // type: 'Function',
51
+ // hash: '814fab043d5bcee7a589b1d73a9fb42a2d716c3f615056c41a062478e7844827'
52
+ // }
53
+
54
+ hashedObj.body = 'return doSomethingMalicious(...arguments);';
55
+ const tamperedFunc = await deserialize(hashedObj, { hash: true });
56
+ // ChecksumError: Checksum failed
57
+ ```
58
+
59
+ > Under the hood, the browser-based implementation uses the [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) API, while the node implementation uses the built-in [crypto module](https://nodejs.org/docs/latest-v20.x/api/crypto.html).
60
+
61
+ ## Whitespace and comments
62
+
63
+ 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.
64
+
65
+ You can optionally preserve either or both, with the corresponding options:
66
+
67
+ ```js
68
+ function thingNumberTwo(
69
+ /* marco */
70
+ a, b, c,
71
+ d,e/* polo */
72
+ ) {
73
+ // add some things
74
+ const sum = a + b;
75
+
76
+ /* multiply by another thing */
77
+ const product = sum * c;
78
+
79
+ // divide by _another_
80
+ // different thing
81
+ const quotient = product / d;
82
+ /*
83
+ and modulus THAT thing
84
+ */
85
+ const remainder = quotient % e;
86
+ return remainder;
87
+ }
88
+
89
+ const commentedObj = serialize(thingNumberTwo, { whitespace: true, comments: true });
90
+ console.log(commentedObj);
91
+ // {
92
+ // params: [ '\n /* marco */\n a', ' b', '\tc', '\n d', 'e/* polo */\n' ],
93
+ // body: '\n' +
94
+ // ' // add some things\n' +
95
+ // ' const sum = a + b;\n' +
96
+ // '\n' +
97
+ // ' /* multiply by another thing */\n' +
98
+ // ' const product = sum * c;\n' +
99
+ // '\n' +
100
+ // ' // divide by _another_\n' +
101
+ // ' // different thing\n' +
102
+ // ' const quotient = product / d;\n' +
103
+ // ' /*\n' +
104
+ // ' and modulus THAT thing\n' +
105
+ // ' */\n' +
106
+ // ' const remainder = quotient % e;\n' +
107
+ // ' return remainder;\n',
108
+ // type: 'Function'
109
+ // }
110
+ ```
111
+
112
+ ## Function type support
113
+
114
+ Arrow functions, generators, and all async variants are supported (contingent on _browser support_ where relevant):
115
+
116
+ ```js
117
+ serialize(
118
+ (i,j,k) => ({ i, j, k })
119
+ );
120
+ // {
121
+ // params: [ 'i', 'j', 'k' ],
122
+ // body: 'return (({ i, j, k }));',
123
+ // type: 'ArrowFunction'
124
+ // }
125
+
126
+ serialize(
127
+ function* (x,y,z) {
128
+ yield x;
129
+ yield y;
130
+ yield z;
131
+ }
132
+ );
133
+ // {
134
+ // params: [ 'x', 'y', 'z' ],
135
+ // body: 'yield x;\nyield y;\nyield z;',
136
+ // type: 'Generator'
137
+ // }
138
+
139
+ serialize(
140
+ async (ms) => new Promise(
141
+ resolve => setTimeout(resolve, ms)
142
+ )
143
+ );
144
+ // {
145
+ // params: [ 'ms' ],
146
+ // body: 'return (new Promise(\nresolve => setTimeout(resolve, ms)\n));',
147
+ // type: 'AsyncArrowFunction'
148
+ // }
149
+ ```
@@ -0,0 +1,5 @@
1
+ // ESM support
2
+ import { serialize, deserialize } from './main.js';
3
+
4
+ export { serialize, deserialize }
5
+ export default { serialize, deserialize }
package/dist/main.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.deserialize=deserialize;exports.serialize=serialize;function _interopRequireWildcard(e,t){if("function"==typeof WeakMap)var r=new WeakMap,n=new WeakMap;return(_interopRequireWildcard=function(e,t){if(!t&&e&&e.__esModule)return e;var o,i,f={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return f;if(o=t?n:r){if(o.has(e))return o.get(e);o.set(e,f)}for(const t in e)"default"!==t&&{}.hasOwnProperty.call(e,t)&&((i=(o=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,t))&&(i.get||i.set)?o(f,t,i):f[t]=e[t]);return f})(e,t)}async function digest(input){const{createHash}=await Promise.resolve().then(()=>_interopRequireWildcard(require("node:crypto")));return createHash("sha256").update(input).digest("hex")}class JsonError extends Error{};class CryptoError extends Error{};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{hashed=await digest(json)}catch(cause){throw new CryptoError("Failed to generate hash digest",{cause})}return hashed}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]+))$/};class SerializeError extends Error{}class DeserializeError extends Error{}class ChecksumError extends Error{}class ConstructError extends Error{}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 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=stringified.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*$/gm,"")}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})}}
package/lib/main.js ADDED
@@ -0,0 +1 @@
1
+ async function digest(input){const hashBuffer=await window.crypto.subtle.digest("SHA-256",new TextEncoder().encode(input));return Array.from(new Uint8Array(hashBuffer)).map(item=>item.toString(16).padStart(2,"0")).join("")}class JsonError extends Error{};class CryptoError extends Error{};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{hashed=await digest(json)}catch(cause){throw new CryptoError("Failed to generate hash digest",{cause})}return hashed}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]+))$/};class SerializeError extends Error{}class DeserializeError extends Error{}class ChecksumError extends Error{}class ConstructError extends Error{}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 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=stringified.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/.*$/gm,"")}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){var _match$,_match$2;let async=match[1]?"Async":"";let params=type==="ArrowFunction"?(_match$=match[2])!==null&&_match$!==void 0?_match$:match[3]:match[2];params=params.split(",").map(p=>opts.whitespace?p:p.trim()).filter(Boolean);let body=type==="ArrowFunction"?(_match$2=match[4])!==null&&_match$2!==void 0?_match$2:`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};
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "serialize-function",
3
+ "version": "1.0.0",
4
+ "description": "Serializes javascript functions to a JSON-friendly format",
5
+ "author": "Evan Kaufman <evan@evanskaufman.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/EvanK/npm-serialize-function.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/EvanK/npm-serialize-function/issues"
13
+ },
14
+ "homepage": "https://github.com/EvanK/npm-serialize-function",
15
+ "keywords": [
16
+ "function",
17
+ "serialize",
18
+ "json"
19
+ ],
20
+ "files": [
21
+ "dist/*",
22
+ "lib/*"
23
+ ],
24
+ "exports": {
25
+ "import": "./dist/import.mjs",
26
+ "require": "./dist/main.js"
27
+ },
28
+ "browser": "./lib/main.js",
29
+ "scripts": {
30
+ "concat-browser": "cat src/001-digest-browser.mjs src/002-hasher.mjs src/003-main.mjs > build-main-browser.mjs",
31
+ "concat-node": "cat src/001-digest-node.mjs src/002-hasher.mjs src/003-main.mjs > build-main-node.mjs",
32
+ "transpile-browser": "babel build-main-browser.mjs --no-babelrc --out-file lib/main.js --config-file ./.babelrc.esm.json",
33
+ "transpile-node": "babel build-main-node.mjs --no-babelrc --out-file dist/main.js --config-file ./.babelrc.cjs.json",
34
+ "build-browser-dev": "npm run concat-browser && npm run transpile-browser",
35
+ "build-node-dev": "npm run concat-node && npm run transpile-node && cp src/import.mjs dist/",
36
+ "build-browser": "npm run concat-browser && npm run transpile-browser -- --minified --no-comments",
37
+ "build-node": "npm run concat-node && npm run transpile-node -- --minified --no-comments && cp src/import.mjs dist/",
38
+ "dist-dev": "npm run build-browser-dev && npm run build-node-dev",
39
+ "dist": "npm run build-browser && npm run build-node",
40
+ "lint": "eslint .",
41
+ "prepare-test-browser": "sed '/TEST_SPEC_END/e cat tests/general.spec.js' tests/003-browser.test.html > ./003-browser.test.html",
42
+ "test-browser": "node ./tests/003-puppeteer.script.mjs",
43
+ "prepare-test-node": "cat tests/001-general.test.template.cjs tests/general.spec.js > tests/001-general.ignore.test.cjs",
44
+ "test-node": "mocha ./tests/**.test.*js",
45
+ "test": "npm run prepare-test-browser && npm run test-browser && npm run prepare-test-node && npm run test-node"
46
+ },
47
+ "devDependencies": {
48
+ "@babel/cli": "^7.28.0",
49
+ "@babel/core": "^7.28.0",
50
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
51
+ "@babel/preset-env": "^7.28.0",
52
+ "@eslint/js": "^9.30.1",
53
+ "@fastify/static": "^8.2.0",
54
+ "@stylistic/eslint-plugin": "^5.1.0",
55
+ "chai": "^5.2.0",
56
+ "eslint": "^9.30.1",
57
+ "eslint-plugin-mocha": "^11.1.0",
58
+ "fastify": "^5.4.0",
59
+ "globals": "^16.3.0",
60
+ "mocha": "^11.7.1",
61
+ "proxyquire": "^2.1.3",
62
+ "puppeteer": "^24.14.0",
63
+ "sinon": "^21.0.0"
64
+ }
65
+ }