Repository: pmndrs/zustand (ts)

Issue:
"TypeError: create is not a function" when using with jest configured for ESM
Consider the following setup:

`package.json`:
```js
{
  "name": "repro",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "dependencies": {
    "jest": "27.1.1",
    "react": "17.0.2",
    "zustand": "3.5.10"
  }
}
```

`index.js`:
```js
import create from 'zustand'

export const useStore = create(set => ({
    foo: 0,
}))
```

`repro.test.js`:
```js
import { useStore } from "."

test("repro", () => {
    console.log(useStore.getState())
});
```

Then, when running `node --experimental-vm-modules node_modules/.bin/jest` I get:

```
    TypeError: create is not a function

      3 | console.log(create)
      4 |
    > 5 | export const useStore = create(set => ({
        |                         ^
      6 |     foo: 0,
      7 | }))
      8 |

      at index.js:5:25
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:387:19)
      at _run10000 (node_modules/@jest/core/build/cli/index.js:408:7)

  console.log
    { default: [Function: create] }
```

It seems that the import is not picking the esm bundle?

Pull request:
Fix: Programmatic `module.exports` addition for named and default exports in CJS
Patches the base index file to export the default as classic node cjs module

## Related Issues

Fixes #559 

## Summary



## Check List

- [x] `yarn run prettier` for formatting code and docs


