diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..a9d545a --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,60 @@ +name: Regenerate documentation + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tagged version from which to generate docs' + required: false + +jobs: + generate-and-commit: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true + ref: main + - uses: actions/setup-node@v6 + with: + node-version: 24.x + cache: 'npm' + + - name: Set target as specified input tag + if: ${{ github.event.inputs.tag }} != '' + run: echo "TAG=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + id: from_input + - name: Set target as latest available tag + if: ${{ github.event.inputs.tag }} == '' + run: echo "TAG=$(git describe --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_OUTPUT + id: from_git + - name: Checkout latest tag + run: git checkout ${{ steps.from_input.outputs.tag }}${{ steps.from_git.outputs.tag }} + + - run: npm ci + + - name: Build html docs + run: npm run docs-html + - name: Checkout docs branch + run: git checkout github-pages + + - name: Remove everything in working dir other than .git and docs directories + run: ls -A -1 --ignore '.git' --ignore 'docs' | xargs rm -rf + + - name: Move html jsdocs to working dir and remove docs dir + run: | + mv -f docs/* ./ + rm -rf docs/ + + - name: Configure, add, commit and push any changes to docs branch + run: | + git config --global user.email "${{ github.actor }}" + git config --global user.name "${{ github.actor }}" + git add . + git commit -m "Docs updated -- $(date)" + git push origin github-pages + continue-on-error: true + \ No newline at end of file diff --git a/.gitignore b/.gitignore index d5f43ff..fcc122c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ build-*.*js lib/ dist/ +# jsdoc stuff +docs/ + # node testing tests/*.ignore.test.cjs diff --git a/README.md b/README.md index 5735d80..eab31c5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ ![npm](https://nodei.co/npm/serialize-function.png) ](https://www.npmjs.com/package/serialize-function) +--- + +- [Quickstart](#quickstart) +- [Deep serialization](#deep-serialization) +- [Hashing](#hashing) +- [Whitespace and comments](#whitespace-and-comments) +- [Function type support](#function-type-support) +- [Changelog](#changelog) + ## Quickstart @@ -30,7 +39,7 @@ Serializes javascript functions to a JSON-encodable object suitable for storage ```js function doTheThing(a,b,c,d,e) { return a + b * c / d % e; } -const obj = serialize(doTheThing); +const obj = await serialize(doTheThing); console.log(obj); // { // params: [ 'a', 'b', 'c', 'd', 'e' ], @@ -42,17 +51,70 @@ console.log(obj); Deserializes back into an invokable function: ```js -const func = deserialize(obj); +const func = await deserialize(obj); console.log( func(1, 2, 3, 4, 5) ); // 2.5 ``` + +## Deep serialization + +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: + +```js +const { deepSerialize, deepDeserialize } = require('serialize-function'); + +const original = { + foo: () => 'something', + bar: [ + function* (seed = 0) { let n = seed; while(true) { n = n * 2; yield n; } } + ], + baz: new Date('2026-01-01') +} + +const clone = await deepSerialize(original); +// { +// foo: { +// params: [], +// body: "return ('something');", +// type: 'ArrowFunction' +// }, +// bar: [ +// { +// params: [ 'seed = 0' ], +// body: 'let n = seed; while(true) { n = n * 2; yield n; }', +// type: 'Generator' +// } +// ], +// baz: 2026-01-01T00:00:00.000Z +// } + +// original container and functions remain unmodified +original.foo(); // 'something' +const gen1 = original.bar[0](3.14); +gen1.next().value; // 6.28 +gen1.next().value; // 12.56 + +const restored = await deepDeserialize(clone); +// { +// foo: [Function: anonymous], +// bar: [ [GeneratorFunction: anonymous] ], +// baz: 2026-01-01T00:00:00.000Z +// } + +// deserialized functions remain invokable +restored.foo(); // 'something' +const gen2 = restored.bar[0](901364); +gen2.next().value; // 1802728 +gen2.next().value; // 3605456 +``` + + ## Hashing Optionally supports SHA256 checksum hashing to prevent MITM tampering: ```js -// note: use of hashing returns a promise const hashedObj = await serialize(doTheThing, { hash: true }); console.log(hashedObj); // { @@ -69,6 +131,7 @@ const tamperedFunc = await deserialize(hashedObj, { hash: true }); > Under the hood, hashing uses the [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) API. + ## Whitespace and comments 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( return remainder; } -const commentedObj = serialize(thingNumberTwo, { whitespace: true, comments: true }); +const commentedObj = await serialize(thingNumberTwo, { whitespace: true, comments: true }); console.log(commentedObj); // { // params: [ '\n /* marco */\n a', ' b', '\tc', '\n d', 'e/* polo */\n' ], @@ -120,12 +183,13 @@ console.log(commentedObj); // } ``` + ## Function type support Arrow functions, generators, and all async variants are supported (contingent on _browser support_ where relevant): ```js -serialize( +await serialize( (i,j,k) => ({ i, j, k }) ); // { @@ -134,7 +198,7 @@ serialize( // type: 'ArrowFunction' // } -serialize( +await serialize( function* (x,y,z) { yield x; yield y; @@ -147,7 +211,7 @@ serialize( // type: 'Generator' // } -serialize( +await serialize( async (ms) => new Promise( resolve => setTimeout(resolve, ms) ) @@ -158,3 +222,22 @@ serialize( // type: 'AsyncArrowFunction' // } ``` + +> [!NOTE] +> 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). +> +> As such, ES6 classes are _not_ currently supported for serialization. +> +> Alternatively, you can rewrite your classes as functions, or transpile them with tools like [Babel](https://babeljs.io/docs/babel-plugin-transform-classes/). + + +## Changelog + +Any potentially breaking changes will be documented here. + +- 1.1.0 - Standardized both node and web builds on SubtleCrypto API +- 1.2.0 - Refactored comment stripping, to address potential regex DOS +- 2.0.0 + - Made all exported functions fully async + - Implemented named captures for format patterns + - Implemented deep de/serialization diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..e12df67 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,25 @@ +{ + "markdown": { + "idInHeadings": true + }, + "opts": { + "template": "classy-template", + "destination": "./docs", + "package": "./package.json", + "readme": "./README.md" + }, + "plugins": [ + "classy-template/plugin", + "plugins/markdown" + ], + "source": { + "includePattern": "main.mjs$" + }, + "templates": { + "classy": { + "outputSourceFiles": false, + "showGitLink": true, + "showVersion": true + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6c1c285..63b8106 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serialize-function", - "version": "1.2.5", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serialize-function", - "version": "1.2.5", + "version": "2.0.0", "license": "MIT", "devDependencies": { "@babel/cli": "^7.28.6", @@ -17,16 +17,39 @@ "@fastify/static": "^9.0.0", "@stylistic/eslint-plugin": "^5.10.0", "chai": "^6.2.2", + "classy-template": "^1.5.4", "eslint": "^10.0.3", "eslint-plugin-mocha": "^11.2.0", "fastify": "^5.8.2", "globals": "^17.4.0", + "jsdoc": "^4.0.5", "mocha": "^11.7.5", "proxyquire": "^2.1.3", "puppeteer": "^24.38.0", "sinon": "^21.0.2" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/cli": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.29.7.tgz", @@ -1571,6 +1594,121 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -2129,6 +2267,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz", + "integrity": "sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.18.1" + }, + "engines": { + "node": ">=v12.0.0" + } + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -2290,6 +2441,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", @@ -2500,6 +2676,13 @@ "node": ">=4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -2728,6 +2911,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", @@ -2806,6 +2996,20 @@ "node": "*" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2850,6 +3054,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2930,6 +3147,19 @@ "devtools-protocol": "*" } }, + "node_modules/classy-template": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/classy-template/-/classy-template-1.5.4.tgz", + "integrity": "sha512-TVwT8+1gqKvGWiBq/3eB9/T5cR9RMrYkoT7dFypPigaZVWx+Bu9TqttYG9xsFfLAEq24KMtQrCaR8prck3mxBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsdom": "^25.0.1" + }, + "peerDependencies": { + "jsdoc": "^4.0.2" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2965,6 +3195,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -3073,6 +3316,27 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -3083,6 +3347,20 @@ "node": ">= 14" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3114,6 +3392,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3136,6 +3421,16 @@ "node": ">= 14" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3173,6 +3468,21 @@ "node": ">=0.3.1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3204,6 +3514,19 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3224,6 +3547,55 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3860,6 +4232,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -3919,26 +4308,65 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { - "pump": "^3.0.0" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "dev": true, "license": "MIT", "dependencies": { @@ -4030,6 +4458,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4040,10 +4488,39 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -4063,6 +4540,19 @@ "he": "bin/he" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -4112,6 +4602,19 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4299,6 +4802,13 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -4365,6 +4875,97 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4449,6 +5050,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4509,6 +5120,26 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4525,6 +5156,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -4583,6 +5221,75 @@ "semver": "bin/semver" } }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -4606,6 +5313,29 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -4639,6 +5369,19 @@ "dev": true, "license": "MIT" }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mocha": { "version": "11.8.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", @@ -4834,6 +5577,13 @@ "node": ">=0.10.0" } }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -4977,6 +5727,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5225,6 +6001,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/puppeteer": { "version": "24.39.1", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.39.1.tgz", @@ -5399,6 +6185,16 @@ "node": ">=0.10.0" } }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -5458,6 +6254,13 @@ "dev": true, "license": "MIT" }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5509,6 +6312,26 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -5841,6 +6664,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tar-fs": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", @@ -5902,6 +6732,26 @@ "node": ">=20" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5936,6 +6786,32 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5973,6 +6849,20 @@ "dev": true, "license": "MIT" }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -6066,6 +6956,19 @@ "punycode": "^2.1.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webdriver-bidi-protocol": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", @@ -6073,6 +6976,54 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6172,6 +7123,30 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3d28d74..0f13dce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serialize-function", - "version": "1.2.5", + "version": "2.0.0", "description": "Serializes javascript functions to a JSON-friendly format", "author": "Evan Kaufman ", "license": "MIT", @@ -36,6 +36,7 @@ "build-node": "npm run transpile-node -- --minified --no-comments && cp src/import.mjs dist/", "dist-dev": "npm run build-browser-dev && npm run build-node-dev", "dist": "npm run build-browser && npm run build-node", + "docs-html": "jsdoc -c ./jsdoc.json ./src/main.mjs", "lint": "eslint .", "prepare-test-browser": "sed '/TEST_SPEC_END/e cat tests/general.spec.js' tests/003-browser.test.html > ./003-browser.test.html", "test-browser": "node ./tests/003-puppeteer.script.mjs", @@ -52,10 +53,12 @@ "@fastify/static": "^9.0.0", "@stylistic/eslint-plugin": "^5.10.0", "chai": "^6.2.2", + "classy-template": "^1.5.4", "eslint": "^10.0.3", "eslint-plugin-mocha": "^11.2.0", "fastify": "^5.8.2", "globals": "^17.4.0", + "jsdoc": "^4.0.5", "mocha": "^11.7.5", "proxyquire": "^2.1.3", "puppeteer": "^24.38.0", diff --git a/src/import.mjs b/src/import.mjs index 5b2e760..22e13fa 100644 --- a/src/import.mjs +++ b/src/import.mjs @@ -1,5 +1,4 @@ // ESM support -import { serialize, deserialize } from './main.js'; - -export { serialize, deserialize } -export default { serialize, deserialize } +import * as everything from './main.js'; +export default everything; +export * from './main.js'; diff --git a/src/main.mjs b/src/main.mjs index aa22698..eeb414f 100644 --- a/src/main.mjs +++ b/src/main.mjs @@ -1,29 +1,52 @@ -// an error for each purpose, and a purpose for each error +/** + * Covers failures stringifying serialized function for hashing purposes + * @extends {Error} + */ class JsonError extends Error {}; +/** + * Covers failures generating SHA hash digest + * @extends {Error} + */ class CryptoError extends Error {}; -class SerializeError extends Error {} -class DeserializeError extends Error {} -class ChecksumError extends Error {} -class ConstructError extends Error {} +/** + * Covers general failures during serialization + * @extends {Error} + */ +class SerializeError extends Error {}; +/** + * Covers general failures during deserialization + * @extends {Error} + */ +class DeserializeError extends Error {}; +/** + * Covers failures matching checksum to SHA hash + * @extends {Error} + */ +class ChecksumError extends Error {}; +/** + * Covers failures reconstructing a function during deserialization + * @extends {Error} + */ +class ConstructError extends Error {}; // function constructors (vanilla `Function` already global) const AsyncFunction = async function () {}.constructor; const Generator = function* () {}.constructor; const AsyncGenerator = async function* () {}.constructor; -// would love to simplify w/ named captures, but javascript support is spotty +// matching stringified functions, by respective types, into named capture groups const formatPatterns = { - 'Generator': /^(async\s+)?function\*\s*[^()]*\(([^)]*)\)\s*{([\s\S]*)}$/, - 'Function': /^(async\s+)?function\s*[^()]*\(([^)]*)\)\s*{([\s\S]*)}$/, - // .1 is async - // .2 is param list - // .3 is braced body - 'ArrowFunction': /^(async\s+)?(?:\(([^)]*)\)|([^=\s(]+))\s*=>\s*(?:{([\s\S]*)}|([\s\S]+))$/, - // .1 is "async " | undefined - // .2 is param list | undefined - // .3 is single param | undefined - // .4 is braced body | undefined - // .5 is body expression | undefined + 'Generator': /^(?async\s+)?function\*\s*[^()]*\((?[^)]*)\)\s*{(?[\s\S]*)}$/, + 'Function': /^(?async\s+)?function\s*[^()]*\((?[^)]*)\)\s*{(?[\s\S]*)}$/, + // 1st group is async + // 2nd group is param list + // 3rd group is braced body + 'ArrowFunction': /^(?async\s+)?(?:\((?[^)]*)\)|(?[^=\s(]+))\s*=>\s*(?:{(?[\s\S]*)}|(?[\s\S]+))$/, + // 1st group is async | undefined + // 2nd group is param list | undefined + // 3rd group is single param | undefined + // 4th group is braced body | undefined + // 5th group is body expression | undefined }; // get a sha hash given an object @@ -84,7 +107,6 @@ function removeComments(input) { regex: false, blockComment: false, lineComment: false, - condComp: false }; // work character by character @@ -120,19 +142,10 @@ function removeComments(input) { 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; @@ -151,7 +164,47 @@ function removeComments(input) { return output.join('').slice(2, -2); } -function serialize(func, opts) { +/** + * Invokable function object + * + * @typedef {Function|Generator|AsyncGenerator} InvokableFunction + */ + +/** + * Object notation for serialized functions + * + * @typedef {object} SerializedFunction + * @property {array} params Function parameters + * @property {string} body Function body + * @property {string} type Function type + * @property {string?} hash Cryptographic hash + */ + +/** + * Options for function serialization + * + * @typedef {object} SerializeOptions + * @property {boolean} [comments=false] Preserves comments + * @property {boolean} [whitespace=false] Preserves whitespace + * @property {boolean} [hash=false] Enables SHA256 hashing of function being serialized + */ + +/** + * Options for function deserialization + * + * @typedef {object} DeserializeOptions + * @property {boolean} [hash=false] Enables SHA256 validating of serialized function's hash + */ + +/** + * Serializes a given function to an object notation + * + * @param {InvokableFunction} func Function to be serialized + * @param {SerializeOptions?} opts Serialization options + * @returns {Promise} + * @throws {SerializeError} + */ +async function serialize(func, opts) { const def = { hash: false, comments: false, whitespace: false }; opts = (typeof opts === 'object' && null !== opts) ? Object.assign({}, def, opts) @@ -190,18 +243,18 @@ function serialize(func, opts) { match = stringified.match(pattern); if (match) { // is async? - let async = match[1] ? 'Async' : ''; + let async = match.groups.isAsync ? 'Async' : ''; // params as string list let params = type === 'ArrowFunction' - ? match[2] ?? match[3] - : match[2] + ? match.groups.params ?? match.groups.singleParam + : match.groups.params ; // normalized into an array params = params.split(',').map((p) => opts.whitespace ? p : p.trim()).filter(Boolean); // body as string let body = type === 'ArrowFunction' - ? match[4] ?? `return (${match[5]});` - : match[3] + ? match.groups.bracedBody ?? `return (${match.groups.bodyExpr});` + : match.groups.body ; // trimmed of extra whitespace if (!opts.whitespace) body = body.trim(); @@ -224,46 +277,47 @@ function serialize(func, opts) { } if (opts.hash) { - return hasher(serialized) - .then(hashed => { - serialized.hash = hashed; - return serialized; - }) - .catch(cause => { - throw new SerializeError('Failure hashing serialized function', { cause }); - }) - ; + try { + const hashed = await hasher(serialized); + serialized.hash = hashed; + } catch (cause) { + throw new SerializeError('Failure hashing serialized function', { cause }); + } } return serialized; } -function deserialize(struct, opts = { hash: false }) { +/** + * Deserializes a given object to an invokable function + * + * @param {SerializedFunction} struct Function to be deserialized + * @param {DeserializeOptions?} opts Deserialization options + * @returns {Promise} + * @throws {DeserializeError|ChecksumError} + */ +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; - 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 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 { @@ -275,7 +329,148 @@ function deserialize(struct, opts = { hash: false }) { } } +/** + * Traversing deep structures, to: + * 1. clone every non-primitive type + * 2. test each value for potential conversion (function to object, vice versa) + * 3. convert each object that passes test + * 4. return cloned and/or converted structure + * + * @param {*} input Value to be deeply traversed + * @param {function} tester Callback to test each value for conversion + * @param {function} converter Callback to convert value + * @returns {Promise<*>} Cloned value with conversions made + * @ignore + */ +async function traverse(input, tester, converter) { + // first step any time through is to test and convert + if (tester(input)) { + return await converter(input); + } + + // return null or primitive types + if (input === null || typeof input !== 'object') { + return input; + } + + // clone and return date objects + if (input instanceof Date) { + return new Date(input); + } + + // iterate arrays + if (input instanceof Array) { + const cloned = []; + for (let i = 0; i < input.length; i++) { + // test and convert element + if (tester(input[i])) cloned[i] = await converter(input[i]); + // or traverse and (maybe) copy it + else cloned[i] = await traverse(input[i], tester, converter); + } + return cloned; + } + + // iterate Sets + if (input instanceof Set) { + const cloned = new Set(); + for (const value of input) { + // test and convert each iterated value + if (tester(value)) cloned.add(await converter(value)); + // or traverse and (maybe) copy it + else cloned.add(await traverse(value, tester, converter)); + } + return cloned; + } + + // iterate Maps and use .get/.set + if (input instanceof Map) { + const cloned = new Map(); + for (const [key, value] of input) { + // test and convert each iterated value + if (tester(value)) cloned.set(key, await converter(value)); + // or traverse and (maybe) copy it + else cloned.set(key, await traverse(value, tester, converter)); + } + return cloned; + } + + // iterate objects + if (input instanceof Object) { + const cloned = Object.create(Object.getPrototypeOf(input)); + for (const key in input) { + // skip inherited props + if (Object.hasOwn(input, key)) { + // test and convert each property + if (tester(input[key])) cloned[key] = await converter(input[key]); + // or traverse and (maybe) copy it + else cloned[key] = await traverse(input[key], tester, converter); + } + } + return cloned; + } + + // return unmodified anything unanticipated + return input; +} + +/** + * Accepts and traverses an input value of arbitrary depth, returning a copy with any + * nested functions serialized in the process + * + * @param {*} value Structure to deeply serialize + * @param {SerializeOptions?} options Serialization options + * @returns {Promise<*>} + * @throws {SerializeError} + */ +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 }); + } +} + +/** + * Accepts and traverses an input value of arbitrary depth, returning a copy with any + * nested serialized functions deserialized in the process + * + * @param {*} value Structure to deeply deserialize + * @param {DeserializeOptions} options Deserialization options + * @returns {Promise<*>} + * @throws {DeserializeError} + */ +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, }; diff --git a/tests/001-general.test.template.cjs b/tests/001-general.test.template.cjs index 80dcec9..ebde5e0 100644 --- a/tests/001-general.test.template.cjs +++ b/tests/001-general.test.template.cjs @@ -7,7 +7,11 @@ chaiConfig.truncateThreshold = 0; // prepare for stubbing/calling through to node:crypto const cryptoStub = sinon.stub(); // proxy the test subjects and use pre-proxied hasher -const { serialize, deserialize } = proxyquire('../dist/main.js', { +const { + serialize, deserialize, + deepSerialize, deepDeserialize, + SerializeError, DeserializeError, CryptoError, +} = proxyquire('../dist/main.js', { 'node:crypto': { createHash: cryptoStub }, diff --git a/tests/002-esm.test.mjs b/tests/002-esm.test.mjs index 7248e9f..dea22a8 100644 --- a/tests/002-esm.test.mjs +++ b/tests/002-esm.test.mjs @@ -4,10 +4,19 @@ chaiConfig.truncateThreshold = 0; describe('002A - esm imports', function () { it('import named', async function () { - const { serialize, deserialize } = await import('../dist/import.mjs'); + const { + serialize, deserialize, + deepSerialize, deepDeserialize, + SerializeError, DeserializeError, CryptoError, + } = await import('../dist/import.mjs'); assert.isFunction(serialize); assert.isFunction(deserialize); + + assert.isFunction(deepSerialize); + assert.isFunction(deepDeserialize); + + assert.instanceOf(SerializeError.prototype, Error); }); it('import default', async function () { @@ -17,5 +26,13 @@ describe('002A - esm imports', function () { assert.isFunction(importedDefault.serialize); assert.property(importedDefault, 'deserialize'); assert.isFunction(importedDefault.deserialize); + + assert.property(importedDefault, 'deepSerialize'); + assert.isFunction(importedDefault.deepSerialize); + assert.property(importedDefault, 'deepDeserialize'); + assert.isFunction(importedDefault.deepDeserialize); + + assert.property(importedDefault, 'SerializeError'); + assert.instanceOf(importedDefault.SerializeError.prototype, Error); }); }); diff --git a/tests/003-browser.test.html b/tests/003-browser.test.html index d2186d6..f635f94 100644 --- a/tests/003-browser.test.html +++ b/tests/003-browser.test.html @@ -15,7 +15,11 @@