diff --git a/packages/fast-stable-stringify/package.json b/packages/fast-stable-stringify/package.json index e319f24d452..6f9948ccd4e 100644 --- a/packages/fast-stable-stringify/package.json +++ b/packages/fast-stable-stringify/package.json @@ -37,6 +37,7 @@ ], "scripts": { "compile:js": "tsup --config build-scripts/tsup.config.package.ts", + "dev": "jest -c ../../node_modules/@solana/test-config/jest-dev.config.ts --rootDir . --watch", "prepublishOnly": "pnpm pkg delete devDependencies", "publish-impl": "npm view $npm_package_name@$npm_package_version > /dev/null 2>&1 || pnpm publish --tag ${PUBLISH_TAG:-preview} --access public --no-git-checks", "publish-packages": "pnpm prepublishOnly && pnpm publish-impl", @@ -46,8 +47,12 @@ "test:treeshakability:browser": "agadoo dist/index.browser.js", "test:treeshakability:native": "agadoo dist/index.native.js", "test:treeshakability:node": "agadoo dist/index.node.js", - "test:unit:browser": "jest -c ../../node_modules/@solana/test-config/jest-unit.config.browser.ts --globalSetup @solana/test-config/test-validator-setup.js --globalTeardown @solana/test-config/test-validator-teardown.js --rootDir . --silent", - "test:unit:node": "jest -c ../../node_modules/@solana/test-config/jest-unit.config.node.ts --globalSetup @solana/test-config/test-validator-setup.js --globalTeardown @solana/test-config/test-validator-teardown.js --rootDir . --silent" + "test:unit:browser": "jest -c ../../node_modules/@solana/test-config/jest-unit.config.browser.ts --rootDir . --silent", + "test:unit:node": "jest -c ../../node_modules/@solana/test-config/jest-unit.config.node.ts --rootDir . --silent" + }, + "devDependencies": { + "@types/json-stable-stringify": "^1.0.36", + "json-stable-stringify": "^1.1.1" }, "author": "Solana Labs Maintainers ", "license": "MIT", diff --git a/packages/fast-stable-stringify/src/__tests__/index-test.ts b/packages/fast-stable-stringify/src/__tests__/index-test.ts new file mode 100644 index 00000000000..19264bae30f --- /dev/null +++ b/packages/fast-stable-stringify/src/__tests__/index-test.ts @@ -0,0 +1,201 @@ +import jsonStableStringify from 'json-stable-stringify'; + +import stringify from '../index'; + +class ImplementingToJSON { + toJSON() { + return 'dummy!'; + } +} + +class NotImplementingToJSON {} + +describe('fastStableStringify', function () { + it.each([ + [ + 'strings', + { + BACKSPACE: '\b', + CARRIAGE_RETURN: '\r', + EMPTY_STRING: '', + ESCAPE_RANGE: '\u0000\u001F', + FORM_FEED: '\f', + LINE_FEED: '\n', + LOWERCASE: 'abc', + MIXED: 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b', + NON_ESCAPE_RANGE: '\u0020\uFFFF', + NUMBER_ONLY: '123', + QUOTATION_MARK: '"', + REVERSE_SOLIDUS: '\\', + SOLIDUS: '/', + TAB: '\t', + UPPERCASE: 'ABC', + UTF16: '☃', + VALUES_WITH_SPACES: 'a b c', + }, + ], + [ + 'object keys', + { + '': 'EMPTY_STRING', + '\u0000\u001F': 'ESCAPE_RANGE', + '\b': 'BACKSPACE', + '\t': 'TAB', + '\n': 'LINE_FEED', + '\f': 'FORM_FEED', + '\r': 'CARRIAGE_RETURN', + '\u0020\uFFFF': 'NON_ESCAPE_RANGE', + '"': 'QUOTATION_MARK', + '/': 'SOLIDUS', + ABC: 'UPPERCASE', + 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b': 'MIXED', + NUMBER_ONLY: '123', + '\\': 'REVERSE_SOLIDUS', + 'a b c': 'VALUES_WITH_SPACES', + abc: 'LOWERCASE', + '☃': 'UTF16', + }, + ], + [ + 'numbers', + { + FALSY: 0, + FLOAT: 0.1234567, + INFINITY: Infinity, + MAX_SAFE_INTEGER: 9007199254740991, + MAX_VALUE: 1.7976931348623157e308, + MIN_SAFE_INTEGER: -9007199254740991, + MIN_VALUE: 5e-324, + NAN: NaN, + NEGATIVE: -1, + NEGATIVE_FLOAT: -0.9876543, + NEGATIVE_MAX_VALUE: -1.7976931348623157e308, + NEGATIVE_MIN_VALUE: -5e-324, + NEG_INFINITY: -Infinity, + }, + ], + ['true', true], + ['false', false], + ['undefined', undefined], + ['null', null], + ['objects of undefineds', { ONE: undefined, THREE: undefined, TWO: undefined }], + ['objects of null', { NULL: null }], + ['a Date instance', new Date('2017')], + ['a function', function () {}], + ['object that implements `toJSON`', new ImplementingToJSON()], + ['objects that does not implements `toJSON`', new NotImplementingToJSON()], + [ + 'objects of mixed values', + { + 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b': 'MIXED', + FALSE: false, + MAX_VALUE: 1.7976931348623157e308, + MIN_VALUE: 5e-324, + MIXED: 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b', + NEGATIVE_MAX_VALUE: -1.7976931348623157e308, + NEGATIVE_MIN_VALUE: -5e-324, + NULL: null, + TRUE: true, + UNDEFINED: undefined, + zzz: 'ending', + }, + ], + [ + 'arrays of numbers', + [ + 9007199254740991, + -9007199254740991, + 0, + -1, + 0.1234567, + -0.9876543, + 1.7976931348623157e308, + 5e-324, + -1.7976931348623157e308, + -5e-324, + Infinity, + -Infinity, + NaN, + ], + ], + [ + 'arrays of strings', + [ + 'a b c', + 'abc', + 'ABC', + 'NUMBER_ONLY', + '', + '\u0000\u001F', + '\u0020\uFFFF', + '☃', + '"', + '\\', + '/', + '\f', + '\n', + '\r', + '\t', + '\b', + 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b', + ], + ], + ['arrays of booleans ', [true, false]], + ['arrays of null', [null]], + ['arrays of undefined', [undefined]], + ['arrays of Date instances', [new Date('2017')]], + ['arrays of instances that implement `toJSON`', [new ImplementingToJSON()]], + ['arrays of instances that do not implement `toJSON`', [new NotImplementingToJSON()]], + ['arrays of functions', [function () {}]], + [ + 'arrays of mixed values', + [ + -1.7976931348623157e308, + -5e-324, + 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b', + true, + false, + null, + undefined, + ], + ], + [ + 'mixed values', + [ + { + 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b': 'MIXED', + DATE: new Date('2017'), + FALSE: false, + FUNCTION: function () {}, + IMPLEMENTING_TO_JSON: new ImplementingToJSON(), + MAX_VALUE: 1.7976931348623157e308, + MIN_VALUE: 5e-324, + MIXED: 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b', + NEGATIVE_MAX_VALUE: -1.7976931348623157e308, + NEGATIVE_MIN_VALUE: -5e-324, + NOT_IMPLEMENTING_TO_JSON: new NotImplementingToJSON(), + NULL: null, + TRUE: true, + UNDEFINED: undefined, + zzz: 'ending', + }, + -1.7976931348623157e308, + -5e-324, + 'Aa1 Bb2 Cc3 \u0000\u001F\u0020\uFFFF☃"\\/\f\n\r\t\b', + true, + false, + null, + undefined, + new Date('2017'), + function () {}, + new ImplementingToJSON(), + new NotImplementingToJSON(), + ], + ], + ])('matches the output of `json-stable-stringify` when hashing %s (`%s`)', (_, value) => { + expect(stringify(value)).toBe(jsonStableStringify(value)); + }); + it('hashes bigints', function () { + expect(stringify(200n)).toMatch('"200n"'); + }); +}); diff --git a/packages/fast-stable-stringify/src/__tests__/json_test.ts b/packages/fast-stable-stringify/src/__tests__/json_test.ts deleted file mode 100644 index 2f1f7c557a9..00000000000 --- a/packages/fast-stable-stringify/src/__tests__/json_test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import stringify from '..'; - -describe('test_fast_stable_stringify', function () { - it('big int', function () { - const obj = { age: 23, bigint: BigInt('200'), name: 'ABCD' }; - expect(stringify(obj)).toMatch('{"age":23,"bigint":"200n","name":"ABCD"}'); - }); - - it('only string', function () { - const obj = { food: 'Pizza', name: 'ABCD' }; - expect(stringify(obj)).toMatch('{"food":"Pizza","name":"ABCD"}'); - }); - - it('array', function () { - const obj = { hobbies: ['football', 'basketball', 'skating'], name: 'ABCD' }; - expect(stringify(obj)).toMatch('{"hobbies":["football","basketball","skating"],"name":"ABCD"}'); - }); - - it('undefined', function () { - const obj = { hobbies: undefined, name: 'ABCD' }; - expect(stringify(obj)).toMatch('{"name":"ABCD"}'); - }); - - it('nested object', function () { - const obj = { address: { country: 'India', pincode: 10101, state: 'delhi' }, name: 'ABCD' }; - expect(stringify(obj)).toMatch('{"address":{"country":"India","pincode":10101,"state":"delhi"},"name":"ABCD"}'); - }); - - it('infinity', function () { - const obj = { name: 'ABCD', value: Infinity }; - expect(stringify(obj)).toMatch('{"name":"ABCD","value":null}'); - }); - - it('toJSON function', function () { - let obj = { - age: 23, - name: 'ABCD', - toJSON: () => { - return { value: '1' }; - }, - }; - expect(stringify(obj)).toMatch('{"value":"1"}'); - - obj = { - address: { - country: 'Africa', - toJSON: () => { - return { country: 'India' }; - }, - }, - name: 'ABCD', - }; - expect(stringify(obj)).toMatch('{"address":{"country":"India"},"name":"ABCD"}'); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a556df5ccb6..329c0e445ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,7 +264,14 @@ importers: specifier: ^12.0.0 version: 12.0.0 - packages/fast-stable-stringify: {} + packages/fast-stable-stringify: + devDependencies: + '@types/json-stable-stringify': + specifier: ^1.0.36 + version: 1.0.36 + json-stable-stringify: + specifier: ^1.1.1 + version: 1.1.1 packages/functional: {} @@ -4654,6 +4661,10 @@ packages: resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} dev: true + /@types/json-stable-stringify@1.0.36: + resolution: {integrity: sha512-b7bq23s4fgBB76n34m2b3RBf6M369B0Z9uRR8aHTMd8kZISRkmDEpPD8hhpYvDFzr3bJCPES96cm3Q6qRNDbQw==} + dev: true + /@types/json5@0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} requiresBuild: true @@ -9790,6 +9801,16 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /json-stable-stringify@1.1.1: + resolution: {integrity: sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + dev: true + /json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -9825,6 +9846,10 @@ packages: graceful-fs: 4.2.11 dev: true + /jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + dev: true + /jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0}