Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/*
!index.d.ts
!index.js
!wrapper.cjs
!LICENSE
!package.json
!esm/*
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Next

- **Breaking change:** Require Node.js 18 or later.
- Fix ESM imports in Node.js and bundlers, including applications that also use CommonJS.

- Changed the default circular marker to `'[Circular: N]'`, where `N` is the referenced ancestor's depth, starting at 0 for the root.
Set `circularValue: '[Circular]'` to retain the previous format. This changes the serialized output for circular references.

Expand Down
4 changes: 0 additions & 4 deletions esm/wrapper.d.ts

This file was deleted.

11 changes: 7 additions & 4 deletions esm/wrapper.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import cjsModule from '../index.js'
import '../index.js'

export const configure = cjsModule.configure
const sharedGlobal = /** @type {Record<symbol, typeof import('../index.js').default>} */ (
globalThis
)

export const stringify = cjsModule
export default cjsModule
export const stringify = sharedGlobal[Symbol.for('safe-stable-stringify@3.0.0')]
export const configure = stringify.configure
export default stringify
42 changes: 23 additions & 19 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
export type Replacer = (number | string)[] | null | undefined | ((key: string, value: unknown) => string | number | boolean | null | object)

export function stringify(value: undefined | symbol | ((...args: unknown[]) => unknown), replacer?: Replacer, space?: string | number): undefined
export function stringify(value: string | number | unknown[] | null | boolean | object, replacer?: Replacer, space?: string | number): string
export function stringify(value: unknown, replacer?: ((key: string, value: unknown) => unknown) | (number | string)[] | null | undefined, space?: string | number): string | undefined