Patch:
diff --git a/docs/guides/auto-generating-selectors.md b/docs/guides/auto-generating-selectors.md
index 27f5320..fe30662 100644
--- a/docs/guides/auto-generating-selectors.md
+++ b/docs/guides/auto-generating-selectors.md
@@ -14,13 +14,13 @@ However, writing these could be tedious. If that is the case for you, you can au
 ## create the following function: `createSelectors`
 
 ```typescript
-import { State, StoreApi, UseBoundStore } from 'zustand'
+import { StoreApi, UseBoundStore } from 'zustand'
 
 type WithSelectors<S> = S extends { getState: () => infer T }
   ? S & { use: { [K in keyof T]: () => T[K] } }
   : never
 
-const createSelectors = <S extends UseBoundStore<StoreApi<State>>>(
+const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
   _store: S
 ) => {
   let store = _store as WithSelectors<typeof _store>
diff --git a/docs/integrations/3rd-party-libraries.md b/docs/integrations/3rd-party-libraries.md
index 589437f..e9a3565 100644
--- a/docs/integrations/3rd-party-libraries.md
+++ b/docs/integrations/3rd-party-libraries.md
@@ -11,11 +11,13 @@ Zustand provides bear necessities for state management which is great for most p
 - [@dhmk/zustand-lens](https://github.com/dhmk083/dhmk-zustand-lens) - Lens support for zustand
 - [@liveblocks/zustand](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-zustand) - Liveblocks middleware to make your application multiplayer.
 - [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) - Auto generate zustand hooks for you store with Typescript support
+- [derive-zustand](https://github.com/dai-shi/derive-zustand) - A function to create a derived Zustand store from stores
 - [geschichte](https://github.com/BowlingX/geschichte) - zustand and immer based hook to manage query parameters
 - [mobz](https://github.com/2A5F/Mobz) - zustand style mobx api
 - [shared-zustand](https://github.com/Tom-Julux/shared-zustand) - cross-tab state sharing for zustand
 - [simple-zustand-devtools](https://github.com/beerose/simple-zustand-devtools) - Inspect your zustand store in React DevTools 🐻⚛️
 - [solid-zustand](https://github.com/wobsoriano/solid-zustand) - Zustand state management for Solid.
+- [use-zustand](https://github.com/dai-shi/use-zustand) - Another custom hook to use Zustand vanilla store
 - [vue-zustand](https://github.com/wobsoriano/vue-zustand) - State-management solution for Vue based on Zustand.
 - [zoov](https://github.com/InfiniteXyy/zoov) - Use 🐻 Zustand with Module-like api
 - [zundo](https://github.com/charkour/zundo) - 🍜 enable time-travel in your apps. undo/redo middleware for zustand
diff --git a/rollup.config.js b/rollup.config.js
index cd69816..b1e5e38 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -75,10 +75,16 @@ function createESMConfig(input, output) {
   }
 }
 
-function createCommonJSConfig(input, output) {
+function createCommonJSConfig(input, output, options) {
   return {
     input,
-    output: { file: `${output}.js`, format: 'cjs' },
+    output: {
+      file: `${output}.js`,
+      format: 'cjs',
+      outro: options.addModuleExport
+        ? 'module.exports = exports.default; Object.assign(exports.default, exports);'
+        : '',
+    },
     external,
     plugins: [
       alias({
@@ -166,7 +172,9 @@ module.exports = function (args) {
   }
   return [
     ...(c === 'index' ? [createDeclarationConfig(`src/${c}.ts`, 'dist')] : []),
-    createCommonJSConfig(`src/${c}.ts`, `dist/${c}`),
+    createCommonJSConfig(`src/${c}.ts`, `dist/${c}`, {
+      addModuleExport: c === 'index',
+    }),
     createESMConfig(`src/${c}.ts`, `dist/esm/${c}.js`),
     createESMConfig(`src/${c}.ts`, `dist/esm/${c}.mjs`),
     createUMDConfig(`src/${c}.ts`, `dist/umd/${c}`, 'development'),
diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts
index 1c6641d..6f958e5 100644
--- a/src/middleware/devtools.ts
+++ b/src/middleware/devtools.ts
@@ -160,7 +160,7 @@ const devtoolsImpl: DevtoolsImpl =
     try {
       extensionConnector =
         (enabled ?? __DEV__) && window.__REDUX_DEVTOOLS_EXTENSION__
-    } catch {
+    } catch (e) {
       // ignored
     }
 
diff --git a/src/shallow.ts b/src/shallow.ts
index 6d9ec7c..0e7f59e 100644
--- a/src/shallow.ts
+++ b/src/shallow.ts
@@ -10,6 +10,29 @@ function shallow<T>(objA: T, objB: T) {
   ) {
     return false
   }
+
+  if (objA instanceof Map && objB instanceof Map) {
+    if (objA.size !== objB.size) return false
+
+    for (const [key, value] of objA) {
+      if (!Object.is(value, objB.get(key))) {
+        return false
+      }
+    }
+    return true
+  }
+
+  if (objA instanceof Set && objB instanceof Set) {
+    if (objA.size !== objB.size) return false
+
+    for (const value of objA) {
+      if (!objB.has(value)) {
+        return false
+      }
+    }
+    return true
+  }
+
   const keysA = Object.keys(objA)
   if (keysA.length !== Object.keys(objB).length) {
     return false
diff --git a/src/vanilla.ts b/src/vanilla.ts
index 315b07c..469ac70 100644
--- a/src/vanilla.ts
+++ b/src/vanilla.ts
@@ -60,7 +60,7 @@ const createStoreImpl: CreateStoreImpl = (createState) => {
   let state: TState
   const listeners: Set<Listener> = new Set()
 
-  const setState: SetStateInternal<TState> = (partial, replace) => {
+  const setState: StoreApi<TState>['setState'] = (partial, replace) => {
     // TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
     // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342
     const nextState =
@@ -77,15 +77,15 @@ const createStoreImpl: CreateStoreImpl = (createState) => {
     }
   }
 
-  const getState: () => TState = () => state
+  const getState: StoreApi<TState>['getState'] = () => state
 
-  const subscribe: (listener: Listener) => () => void = (listener) => {
+  const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
     listeners.add(listener)
     // Unsubscribe
     return () => listeners.delete(listener)
   }
 
-  const destroy: () => void = () => listeners.clear()
+  const destroy: StoreApi<TState>['destroy'] = () => listeners.clear()
   const api = { setState, getState, subscribe, destroy }
   state = createState(setState, getState, api)
   return api as any


Test patch:
diff --git a/tests/shallow.test.tsx b/tests/shallow.test.tsx
index 681b217..a60008e 100644
--- a/tests/shallow.test.tsx
+++ b/tests/shallow.test.tsx
@@ -39,6 +39,43 @@ describe('shallow', () => {
     expect(shallow([{ foo: 'bar' }], [{ foo: 'bar', asd: 123 }])).toBe(false)
   })
 
+  it('compares Maps', () => {
+    function createMap<T extends object>(obj: T) {
+      return new Map(Object.entries(obj))
+    }
+
+    expect(
+      shallow(
+        createMap({ foo: 'bar', asd: 123 }),
+        createMap({ foo: 'bar', asd: 123 })
+      )
+    ).toBe(true)
+
+    expect(
+      shallow(
+        createMap({ foo: 'bar', asd: 123 }),
+        createMap({ foo: 'bar', foobar: true })
+      )
+    ).toBe(false)
+
+    expect(
+      shallow(
+        createMap({ foo: 'bar', asd: 123 }),
+        createMap({ foo: 'bar', asd: 123, foobar: true })
+      )
+    ).toBe(false)
+  })
+
+  it('compares Sets', () => {
+    expect(shallow(new Set(['bar', 123]), new Set(['bar', 123]))).toBe(true)
+
+    expect(shallow(new Set(['bar', 123]), new Set(['bar', 2]))).toBe(false)
+
+    expect(shallow(new Set(['bar', 123]), new Set(['bar', 123, true]))).toBe(
+      false
+    )
+  })
+
   it('compares functions', () => {
     function firstFnCompare() {
       return { foo: 'bar' }