Skip to content

Commit 381208a

Browse files
committed
fix: use toString and prototype instead of constructor for type checking
1 parent 13da38b commit 381208a

File tree

6 files changed

+49
-7
lines changed

6 files changed

+49
-7
lines changed

.changeset/easy-boxes-shine.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"stable-hash-x": patch
3+
---
4+
5+
fix: use `toString` and `prototype` instead of `constructor` for type checking

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.yarn
2+
/public/site.webmanifest

.size-limit.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
{
33
"path": "./lib/index.js",
4-
"limit": "460B"
4+
"limit": "540B"
55
}
66
]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
1414
[![changesets](https://img.shields.io/badge/maintained%20with-changesets-176de3.svg)](https://github.com/changesets/changesets)
1515

16-
A tiny and fast (460b <sup>[unpkg](https://unpkg.com/stable-hash-x@latest/lib/index.js)</sup>) lib for "stably hashing" a JavaScript value. Originally created for [SWR](https://github.com/vercel/swr) by [Shu Ding][] at [`stable-hash`](https://github.com/shuding/stable-hash), we forked it because the original one is a bit out of maintenance for a long time.
16+
A tiny and fast (540b <sup>[unpkg](https://unpkg.com/stable-hash-x@latest/lib/index.js)</sup>) lib for "stably hashing" a JavaScript value, works with cross-realm objects. Originally created for [SWR](https://github.com/vercel/swr) by [Shu Ding][] at [`stable-hash`](https://github.com/shuding/stable-hash), we forked it because the original one is a bit out of maintenance for a long time.
1717

1818
It's similar to `JSON.stringify(value)`, but:
1919

src/index.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ const table = new WeakMap<object, string>()
66
// A counter of the key.
77
let counter = 0
88

9+
// eslint-disable-next-line @typescript-eslint/unbound-method
10+
const { toString } = Object.prototype // type-coverage:ignore-line
11+
12+
const isType = (arg: unknown, type: string): boolean =>
13+
toString.call(arg) === `[object ${type}]`
14+
15+
// based on https://github.com/sindresorhus/is-plain-obj
16+
const isPlainObject = (val: object) => {
17+
const prototype: unknown = Object.getPrototypeOf(val)
18+
return (
19+
prototype === null ||
20+
prototype === Object.prototype || // type-coverage:ignore-line
21+
Object.getPrototypeOf(prototype) === null
22+
)
23+
}
24+
925
// A stable hash implementation that supports:
1026
// - Fast and ensures unique hash properties
1127
// - Handles unserializable values
@@ -17,10 +33,9 @@ let counter = 0
1733
// eslint-disable-next-line sonarjs/cognitive-complexity
1834
export function stableHash(arg: unknown): string {
1935
const type = typeof arg
20-
const constructor = arg?.constructor
21-
const isDate = constructor === Date
36+
const isDate = isType(arg, 'Date')
2237

23-
if (Object(arg) === arg && !isDate && constructor != RegExp) {
38+
if (Object(arg) === arg && !isDate && !isType(arg, 'RegExp')) {
2439
const arg_ = arg as object
2540
// Object/function, not null/date/regexp. Use WeakMap to store the id first.
2641
// If it's already hashed, directly return the result.
@@ -34,15 +49,15 @@ export function stableHash(arg: unknown): string {
3449
result = ++counter + '~'
3550
table.set(arg_, result)
3651
let index: number | string | undefined
37-
if (constructor === Array) {
52+
if (Array.isArray(arg)) {
3853
const arg_ = arg as unknown[]
3954
// Array.
4055
result = '@'
4156
for (index = 0; index < arg_.length; index++) {
4257
result += stableHash(arg_[index]) + ','
4358
}
4459
table.set(arg_, result)
45-
} else if (constructor === Object) {
60+
} else if (isPlainObject(arg_)) {
4661
// Object, sort keys.
4762
result = '#'
4863
// eslint-disable-next-line sonarjs/no-alphabetical-sort
@@ -58,9 +73,11 @@ export function stableHash(arg: unknown): string {
5873
}
5974
return result
6075
}
76+
6177
if (isDate) {
6278
return (arg as Date).toJSON()
6379
}
80+
6481
if (type === 'symbol') {
6582
return (arg as symbol).toString()
6683
}

tests/unit.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-magic-numbers */
22

3+
import { runInNewContext } from 'node:vm'
4+
35
import { hash } from 'stable-hash-x'
46

57
describe(`Booleans`, () => {
@@ -362,4 +364,21 @@ describe(`The Func-y Bunch featuring The Referential Squad`, () => {
362364
expect(hash(catMap)).toEqual(hash(catMap))
363365
expect(hash(catMap)).not.toEqual(hash(catMap2))
364366
})
367+
368+
test('across realms', () => {
369+
const obj1 = {
370+
a: 1,
371+
b: new Date('2022-06-25T01:55:27.743Z'),
372+
c: /test/,
373+
f: Symbol('test'),
374+
}
375+
const obj2 = runInNewContext(`({
376+
a: 1,
377+
b: new Date('2022-06-25T01:55:27.743Z'),
378+
c: /test/,
379+
f: Symbol('test'),
380+
})`) as typeof obj1
381+
expect(obj1).not.toEqual(obj2)
382+
expect(hash(obj1)).toEqual(hash(obj2))
383+
})
365384
})

0 commit comments

Comments
 (0)