diff --git a/DOWNLOAD_STATS.md b/DOWNLOAD_STATS.md index b439e33..1535a14 100644 --- a/DOWNLOAD_STATS.md +++ b/DOWNLOAD_STATS.md @@ -55,6 +55,7 @@ | `@nolyfill/is-weakref` | [![npm](https://img.shields.io/npm/dt/@nolyfill/is-weakref.svg?style=flat-square&logo=npm&logoColor=white&label=total%20downloads&color=333)](https://www.npmjs.com/package/@nolyfill/is-weakref) | | `@nolyfill/isarray` | [![npm](https://img.shields.io/npm/dt/@nolyfill/isarray.svg?style=flat-square&logo=npm&logoColor=white&label=total%20downloads&color=333)](https://www.npmjs.com/package/@nolyfill/isarray) | | `@nolyfill/iterator.prototype` | [![npm](https://img.shields.io/npm/dt/@nolyfill/iterator.prototype.svg?style=flat-square&logo=npm&logoColor=white&label=total%20downloads&color=333)](https://www.npmjs.com/package/@nolyfill/iterator.prototype) | +| `@nolyfill/json-stable-stringify` | [![npm](https://img.shields.io/npm/dt/@nolyfill/json-stable-stringify.svg?style=flat-square&logo=npm&logoColor=white&label=total%20downloads&color=333)](https://www.npmjs.com/package/@nolyfill/json-stable-stringify) | | `@nolyfill/jsonify` | [![npm](https://img.shields.io/npm/dt/@nolyfill/jsonify.svg?style=flat-square&logo=npm&logoColor=white&label=total%20downloads&color=333)](https://www.npmjs.com/package/@nolyfill/jsonify) | | `@nolyfill/object-is` | [![npm](https://img.shields.io/npm/dt/@nolyfill/object-is.svg?style=flat-square&logo=npm&logoColor=white&label=total%20downloads&color=333)](https://www.npmjs.com/package/@nolyfill/object-is) | | `@nolyfill/object-keys` | [![npm](https://img.shields.io/npm/dt/@nolyfill/object-keys.svg?style=flat-square&logo=npm&logoColor=white&label=total%20downloads&color=333)](https://www.npmjs.com/package/@nolyfill/object-keys) | diff --git a/create.ts b/create.ts index 27082a4..d4bd155 100644 --- a/create.ts +++ b/create.ts @@ -131,7 +131,8 @@ const singleFilePackagesList = [ ['hasown'], ['jsonify'], ['isarray'], - ['is-typed-array', { '@nolyfill/which-typed-array': 'workspace:*' }] + ['is-typed-array', { '@nolyfill/which-typed-array': 'workspace:*' }], + ['json-stable-stringify'] ] as const; const manualPackagesList = [ diff --git a/package.json b/package.json index 97914fd..2a03c47 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "is-weakref": "workspace:@nolyfill/is-weakref@*", "isarray": "workspace:@nolyfill/isarray@*", "iterator.prototype": "workspace:@nolyfill/iterator.prototype@*", + "json-stable-stringify": "workspace:@nolyfill/json-stable-stringify@*", "jsonify": "workspace:@nolyfill/jsonify@*", "object-is": "workspace:@nolyfill/object-is@*", "object-keys": "workspace:@nolyfill/object-keys@*", @@ -197,6 +198,7 @@ "is-weakref": "npm:@nolyfill/is-weakref@latest", "isarray": "npm:@nolyfill/isarray@latest", "iterator.prototype": "npm:@nolyfill/iterator.prototype@latest", + "json-stable-stringify": "npm:@nolyfill/json-stable-stringify@latest", "jsonify": "npm:@nolyfill/jsonify@latest", "object-is": "npm:@nolyfill/object-is@latest", "object-keys": "npm:@nolyfill/object-keys@latest", diff --git a/packages/data/single-file/src/json-stable-stringify.ts b/packages/data/single-file/src/json-stable-stringify.ts new file mode 100644 index 0000000..6b3143d --- /dev/null +++ b/packages/data/single-file/src/json-stable-stringify.ts @@ -0,0 +1,125 @@ +'use strict'; + +interface Element { + key: string, + value: any +} + +export type Replacer = (key: string, value: any) => any; + +interface ComparatorOption { + get?: (key: any) => any +} +export type Comparator = (a: Element, b: Element, opt?: ComparatorOption) => number; + +const defaultReplacer: Replacer = function (_key: string, value: any) { return value; }; + +export interface Options { + /** + * Custom comparator for key + */ + cmp?: Comparator, + + /** + * Indent the output for pretty-printing. + * + * Supported is either a string or a number of spaces. + */ + space?: string | number, + + /** + * Option to replace values to simpler values + */ + replacer?: Replacer, + + /** + * true to allow cycles, by marking the entries as __cycle__. + */ + cycles?: boolean +} + +export default function stableStringify(obj: any, opts?: Comparator | Options): string { + const space = opts && 'space' in opts && opts.space + ? (typeof opts.space === 'number' + ? ' '.repeat(opts.space) + : opts.space) + : ''; + + const cycles = opts && 'cycles' in opts && typeof opts.cycles === 'boolean' + ? opts.cycles + : false; + + const replacer = opts && 'replacer' in opts && opts.replacer + ? opts.replacer + : defaultReplacer; + + const cmpOpt = typeof opts === 'function' ? opts : opts?.cmp; + + const cmp = cmpOpt + ? ((node: any) => { + const get = cmpOpt.length > 2 && function get(k: any) { return node[k]; }; + const thirdArg: ComparatorOption | undefined = get ? { get } : undefined; + + return (a: string, b: string) => { + const aobj: Element = { key: a, value: node[a] }; + const bobj: Element = { key: b, value: node[b] }; + return cmpOpt(aobj, bobj, thirdArg); + }; + }) + : undefined; + + // Cycle + const seen = new Set(); + + function stringify(parent: any, key: string, node: any, level: number): string { + const indent = space ? `\n${space.repeat(level)}` : ''; + const colonSeparator = space ? ': ' : ':'; + + if (node?.toJSON && typeof node.toJSON === 'function') { + node = node.toJSON(); + } + + node = replacer.call(parent, key, node); + + if (node === undefined) { + // @ts-expect-error -- fuck undefined + return; + } + if (typeof node !== 'object' || node === null) { + return JSON.stringify(node); + } + if (Array.isArray(node)) { + const out = []; + for (let i = 0; i < node.length; i++) { + // @ts-expect-error -- fuck js + const item = stringify(node, i, node[i], level + 1) || JSON.stringify(null); + out.push(indent + space + item); + } + return `[${out.join(',')}${indent}]`; + } + + if (seen.has(node)) { + if (cycles) { return JSON.stringify('__cycle__'); } + throw new TypeError('Converting circular structure to JSON'); + } else { seen.add(node); } + + const keys = Object.keys(node).sort(cmp?.(node)); + const out = []; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = stringify(node, key, node[key], level + 1); + + if (!value) { continue; } + + const keyValue = JSON.stringify(key) + + colonSeparator + + value; + + out.push(indent + space + keyValue); + } + seen.delete(node); + return `{${out.join(',')}${indent}}`; + } + + return stringify({ '': obj }, '', obj, 0); +} diff --git a/packages/generated/json-stable-stringify/index.js b/packages/generated/json-stable-stringify/index.js new file mode 100644 index 0000000..d7bdb4e --- /dev/null +++ b/packages/generated/json-stable-stringify/index.js @@ -0,0 +1,3 @@ +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"default",{enumerable:!0,get:function(){return t}});const e=function(e,t){return t};function t(t,r){let n=r&&"space"in r&&r.space?"number"==typeof r.space?" ".repeat(r.space):r.space:"",l=!!r&&"cycles"in r&&"boolean"==typeof r.cycles&&r.cycles,o=r&&"replacer"in r&&r.replacer?r.replacer:e,i="function"==typeof r?r:null==r?void 0:r.cmp,u=i?e=>{let t=i.length>2&&function(t){return e[t]},r=t?{get:t}:void 0;return(t,n)=>i({key:t,value:e[t]},{key:n,value:e[n]},r)}:void 0,c=new Set;return function e(t,r,i,f){let s=n?` +${n.repeat(f)}`:"",p=n?": ":":";if((null==i?void 0:i.toJSON)&&"function"==typeof i.toJSON&&(i=i.toJSON()),void 0===(i=o.call(t,r,i)))return;if("object"!=typeof i||null===i)return JSON.stringify(i);if(Array.isArray(i)){let t=[];for(let r=0;r=12.4.0" + } +} diff --git a/packages/tools/cli/src/all-packages.ts b/packages/tools/cli/src/all-packages.ts index c74a79a..299e52e 100644 --- a/packages/tools/cli/src/all-packages.ts +++ b/packages/tools/cli/src/all-packages.ts @@ -54,6 +54,7 @@ export const allPackages = [ "is-weakref", "isarray", "iterator.prototype", + "json-stable-stringify", "jsonify", "object-is", "object-keys", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff5f12e..e8096a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,7 @@ overrides: is-weakref: workspace:@nolyfill/is-weakref@* isarray: workspace:@nolyfill/isarray@* iterator.prototype: workspace:@nolyfill/iterator.prototype@* + json-stable-stringify: workspace:@nolyfill/json-stable-stringify@* jsonify: workspace:@nolyfill/jsonify@* object-is: workspace:@nolyfill/object-is@* object-keys: workspace:@nolyfill/object-keys@* @@ -485,6 +486,8 @@ importers: packages/generated/iterator.prototype: {} + packages/generated/json-stable-stringify: {} + packages/generated/jsonify: {} packages/generated/object-is: