@stackline/stable-stringify 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +10 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.min.js +1 -1
- package/docs/ADOPTION.md +37 -0
- package/docs/ARCHITECTURE.md +103 -0
- package/docs/BENCHMARKS.md +25 -0
- package/docs/COMPATIBILITY.md +79 -0
- package/docs/MARKET_RESEARCH.md +100 -0
- package/docs/RELEASING.md +68 -0
- package/examples/cache-key.mjs +12 -0
- package/examples/content-digest.mjs +17 -0
- package/examples/safe-logging.mjs +14 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes are documented here. This project follows Semantic
|
|
4
4
|
Versioning.
|
|
5
5
|
|
|
6
|
+
## [1.0.2] - 2026-08-21
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Executable examples for deterministic cache keys, RFC 8785 content digests,
|
|
11
|
+
and bounded cyclic logging.
|
|
12
|
+
- Adoption guidance for direct installs, npm aliases, safe diagnostics, and
|
|
13
|
+
canonical signatures.
|
|
14
|
+
- Reproducible benchmark methodology and Stackline package catalog link.
|
|
15
|
+
- First-party documentation analytics that never records serialized input.
|
|
16
|
+
- Trusted-publishing workflow for provenance-enabled future releases.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Package tarballs now include the public guides and executable examples.
|
|
21
|
+
|
|
22
|
+
No runtime API or declaration behavior changed in this release.
|
|
23
|
+
|
|
6
24
|
## [1.0.1] - 2026-08-16
|
|
7
25
|
|
|
8
26
|
### Fixed
|
|
@@ -32,3 +50,4 @@ Versioning.
|
|
|
32
50
|
|
|
33
51
|
[1.0.1]: https://github.com/alexandroit/stackline-stable-stringify/compare/v1.0.0...v1.0.1
|
|
34
52
|
[1.0.0]: https://github.com/alexandroit/stackline-stable-stringify/releases/tag/v1.0.0
|
|
53
|
+
[1.0.2]: https://github.com/alexandroit/stackline-stable-stringify/compare/v1.0.1...v1.0.2
|
package/README.md
CHANGED
|
@@ -264,6 +264,16 @@ on the same payload. Native JSON is shown only as a throughput baseline; it
|
|
|
264
264
|
does not recursively sort object keys. Measure with representative production
|
|
265
265
|
data before selecting limits or modes.
|
|
266
266
|
|
|
267
|
+
## Adoption resources
|
|
268
|
+
|
|
269
|
+
- [Stable, safe, canonical, and drop-in adoption guide](docs/ADOPTION.md)
|
|
270
|
+
- [Reproducible benchmark methodology](docs/BENCHMARKS.md)
|
|
271
|
+
- [Executable examples](examples)
|
|
272
|
+
- [Stackline open-source catalog](https://alexandro.net/docs/open-source/)
|
|
273
|
+
|
|
274
|
+
The examples ship in the npm tarball and cover deterministic cache keys,
|
|
275
|
+
canonical content digests, and bounded logging of cyclic BigInt data.
|
|
276
|
+
|
|
267
277
|
## Verification
|
|
268
278
|
|
|
269
279
|
Every release runs:
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/index.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @stackline/stable-stringify v1.0.
|
|
1
|
+
/*! @stackline/stable-stringify v1.0.2 | MIT */
|
|
2
2
|
var StacklineStableStringifyModule=(()=>{var b=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var F=Object.prototype.hasOwnProperty;var V=(e,n)=>{for(var t in n)b(e,t,{get:n[t],enumerable:!0})},_=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of P(n))!F.call(e,o)&&o!==t&&b(e,o,{get:()=>n[o],enumerable:!(r=k(n,o))||r.enumerable});return e};var R=e=>_(b({},"__esModule",{value:!0}),e);var ue={};V(ue,{CanonicalizationError:()=>s,StableStringifyLimitError:()=>f,canonicalize:()=>N,canonicalizeBytes:()=>I,configure:()=>C,default:()=>se,safeStringify:()=>E,stableStringify:()=>u,stringify:()=>M});var U=Object.prototype.hasOwnProperty,B=Object.prototype.propertyIsEnumerable,v=Object.prototype.toString,m=Symbol("omit"),x=null,w=Object.freeze({bigint:"string",cycleValue:"[Circular]",maxDepth:100,maxEntries:1e5,maxLength:1e6,onCycle:"marker"}),S=Object.freeze({maxDepth:1e3,maxEntries:1e5,maxLength:16*1024*1024}),f=class extends RangeError{constructor(n,t,r){let o=h(r);super(`Stable stringify ${n} limit of ${t} exceeded at ${o}`),this.name="StableStringifyLimitError",this.code="ERR_STABLE_STRINGIFY_LIMIT",this.kind=n,this.limit=t,this.path=o}},s=class extends TypeError{constructor(n,t){let r=h(t);super(`JSON canonicalization failed at ${r}: ${n}`),this.name="CanonicalizationError",this.code="ERR_JSON_CANONICALIZATION",this.path=r,this.reason=n}};function u(e,n){let t=$(n);return A(e,t,!1)}var M=u;function C(e){let n=typeof e=="function"?{cmp:e}:y(e);return $(n),function(r,o){let i=typeof o=="function"?{cmp:o}:y(o);return u(r,{...n,...i})}}function E(e,n,t,r){let o=y(r),i={...w,...o,maxDepth:o.maxDepth===void 0?o.depthLimit:o.maxDepth,maxEntries:o.maxEntries===void 0?o.edgesLimit:o.maxEntries,replacer:n===void 0?o.replacer:n,space:t===void 0?o.space:t};i.maxDepth===void 0&&(i.maxDepth=w.maxDepth),i.maxEntries===void 0&&(i.maxEntries=w.maxEntries);try{return u(e,i)}catch(c){if(o.throwOnError===!0)throw c;let l="UnknownError";try{c&&typeof c.name=="string"&&(l=c.name.slice(0,80))}catch(ae){}return JSON.stringify(`[Unable to serialize: ${l}]`)}}function N(e,n){let t=re(n);return A(e,t,!0)}function I(e,n){return ce(N(e,n))}function A(e,n,t){let r={ancestors:new WeakMap,canonical:t,chunks:[],entries:0,length:0,options:n},i=O(e,{"":e},"",x,r);if(i===m)return;let c=[{depth:0,path:x,prepared:i,type:"value"}];for(;c.length>0;){let l=c.pop();l.type==="value"?Z(l,c,r):G(l,c,r)}return r.chunks.join("")}function Z(e,n,t){if(e.prepared.kind==="primitive"){a(t,e.prepared.text,e.path);return}let r=e.prepared.value,o=t.ancestors.get(r);if(o!==void 0||t.ancestors.has(r)){ne(t,o,e.path);return}te(t,e.depth,e.path);let i=Array.isArray(r);t.canonical&&(ee(r,e.path),i||X(r,e.path)),t.ancestors.set(r,e.path);let c=W(r,i,e.depth,e.path,t);if(a(t,i?"[":"{",e.path),c.total===0){a(t,i?"]":"}",e.path),t.ancestors.delete(r);return}n.push(c)}function G(e,n,t){if(e.index>=e.total){e.emitted&&t.options.gap!==""&&a(t,`
|
|
3
3
|
${e.indent}`,e.path),a(t,e.isArray?"]":"}",e.path),t.ancestors.delete(e.value);return}if(e.isArray){let r=e.index;e.index+=1;let o=g(e.path,r);L(t,o);let i=q(e.value,r,o,t),c=O(i,e.value,String(r),o,t);c===m&&(c=p("null")),J(e,o,t),n.push(e),n.push({depth:e.depth+1,path:o,prepared:c,type:"value"});return}for(;e.index<e.total;){let r=e.keys[e.index];e.index+=1;let o=g(e.path,r);L(t,o);let i=Q(e.value,r,o,t),c=O(i,e.value,r,o,t);if(c!==m){J(e,o,t),a(t,JSON.stringify(r),o),a(t,t.options.gap===""?":":": ",o),n.push(e),n.push({depth:e.depth+1,path:o,prepared:c,type:"value"});return}}n.push(e)}function W(e,n,t,r,o){let i,c;return n?c=e.length:(i=H(e,r,o),c=i.length),{depth:t,emitted:!1,indent:o.options.gap===""?"":o.options.gap.repeat(t),index:0,isArray:n,keys:i,path:r,total:c,type:"container",value:e}}function H(e,n,t){let r;if(t.canonical||t.options.propertyList===void 0?r=Object.keys(e):r=t.options.propertyList.slice(),t.canonical){for(let o of r)if(z(o))throw new s("property names must not contain lone UTF-16 surrogates",g(n,o));return r.sort()}if(t.options.accessors!=="invoke"&&(r=r.filter(o=>{let i=Object.getOwnPropertyDescriptor(e,o);if(!i||!i.get&&!i.set)return!0;if(t.options.accessors==="throw")throw new TypeError(`Refusing to invoke accessor at ${h(g(n,o))}`);return!1})),t.options.cmp){let o=t.options.cmp;r.sort((i,c)=>o({key:i,value:T(e,i,n,t)},{key:c,value:T(e,c,n,t)}))}else r.sort();return r}function O(e,n,t,r,o){if(o.canonical)return K(e,r);let i=e;if(o.options.toJSON&&i!==null&&i!==void 0){let c=i.toJSON;typeof c=="function"&&(i=c.call(i))}if(o.options.replacerFunction&&(i=o.options.replacerFunction.call(n,t,i)),i===null)return p("null");switch(typeof i){case"string":return p(JSON.stringify(i));case"number":return p(Number.isFinite(i)?String(i):"null");case"boolean":return p(i?"true":"false");case"bigint":return Y(i,o.options.bigint,r);case"object":return{kind:"object",value:i};default:return m}}function K(e,n){if(e===null)return p("null");switch(typeof e){case"string":if(z(e))throw new s("strings must not contain lone UTF-16 surrogates",n);return p(JSON.stringify(e));case"number":if(!Number.isFinite(e))throw new s("NaN and Infinity are not valid I-JSON numbers",n);return p(JSON.stringify(e));case"boolean":return p(e?"true":"false");case"object":return{kind:"object",value:e};case"bigint":throw new s("BigInt values must be represented as JSON strings",n);default:throw new s(`values of type ${typeof e} are not valid JSON data`,n)}}function Y(e,n,t){if(n==="string")return p(JSON.stringify(String(e)));if(n==="number"){let r=Number(e);if(!Number.isSafeInteger(r))throw new RangeError(`BigInt at ${h(t)} cannot be represented as a safe JSON number`);return p(String(r))}throw new TypeError("Do not know how to serialize a BigInt")}function p(e){return{kind:"primitive",text:e}}function q(e,n,t,r){if(r.canonical){if(!U.call(e,n))throw new s("sparse arrays are not valid I-JSON data",t);return D(e,String(n),t)}return j(e,String(n),t,r.options.accessors)}function Q(e,n,t,r){return r.canonical?D(e,n,t):j(e,n,t,r.options.accessors)}function D(e,n,t){let r=Object.getOwnPropertyDescriptor(e,n);if(!r||r.get||r.set)throw new s("accessor properties are not valid canonical JSON input",t);return r.value}function j(e,n,t,r){if(r==="invoke")return e[n];let o=new WeakSet,i=e;for(;i!==null&&!o.has(i);){o.add(i);let c=Object.getOwnPropertyDescriptor(i,n);if(c){if(!c.get&&!c.set)return c.value;if(r==="throw")throw new TypeError(`Refusing to invoke accessor at ${h(t)}`);return}i=Object.getPrototypeOf(i)}}function T(e,n,t,r){return j(e,n,g(t,n),r.options.accessors)}function X(e,n){let t=Object.getPrototypeOf(e);if(t!==null&&Object.getPrototypeOf(t)!==null)throw new s("class instances must be converted to plain JSON objects",n)}function ee(e,n){if(typeof Object.getOwnPropertySymbols!="function")return;if(Object.getOwnPropertySymbols(e).some(r=>B.call(e,r)))throw new s("symbol properties are not valid JSON object members",n)}function J(e,n,t){if(t.options.gap==="")e.emitted&&a(t,",",n);else{let r=t.options.gap.repeat(e.depth+1);a(t,e.emitted?`,
|
|
4
4
|
${r}`:`
|
package/docs/ADOPTION.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Adoption Guide
|
|
2
|
+
|
|
3
|
+
## Stable cache keys
|
|
4
|
+
|
|
5
|
+
Use the default export when object insertion order must not change a cache key,
|
|
6
|
+
snapshot, deduplication identifier, or content hash.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import stringify from '@stackline/stable-stringify';
|
|
10
|
+
|
|
11
|
+
const key = stringify({ tenant: 42, query: { status: 'open' } });
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Drop-in package alias
|
|
15
|
+
|
|
16
|
+
Applications already using `fast-json-stable-stringify` can preserve their
|
|
17
|
+
imports:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm uninstall fast-json-stable-stringify
|
|
21
|
+
npm install fast-json-stable-stringify@npm:@stackline/stable-stringify
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Commit the lockfile and run the complete application suite. The default call
|
|
25
|
+
shape and comparator contract are covered by differential compatibility tests.
|
|
26
|
+
|
|
27
|
+
## Safe diagnostics
|
|
28
|
+
|
|
29
|
+
Use `safeStringify` for logs and error paths that can contain cycles, BigInt,
|
|
30
|
+
getters, or deeply nested data. Its bounded defaults keep diagnostic output
|
|
31
|
+
from becoming another failure source.
|
|
32
|
+
|
|
33
|
+
## Cryptographic canonicalization
|
|
34
|
+
|
|
35
|
+
Use `canonicalize` or `canonicalizeBytes` only when strict RFC 8785 behavior is
|
|
36
|
+
required. Canonical mode intentionally rejects values outside its I-JSON
|
|
37
|
+
contract instead of silently applying safe-logging policies.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
The runtime converts a JavaScript value into deterministic JSON text through
|
|
6
|
+
three policy surfaces:
|
|
7
|
+
|
|
8
|
+
- stable mode for compatibility and cache or snapshot keys;
|
|
9
|
+
- safe mode for bounded diagnostics and logs;
|
|
10
|
+
- canonical mode for RFC 8785 interoperable bytes.
|
|
11
|
+
|
|
12
|
+
All modes share one iterative serialization engine and have zero runtime
|
|
13
|
+
dependencies.
|
|
14
|
+
|
|
15
|
+
## Iterative task stack
|
|
16
|
+
|
|
17
|
+
Recursive serializers fail when object depth exceeds the JavaScript call-stack
|
|
18
|
+
limit. This implementation prepares a root value and processes explicit
|
|
19
|
+
`value` and `container` tasks in a loop. A container frame stores its key list,
|
|
20
|
+
index, depth, indentation, path, and emission state.
|
|
21
|
+
|
|
22
|
+
The approach makes nesting depth a policy decision instead of a runtime stack
|
|
23
|
+
accident. Stable mode is tested with 25,000 nested arrays. Safe and canonical
|
|
24
|
+
modes intentionally apply finite defaults.
|
|
25
|
+
|
|
26
|
+
## Preparation and emission
|
|
27
|
+
|
|
28
|
+
Each value goes through preparation before output:
|
|
29
|
+
|
|
30
|
+
1. stable mode optionally invokes `toJSON` and a replacer;
|
|
31
|
+
2. the value is classified as primitive, container, or omitted;
|
|
32
|
+
3. object keys are selected and sorted;
|
|
33
|
+
4. descriptors or accessors are read according to policy;
|
|
34
|
+
5. text chunks are appended while enforcing the output budget.
|
|
35
|
+
|
|
36
|
+
Arrays preserve order. Unsupported stable-mode array items become `null`,
|
|
37
|
+
matching JSON semantics. Unsupported object members are omitted.
|
|
38
|
+
|
|
39
|
+
## Cycle detection
|
|
40
|
+
|
|
41
|
+
An operation-scoped `WeakMap` tracks active ancestors and their paths. Entries
|
|
42
|
+
are removed when a container closes, so repeated sibling references are not
|
|
43
|
+
misclassified as cycles. The current ancestor path supports strict errors,
|
|
44
|
+
markers, null substitution, and descriptive path markers.
|
|
45
|
+
|
|
46
|
+
No graph is retained after serialization.
|
|
47
|
+
|
|
48
|
+
## Stable mode
|
|
49
|
+
|
|
50
|
+
Stable mode sorts own enumerable string keys by UTF-16 code units. A comparator
|
|
51
|
+
can instead order `{ key, value }` records. Property-list replacers are
|
|
52
|
+
normalized once, deduplicated, and applied at every object depth.
|
|
53
|
+
|
|
54
|
+
Compatibility defaults intentionally leave depth, entry, and length budgets
|
|
55
|
+
unlimited. BigInt throws, accessors and `toJSON` are invoked, and cycles throw
|
|
56
|
+
unless configured otherwise.
|
|
57
|
+
|
|
58
|
+
## Safe mode
|
|
59
|
+
|
|
60
|
+
Safe mode delegates to the same engine with cycle markers, BigInt strings, and
|
|
61
|
+
finite budgets. It catches traversal failures and returns a controlled JSON
|
|
62
|
+
string unless `throwOnError` is enabled. This prevents the diagnostic path from
|
|
63
|
+
hiding the original application error behind a second serialization error.
|
|
64
|
+
|
|
65
|
+
## Canonical mode
|
|
66
|
+
|
|
67
|
+
Canonical mode validates I-JSON data while traversing:
|
|
68
|
+
|
|
69
|
+
- keys use recursive UTF-16 ordering required by RFC 8785;
|
|
70
|
+
- numbers and strings use ECMAScript JSON serialization;
|
|
71
|
+
- malformed Unicode and non-finite numbers are rejected;
|
|
72
|
+
- only arrays, plain objects, and null-prototype objects are accepted;
|
|
73
|
+
- data descriptors are read without invoking accessors;
|
|
74
|
+
- symbols, cycles, sparse arrays, and non-JSON values are rejected;
|
|
75
|
+
- output contains no insignificant whitespace.
|
|
76
|
+
|
|
77
|
+
`canonicalizeBytes` encodes the resulting string as UTF-8. It uses
|
|
78
|
+
`TextEncoder` where available and includes a small standards-compatible
|
|
79
|
+
fallback for older environments.
|
|
80
|
+
|
|
81
|
+
## Resource accounting
|
|
82
|
+
|
|
83
|
+
The engine tracks:
|
|
84
|
+
|
|
85
|
+
- container depth;
|
|
86
|
+
- visited object keys and array positions;
|
|
87
|
+
- UTF-16 output length.
|
|
88
|
+
|
|
89
|
+
Limit errors include a structured kind, configured limit, and a path such as
|
|
90
|
+
`<root>.request.headers` or `<root>.items[4]`.
|
|
91
|
+
|
|
92
|
+
## Distribution
|
|
93
|
+
|
|
94
|
+
One source module builds to:
|
|
95
|
+
|
|
96
|
+
- `dist/index.js` for ESM;
|
|
97
|
+
- `dist/index.cjs` for callable CommonJS;
|
|
98
|
+
- `dist/index.min.js` for the `StacklineStableStringify` browser global;
|
|
99
|
+
- resolver-specific `.d.mts`, `.d.cts`, and `.d.ts` declarations;
|
|
100
|
+
- source maps for every JavaScript artifact.
|
|
101
|
+
|
|
102
|
+
The build banner identifies package version and MIT license. Package tests load
|
|
103
|
+
all three JavaScript forms and execute the browser artifact in an isolated VM.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Benchmark Methodology
|
|
2
|
+
|
|
3
|
+
The benchmark runs native `JSON.stringify`,
|
|
4
|
+
`fast-json-stable-stringify@2.1.0`, stable mode, safe mode, and RFC 8785 mode
|
|
5
|
+
against the same representative payload in one Node.js process.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm ci
|
|
9
|
+
npm run benchmark
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Control the sample size with `BENCHMARK_ITERATIONS`:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
BENCHMARK_ITERATIONS=250000 npm run benchmark
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Native JSON is a throughput baseline only. It does not recursively sort object
|
|
19
|
+
keys and is not behaviorally equivalent. Stable, safe, and canonical modes
|
|
20
|
+
also implement different contracts, so compare a mode only with alternatives
|
|
21
|
+
that provide the same guarantees.
|
|
22
|
+
|
|
23
|
+
When publishing results, include the command, Node.js version, CPU, complete
|
|
24
|
+
raw output, and payload shape. Do not present one machine's result as a
|
|
25
|
+
universal ranking.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
## Default contract
|
|
4
|
+
|
|
5
|
+
The default export follows documented `fast-json-stable-stringify@2.1.0`
|
|
6
|
+
behavior:
|
|
7
|
+
|
|
8
|
+
- object keys are sorted recursively;
|
|
9
|
+
- arrays retain their order;
|
|
10
|
+
- a comparator receives `{ key, value }` records;
|
|
11
|
+
- comparator functions can be passed directly or as `cmp`;
|
|
12
|
+
- `{ cycles: true }` writes `__cycle__` for circular objects;
|
|
13
|
+
- `toJSON` runs before value serialization;
|
|
14
|
+
- JSON omission, primitive, wrapper-object, and non-finite number behavior is
|
|
15
|
+
preserved.
|
|
16
|
+
|
|
17
|
+
The suite performs 30,000 deterministic differential serializations across
|
|
18
|
+
generated JSON-compatible values, comparators, and option forms.
|
|
19
|
+
|
|
20
|
+
## Intentional additions
|
|
21
|
+
|
|
22
|
+
| Area | Compatible default | Addition |
|
|
23
|
+
| :--- | :--- | :--- |
|
|
24
|
+
| Circular arrays | Upstream can overflow | Same policy as circular objects |
|
|
25
|
+
| Deep input | Recursive stack limit | Iterative traversal |
|
|
26
|
+
| Cycles | Throw or `__cycle__` | Path, null, and custom marker policies |
|
|
27
|
+
| BigInt | Throw | String or safe-number policies |
|
|
28
|
+
| Replacer | Not exposed | Function and JSON property list |
|
|
29
|
+
| Formatting | Compact | JSON-style `space` option |
|
|
30
|
+
| Accessors | Invoke | Omit or throw policies |
|
|
31
|
+
| Resources | Unbounded | Opt-in depth, entry, and length limits |
|
|
32
|
+
| Diagnostics | Separate ecosystem | `safeStringify` named export |
|
|
33
|
+
| Canonical JSON | Separate ecosystem | RFC 8785 string and byte exports |
|
|
34
|
+
|
|
35
|
+
No addition changes output unless its option or named API is used, except that
|
|
36
|
+
circular arrays now return the configured cycle behavior instead of causing a
|
|
37
|
+
call-stack failure.
|
|
38
|
+
|
|
39
|
+
## Package forms
|
|
40
|
+
|
|
41
|
+
| Consumer | Supported form |
|
|
42
|
+
| :--- | :--- |
|
|
43
|
+
| ESM | default and named imports |
|
|
44
|
+
| CommonJS | callable `require()` plus static helpers |
|
|
45
|
+
| Browser | `StacklineStableStringify` global |
|
|
46
|
+
| TypeScript | resolver-specific declarations |
|
|
47
|
+
| npm alias | `fast-json-stable-stringify@npm:@stackline/stable-stringify` |
|
|
48
|
+
|
|
49
|
+
## TypeScript
|
|
50
|
+
|
|
51
|
+
The public declarations use syntax accepted by TypeScript 3.9. ESM-specific
|
|
52
|
+
`.d.mts` resolution is validated from TypeScript 4.7 onward. The release matrix
|
|
53
|
+
currently covers 3.9.10, 4.7.4, 4.9.5, 5.9.3, 6.0.2, and 7.0.2.
|
|
54
|
+
|
|
55
|
+
Return types remain `string | undefined`, matching JSON root-value behavior.
|
|
56
|
+
Canonical APIs return `string` or `Uint8Array` because invalid values throw.
|
|
57
|
+
|
|
58
|
+
## Migration
|
|
59
|
+
|
|
60
|
+
Direct migration:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import stringify from '@stackline/stable-stringify';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
No-import-change migration:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install fast-json-stable-stringify@npm:@stackline/stable-stringify
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The clean-install test executes both names in ESM and CommonJS from the packed
|
|
73
|
+
tarball, rather than resolving against the development workspace.
|
|
74
|
+
|
|
75
|
+
## Standards boundary
|
|
76
|
+
|
|
77
|
+
Stable output is deterministic for the same JavaScript data and options. It is
|
|
78
|
+
not automatically RFC 8785 canonical output. Use `canonicalize` when another
|
|
79
|
+
system, signature specification, or protocol requires JCS.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Market Research
|
|
2
|
+
|
|
3
|
+
## Decision
|
|
4
|
+
|
|
5
|
+
The selected opportunity is a unified deterministic JSON serializer with a
|
|
6
|
+
compatible stable API, safe diagnostic mode, and RFC 8785 canonical mode.
|
|
7
|
+
|
|
8
|
+
It addresses a mature, high-volume need without copying an abandoned codebase.
|
|
9
|
+
The runtime is original; comparison packages are development references only.
|
|
10
|
+
|
|
11
|
+
## Evidence window
|
|
12
|
+
|
|
13
|
+
The research uses the official npm downloads and registry APIs. The fixed
|
|
14
|
+
30-day window is 2026-07-17 through 2026-08-15. The comparison window is
|
|
15
|
+
2026-06-17 through 2026-07-16. Fixed dates make the findings reproducible.
|
|
16
|
+
|
|
17
|
+
| Package | Recent 30 days | Prior 30 days | Change | Latest release |
|
|
18
|
+
| :--- | ---: | ---: | ---: | :--- |
|
|
19
|
+
| `fast-json-stable-stringify` | 587,334,858 | 581,507,983 | +1.00% | 2.1.0, 2019-12-14 |
|
|
20
|
+
| `safe-stable-stringify` | 210,997,298 | 206,370,668 | +2.24% | 2.5.0, 2024-08-24 |
|
|
21
|
+
| `fast-safe-stringify` | 172,544,640 | 161,813,217 | +6.63% | 2.1.1, 2021-09-08 |
|
|
22
|
+
| `json-stringify-safe` | 157,096,721 | 154,552,336 | +1.65% | 5.0.1, 2015-05-19 |
|
|
23
|
+
| **Combined package activity** | **1,127,973,517** | **1,104,244,204** | **+2.15%** | four contracts |
|
|
24
|
+
|
|
25
|
+
The same packages recorded 9,395,146,202 combined downloads from 2025-08-16
|
|
26
|
+
through 2026-08-15.
|
|
27
|
+
|
|
28
|
+
These sums measure package activity, not unique users. Dependency trees can
|
|
29
|
+
install more than one package and repeated CI installs count again. The figures
|
|
30
|
+
demonstrate category scale and fragmentation; they do not guarantee adoption
|
|
31
|
+
for a new package.
|
|
32
|
+
|
|
33
|
+
## Primary sources
|
|
34
|
+
|
|
35
|
+
- [fast-json-stable-stringify downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/fast-json-stable-stringify)
|
|
36
|
+
- [safe-stable-stringify downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/safe-stable-stringify)
|
|
37
|
+
- [fast-safe-stringify downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/fast-safe-stringify)
|
|
38
|
+
- [json-stringify-safe downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/json-stringify-safe)
|
|
39
|
+
- [npm registry metadata](https://registry.npmjs.org/fast-json-stable-stringify)
|
|
40
|
+
- [RFC 8785: JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html)
|
|
41
|
+
- [ECMAScript JSON.stringify algorithm](https://tc39.es/ecma262/multipage/structured-data.html#sec-json.stringify)
|
|
42
|
+
|
|
43
|
+
## Product gap
|
|
44
|
+
|
|
45
|
+
The category is split by concern:
|
|
46
|
+
|
|
47
|
+
- the largest compatibility package offers stable key order but no current ESM
|
|
48
|
+
declaration surface, safe diagnostic mode, resource limits, or JCS API;
|
|
49
|
+
- safe serializers focus on cycles and logging but differ in BigInt, getter,
|
|
50
|
+
`toJSON`, depth, edge, and fallback behavior;
|
|
51
|
+
- canonical JSON implementations form another package category even though
|
|
52
|
+
cache keys and signatures often exist in the same systems;
|
|
53
|
+
- several established releases are between two and eleven years old while
|
|
54
|
+
download activity continues to grow.
|
|
55
|
+
|
|
56
|
+
Open issue themes in the comparison repositories include ESM, TypeScript
|
|
57
|
+
options, RFC 8785 support, wrapper semantics, output limits, getters, `toJSON`,
|
|
58
|
+
BigInt, and deep-stack behavior. The product contract directly tests those
|
|
59
|
+
boundaries instead of bundling unrelated utilities.
|
|
60
|
+
|
|
61
|
+
## Why this shape can grow
|
|
62
|
+
|
|
63
|
+
The adoption path has three levels:
|
|
64
|
+
|
|
65
|
+
1. existing users can keep the established function signature;
|
|
66
|
+
2. npm alias users can keep the existing import specifier;
|
|
67
|
+
3. new users gain safe and canonical modes without adding two more packages.
|
|
68
|
+
|
|
69
|
+
Zero runtime dependencies reduce supply-chain exposure. TypeScript 3.9 through
|
|
70
|
+
7.0, Node.js 14.17+, browsers, Deno, and Bun widen the usable install base.
|
|
71
|
+
|
|
72
|
+
## Rejected directions
|
|
73
|
+
|
|
74
|
+
### Another generic utility collection
|
|
75
|
+
|
|
76
|
+
Broad utility packages compete on dozens of unrelated APIs and create a larger
|
|
77
|
+
maintenance and security surface. The selected package owns one data boundary.
|
|
78
|
+
|
|
79
|
+
### Canonical JSON only
|
|
80
|
+
|
|
81
|
+
Standards compliance is valuable but is a smaller migration surface. Combining
|
|
82
|
+
it with the established stable call shape creates a practical entry point.
|
|
83
|
+
|
|
84
|
+
### Forking a legacy serializer
|
|
85
|
+
|
|
86
|
+
Forking would accelerate initial code volume but inherit architecture and
|
|
87
|
+
maintenance constraints. Behavioral compatibility is validated through tests;
|
|
88
|
+
the runtime remains independently designed and licensed.
|
|
89
|
+
|
|
90
|
+
## Success metrics
|
|
91
|
+
|
|
92
|
+
Potential is evaluated through measurable signals, not a promised download
|
|
93
|
+
number:
|
|
94
|
+
|
|
95
|
+
- direct and alias installs that need no code changes;
|
|
96
|
+
- issue resolution time and compatibility reports;
|
|
97
|
+
- reverse dependencies and weekly npm downloads;
|
|
98
|
+
- bundle size and runtime dependency count;
|
|
99
|
+
- regression-free releases across the supported matrix;
|
|
100
|
+
- adoption of safe and canonical named APIs.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
## Preconditions
|
|
4
|
+
|
|
5
|
+
- `main` is the only default development branch.
|
|
6
|
+
- Version and changelog agree.
|
|
7
|
+
- Runtime dependencies remain zero or the exception is documented.
|
|
8
|
+
- The full local suite and GitHub CI pass.
|
|
9
|
+
- The npm and Verdaccio package names are available or authenticated.
|
|
10
|
+
|
|
11
|
+
## Local release gate
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm ci
|
|
15
|
+
npm test
|
|
16
|
+
npm run test:attw
|
|
17
|
+
npm run audit:dependencies
|
|
18
|
+
npm pack --dry-run
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run the benchmark for regression evidence, not as a pass/fail release gate.
|
|
22
|
+
|
|
23
|
+
## Artifact creation
|
|
24
|
+
|
|
25
|
+
Build once and retain one immutable tarball:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
version="$(node -p "require('./package.json').version")"
|
|
29
|
+
mkdir -p "release/$version"
|
|
30
|
+
npm pack --ignore-scripts --pack-destination "release/$version"
|
|
31
|
+
(cd "release/$version" && sha512sum *.tgz > SHA512SUMS)
|
|
32
|
+
npm sbom --omit=dev --sbom-format cyclonedx > "release/$version/sbom.cdx.json"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run `scripts/smoke-install.mjs` against the retained path. The same bytes must
|
|
36
|
+
be published to Verdaccio and public npm.
|
|
37
|
+
|
|
38
|
+
## Registry order
|
|
39
|
+
|
|
40
|
+
1. publish the retained tarball to Verdaccio;
|
|
41
|
+
2. install directly and through the compatibility alias;
|
|
42
|
+
3. compare tarball integrity and run smoke tests;
|
|
43
|
+
4. run `publish.yml` with the expected SHA-512; the trusted workflow rebuilds
|
|
44
|
+
the reviewed commit and stops unless its tarball is byte-identical;
|
|
45
|
+
5. let the workflow publish with npm provenance, then download it and compare
|
|
46
|
+
SHA-512;
|
|
47
|
+
6. verify `latest`, provenance metadata, dependency audit, and registry
|
|
48
|
+
signature where available.
|
|
49
|
+
|
|
50
|
+
Never rebuild between registry publications.
|
|
51
|
+
|
|
52
|
+
## GitHub release
|
|
53
|
+
|
|
54
|
+
Tag the exact tested commit as `vX.Y.Z`. Attach the tarball, `SHA512SUMS`, and
|
|
55
|
+
CycloneDX SBOM. Confirm CI and CodeQL are green for the tagged source.
|
|
56
|
+
|
|
57
|
+
## Documentation
|
|
58
|
+
|
|
59
|
+
Build and test the static site, deploy it under
|
|
60
|
+
`/docs/vanilla/stable-stringify/`, update the central sitemap, and verify HTML,
|
|
61
|
+
CSS, JavaScript, image, robots, sitemap, metadata, LLM references, mobile
|
|
62
|
+
layout, and playground behavior in production.
|
|
63
|
+
|
|
64
|
+
## Rollback
|
|
65
|
+
|
|
66
|
+
npm versions are immutable. Do not unpublish a consumed release except for a
|
|
67
|
+
compelling security or legal reason. Deprecate a bad version with a precise
|
|
68
|
+
message and publish a patch from the last known-good source.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import stringify from '@stackline/stable-stringify';
|
|
4
|
+
|
|
5
|
+
const first = { query: { page: 2, status: 'open' }, tenant: 42 };
|
|
6
|
+
const second = { tenant: 42, query: { status: 'open', page: 2 } };
|
|
7
|
+
|
|
8
|
+
const firstKey = stringify(first);
|
|
9
|
+
const secondKey = stringify(second);
|
|
10
|
+
|
|
11
|
+
assert.equal(firstKey, secondKey);
|
|
12
|
+
console.log(firstKey);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
import { canonicalizeBytes } from '@stackline/stable-stringify';
|
|
5
|
+
|
|
6
|
+
const document = {
|
|
7
|
+
issuedAt: '2026-08-21T00:00:00Z',
|
|
8
|
+
subject: 'release-manifest',
|
|
9
|
+
version: 1
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const digest = createHash('sha256')
|
|
13
|
+
.update(canonicalizeBytes(document))
|
|
14
|
+
.digest('hex');
|
|
15
|
+
|
|
16
|
+
assert.equal(digest.length, 64);
|
|
17
|
+
console.log(digest);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { safeStringify } from '@stackline/stable-stringify';
|
|
4
|
+
|
|
5
|
+
const event = { id: 42n, name: 'checkout.failed' };
|
|
6
|
+
event.context = event;
|
|
7
|
+
|
|
8
|
+
const output = safeStringify(event);
|
|
9
|
+
|
|
10
|
+
assert.equal(
|
|
11
|
+
output,
|
|
12
|
+
'{"context":"[Circular]","id":"42","name":"checkout.failed"}'
|
|
13
|
+
);
|
|
14
|
+
console.log(output);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackline/stable-stringify",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Deterministic, cycle-aware JSON serialization with a fast-json-stable-stringify-compatible API and RFC 8785 canonicalization",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stable-stringify",
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
},
|
|
63
63
|
"files": [
|
|
64
64
|
"dist",
|
|
65
|
+
"docs",
|
|
66
|
+
"examples",
|
|
65
67
|
"CHANGELOG.md",
|
|
66
68
|
"CONTRIBUTING.md",
|
|
67
69
|
"LICENSE",
|
|
@@ -76,10 +78,11 @@
|
|
|
76
78
|
"clean": "node scripts/clean.mjs",
|
|
77
79
|
"build": "node scripts/build.mjs",
|
|
78
80
|
"lint": "eslint . && node scripts/check-markdown.mjs",
|
|
79
|
-
"test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:package && npm run test:install && npm run test:docs",
|
|
81
|
+
"test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:examples && npm run test:package && npm run test:install && npm run test:docs",
|
|
80
82
|
"test:unit": "node --test --test-reporter=spec test/*.test.mjs",
|
|
81
83
|
"test:coverage": "c8 --all --src src --check-coverage --lines 100 --functions 100 --statements 100 --branches 95 node --test test/core.test.mjs test/compatibility.test.mjs test/cycles-limits.test.mjs test/canonical.test.mjs test/safe.test.mjs",
|
|
82
84
|
"test:types": "node scripts/test-types.mjs",
|
|
85
|
+
"test:examples": "node examples/cache-key.mjs && node examples/content-digest.mjs && node examples/safe-logging.mjs",
|
|
83
86
|
"test:package": "node --test test/package.test.mjs && node scripts/check-dist.mjs && publint",
|
|
84
87
|
"test:install": "node scripts/smoke-install.mjs",
|
|
85
88
|
"test:docs": "npm run docs:build && node scripts/check-docs.mjs",
|