export interface StringifyOptions {
bigint?: boolean | 'string',
/** Defaults to '[Circular: N]', where N is the ancestor depth, starting at 0 for the root. */
circularValue?: string | null | TypeErrorConstructor | ErrorConstructor,
deterministic?: boolean | ((a: string, b: string) => number),
maximumBreadth?: number,
maximumDepth?: number,
strict?: boolean,
safe?: boolean,
interface StringifyFunction {
(value: undefined | symbol | ((...args: unknown[]) => unknown), replacer?: stringify.Replacer, space?: string | number): undefined
(value: string | number | unknown[] | null | boolean | object, replacer?: stringify.Replacer, space?: string | number): string
(value: unknown, replacer?: ((key: string, value: unknown) => unknown) | (number | string)[] | null | undefined, space?: string | number): string | undefined
}

export namespace stringify {
export function configure(options: StringifyOptions): typeof stringify
declare const stringify: StringifyFunction & {
configure(options: stringify.StringifyOptions): StringifyFunction
stringify: typeof stringify
default: typeof stringify
}

export function configure(options: StringifyOptions): typeof stringify
declare namespace stringify {
export type Replacer = (number | string)[] | null | undefined | ((key: string, value: unknown) => string | number | boolean | null | object)

export interface StringifyOptions {
bigint?: boolean | 'string',
/** Defaults to '[Circular: N]', where N is the ancestor depth, starting at 0 for the root. */
circularValue?: string | null | TypeErrorConstructor | ErrorConstructor,
deterministic?: boolean | ((a: string, b: string) => number),
maximumBreadth?: number,
maximumDepth?: number,
strict?: boolean,
safe?: boolean,
}
}

export default stringify
export = stringify
38 changes: 21 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,6 @@ const { hasOwnProperty } = Object.prototype

/** @typedef {{toJSON: (key: string) => unknown}} ToJSON */

const stringify = configure()

// @ts-expect-error
stringify.configure = configure
// @ts-expect-error
stringify.stringify = stringify

// @ts-expect-error
stringify.default = stringify

// @ts-expect-error used for named export
exports.stringify = stringify
// @ts-expect-error used for named export
exports.configure = configure

module.exports = stringify

// eslint-disable-next-line no-control-regex
const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/

Expand Down Expand Up @@ -779,3 +762,24 @@ function configure (options) {

return stringify
}

const sharedGlobal = /** @type {Record<symbol, typeof import('./index.js').default | undefined>} */ (
globalThis
)
const exportSymbol = Symbol.for('safe-stable-stringify@3.0.0')

if (sharedGlobal[exportSymbol] === undefined) {
const stringify = configure()
// @ts-expect-error
stringify.configure = configure
// @ts-expect-error
stringify.stringify = stringify
// @ts-expect-error
stringify.default = stringify
Object.defineProperty(sharedGlobal, exportSymbol, {
configurable: true,
enumerable: false,
value: /** @type {typeof import('./index.js').default} */ (stringify),
writable: true
})
}
19 changes: 11 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"name": "safe-stable-stringify",
"version": "2.5.0",
"version": "3.0.0",
"description": "Deterministic and safely JSON.stringify to quickly serialize JavaScript objects",
"exports": {
"require": "./index.js",
"import": "./esm/wrapper.js"
"types": "./index.d.ts",
"require": "./wrapper.cjs",
"import": "./esm/wrapper.mjs"
},
"keywords": [
"stable",
Expand All @@ -21,18 +22,18 @@
"fast",
"bigint"
],
"main": "index.js",
"main": "wrapper.cjs",
"scripts": {
"test": "standard && tap test.js",
"tap": "tap test.js",
"tap:only": "tap test.js --watch --only",
"test": "standard && tap test.js test-modules.js",
"tap": "tap test.js test-modules.js",
"tap:only": "tap test.js test-modules.js --watch --only",
"benchmark": "node --allow-natives-syntax benchmark.js",
"compare": "node --allow-natives-syntax compare.js",
"lint": "standard --fix",
"tsc": "tsc --project tsconfig.json"
},
"engines": {
"node": ">=10"
"node": ">=18"
},
"author": "Ruben Bridgewater",
"license": "MIT",
Expand All @@ -41,6 +42,7 @@
"@types/json-stable-stringify": "^1.2.0",
"@types/node": "^18.19.130",
"clone": "^2.1.2",
"esbuild": "^0.21.5",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"fast-stable-stringify": "^1.0.0",
Expand All @@ -49,6 +51,7 @@
"json-stable-stringify": "^1.3.0",
"json-stringify-deterministic": "^1.0.12",
"json-stringify-safe": "^5.0.1",
"rollup": "^3.30.0",
"bench-node": "^0.14.0",
"standard": "^17.1.2",
"tap": "^16.3.10",
Expand Down
6 changes: 6 additions & 0 deletions test-fixtures/consumer.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'

const stringify = require('safe-stable-stringify')

if (stringify !== stringify.default || stringify !== stringify.stringify) throw new Error('Export mismatch')
if (stringify({ b: 2, a: 1 }) !== '{"a":1,"b":2}') throw new Error('Incorrect output')
5 changes: 5 additions & 0 deletions test-fixtures/consumer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import stringify, { configure, stringify as named } from 'safe-stable-stringify'

if (stringify !== named || configure !== stringify.configure) throw new Error('Export mismatch')
if (stringify({ b: 2, a: 1 }) !== '{"a":1,"b":2}') throw new Error('Incorrect output')
if (configure({ deterministic: false })({ b: 2, a: 1 }) !== '{"b":2,"a":1}') throw new Error('Incorrect configuration')
55 changes: 55 additions & 0 deletions test-fixtures/modules.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict'

const assert = require('node:assert/strict')

const { version } = require('./node_modules/safe-stable-stringify/package.json')
const root = globalThis

async function check () {
let cjs
let esm
let otherVersion
if (process.argv[2] !== 'import-first') {
cjs = require('./first.cjs')
otherVersion = require('./other-version')
esm = await import('./first.mjs')
} else {
esm = await import('./first.mjs')
otherVersion = require('./other-version')
cjs = require('./first.cjs')
}
assert.notEqual(otherVersion, cjs)
assert.equal(otherVersion({ b: 2, a: 1 }), '{"a":1,"b":2}')
assert.equal(root[Symbol.for(`safe-stable-stringify@${version}`)], cjs)
assert.equal(Object.getOwnPropertyDescriptor(root, Symbol.for(`safe-stable-stringify@${version}`)).enumerable, false)
assert.equal(cjs, esm.default)
assert.equal(cjs, esm.stringify)
assert.equal(cjs.configure, esm.configure)
assert.equal(cjs, cjs.default)
assert.equal(cjs, cjs.stringify)
assert.equal(cjs({ b: 2, a: 1 }), '{"a":1,"b":2}')
const configured = esm.configure({ deterministic: false })
assert.equal(configured.stringify, undefined)
assert.equal(configured.default, undefined)
assert.equal(configured.configure, undefined)
assert.equal(configured({ b: 2, a: 1 }), '{"b":2,"a":1}')
assert.equal(require('./second.cjs'), cjs)
assert.equal(require('./same-version'), cjs)
const modules = await Promise.all([
import('./second.mjs'),
import('./first.mjs'),
import('./same-version/esm/wrapper.mjs')
])
for (const module of modules) {
assert.equal(module.default, cjs)
assert.equal(module.configure, cjs.configure)
}
}

/** @param {Error} error */
function onError (error) {
console.error(error)
process.exitCode = 1
}

check().catch(onError)
28 changes: 28 additions & 0 deletions test-fixtures/types.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import stringify = require('safe-stable-stringify')
import type { Replacer, StringifyOptions } from 'safe-stable-stringify'

const options: StringifyOptions = { deterministic: false }
const replacer: Replacer = ['value']
const result: string | undefined = stringify({ value: 1 })
const alias: string | undefined = stringify.stringify({ result })
stringify.default({ alias })
stringify.configure(options)({ value: 1 }, replacer)

const namespacedOptions: stringify.StringifyOptions = options
const namespacedReplacer: stringify.Replacer = replacer
stringify.configure(namespacedOptions)({ value: 1 }, namespacedReplacer)

const configured = stringify.configure(options)
const configuredResult: string = configured('value')
const omitted: undefined = configured(undefined)
configured({ configuredResult, omitted })

// @ts-expect-error
configured.stringify({ value: 1 })
// @ts-expect-error
configured.default({ value: 1 })
// @ts-expect-error
configured.configure(options)

// @ts-expect-error
stringify.configure({ bigint: 'invalid' })
22 changes: 22 additions & 0 deletions test-fixtures/types.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import stringify, { configure, stringify as named, type Replacer, type StringifyOptions } from 'safe-stable-stringify'

const options: StringifyOptions = { deterministic: false }
const replacer: Replacer = ['value']
const result: string | undefined = stringify({ value: 1 })
const alias: string | undefined = named({ result })
configure(options)({ alias }, replacer)

const configured = configure(options)
const configuredResult: string = configured('value')
const omitted: undefined = configured(undefined)
configured({ configuredResult, omitted })

// @ts-expect-error
configured.stringify({ value: 1 })
// @ts-expect-error
configured.default({ value: 1 })
// @ts-expect-error
configured.configure(options)

// @ts-expect-error
configure({ bigint: 'invalid' })
Loading
Loading