diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..bc8336c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: Publish to NPM + +# publish only when package json has changed - assuming version upgrade +on: + push: + branches: [main] + paths: "packages/persist-and-sync/package.json" + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + packages: write + contents: write + + defaults: + run: + working-directory: ./packages/persist-and-sync + + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-node@v3 + with: + registry-url: https://registry.npmjs.org + node-version: 18 + - run: npm i -g pnpm && pnpm i + name: Install dependencies + # fail and not publish if any of the unit tests are failing + - name: Test + run: pnpm test + - name: Publish to NPM + run: pnpm build && pnpm publish-package + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3158653 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: test + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 8 * * *" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - run: npm i -g pnpm && pnpm i + name: Install dependencies + - name: Run unit tests + run: pnpm test + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + directory: ./packages/persist-and-sync + token: ${{ secrets.CODECOV_TOKEN }} + flags: fork-me + - uses: paambaati/codeclimate-action@v5.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageLocations: ./packages/persist-and-sync/coverage/*.xml:clover diff --git a/README.md b/README.md index 44850a5..f36f524 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# PersistAndSync Zustand Store [![Maintainability](https://api.codeclimate.com/v1/badges/5355eb02cfedc9184e3f/maintainability)](https://codeclimate.com/github/mayank1513/persist-and-sync/maintainability) [![Version](https://img.shields.io/npm/v/persist-and-sync.svg?colorB=green)](https://www.npmjs.com/package/persist-and-sync) [![Downloads](https://img.jsdelivr.com/img.shields.io/npm/dt/persist-and-sync.svg)](https://www.npmjs.com/package/persist-and-sync) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/persist-and-sync) +# PersistAndSync Zustand Store + +[![test](https://github.com/mayank1513/persist-and-sync/actions/workflows/test.yml/badge.svg)](https://github.com/mayank1513/persist-and-sync/actions/workflows/test.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/5355eb02cfedc9184e3f/maintainability)](https://codeclimate.com/github/mayank1513/persist-and-sync/maintainability) [![codecov](https://codecov.io/gh/mayank1513/persist-and-sync/graph/badge.svg)](https://codecov.io/gh/mayank1513/persist-and-sync) [![Version](https://img.shields.io/npm/v/persist-and-sync.svg?colorB=green)](https://www.npmjs.com/package/persist-and-sync) [![Downloads](https://img.jsdelivr.com/img.shields.io/npm/dt/persist-and-sync.svg)](https://www.npmjs.com/package/persist-and-sync) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/persist-and-sync) > Zustand middleware to easily persist and sync Zustand state between tabs / windows / iframes (Same Origin) @@ -79,6 +81,10 @@ use `regExpToIgnore: /^(field1|field2|field3)$/` - [ ] `regExpToInclude` -> once implemented, passing this parameter will sync only matching fields +### 🤩 Don't forger to start [this repo](https://github.com/mayank1513/persist-and-sync)! + +Want handson course for getting started with Turborepo? Check out [React and Next.js with TypeScript](https://mayank-chaudhari.vercel.app/courses/react-and-next-js-with-typescript) and [The Game of Chess with Next.js, React and TypeScrypt](https://www.udemy.com/course/game-of-chess-with-nextjs-react-and-typescrypt/?referralCode=851A28F10B254A8523FE) + ## License Licensed as MIT open source. diff --git a/examples/nextjs/app/Counter.tsx b/examples/nextjs/app/Counter.tsx index f97af5a..4791075 100644 --- a/examples/nextjs/app/Counter.tsx +++ b/examples/nextjs/app/Counter.tsx @@ -1,5 +1,5 @@ "use client"; -import { useMyStore } from "../store"; +import { useMyStore } from "./store"; import styles from "./page.module.css"; interface CounterProps { diff --git a/examples/nextjs/app/store.ts b/examples/nextjs/app/store.ts new file mode 100644 index 0000000..2c09893 --- /dev/null +++ b/examples/nextjs/app/store.ts @@ -0,0 +1,25 @@ +import { create } from "zustand"; +import { persistNSync } from "persist-and-sync"; + +interface MyStoreType { + count: number; + _count: number; + setCount: (c: number) => void; + set_Count: (c: number) => void; +} + +export const useMyStore = create()( + persistNSync( + set => ({ + count: 0, + _count: 0 /** skipped as it matches the regexp provided */, + setCount: count => { + set(state => ({ ...state, count })); + }, + set_Count: _count => { + set(state => ({ ...state, _count })); + }, + }), + { name: "example", regExpToIgnore: /^_/ }, + ), +); diff --git a/package.json b/package.json index 03095b6..05e6ae9 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "build": "turbo run build", "dev": "turbo run dev", "lint": "turbo run lint", + "test": "turbo run test", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,css,scss}\"" }, "devDependencies": { diff --git a/packages/persist-and-sync/__tests__/index.test.ts b/packages/persist-and-sync/__tests__/index.test.ts new file mode 100644 index 0000000..e01506c --- /dev/null +++ b/packages/persist-and-sync/__tests__/index.test.ts @@ -0,0 +1,19 @@ +import { act, cleanup, renderHook } from "@testing-library/react"; + +import { afterEach, describe, test } from "vitest"; +import { useMyStore } from "./store"; + +describe.concurrent("Setting state", () => { + afterEach(cleanup); + test("test initial state", async ({ expect }) => { + const { result } = renderHook(() => useMyStore()); + expect(result.current.count).toBe(0); + }); + + test("test setting state", async ({ expect }) => { + const { result } = renderHook(() => useMyStore()); + act(() => result.current.setCount(5)); + expect(result.current.count).toBe(5); + expect(localStorage.getItem("example")).toBe('{"count":5}'); + }); +}); diff --git a/examples/nextjs/store.ts b/packages/persist-and-sync/__tests__/store.ts similarity index 75% rename from examples/nextjs/store.ts rename to packages/persist-and-sync/__tests__/store.ts index 1e43ff2..444b9ce 100644 --- a/examples/nextjs/store.ts +++ b/packages/persist-and-sync/__tests__/store.ts @@ -1,5 +1,5 @@ -import { create } from "zustand"; -import { persistNSync } from "persist-and-sync"; +import { create } from "../vitest-setup"; +import { persistNSync } from "../src"; type MyStoreType = { count: number; @@ -8,7 +8,7 @@ type MyStoreType = { set_Count: (c: number) => void; }; -export const useMyStore = create()( +export const useMyStore = create( persistNSync( set => ({ count: 0, diff --git a/packages/persist-and-sync/package.json b/packages/persist-and-sync/package.json index f2e5d7e..180b384 100644 --- a/packages/persist-and-sync/package.json +++ b/packages/persist-and-sync/package.json @@ -1,7 +1,7 @@ { "name": "persist-and-sync", "author": "Mayank Kumar Chaudhari ", - "version": "0.1.1", + "version": "0.1.2", "description": "Zustand middleware to easily persist and sync Zustand state between tabs and windows", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -17,15 +17,21 @@ "license": "MIT", "scripts": { "build": "tsc && node createPackageJSON.js", - "publish-package": "cp ../../README.md dist && cd dist && npm publish" + "publish-package": "cp ../../README.md dist && cd dist && npm publish", + "test": "vitest run --coverage" }, "funding": { "type": "github", "url": "https://github.com/sponsors/mayank1513" }, "devDependencies": { + "@testing-library/react": "^14.0.0", "@types/node": "^20.8.6", + "@vitejs/plugin-react": "^4.1.0", + "@vitest/coverage-v8": "^0.34.6", + "jsdom": "^22.1.0", "typescript": "^5.2.2", + "vitest": "^0.34.6", "zustand": "^4.4.3" }, "peerDependencies": { diff --git a/packages/persist-and-sync/vitest-setup.ts b/packages/persist-and-sync/vitest-setup.ts new file mode 100644 index 0000000..6fd7abb --- /dev/null +++ b/packages/persist-and-sync/vitest-setup.ts @@ -0,0 +1,48 @@ +import { act } from "@testing-library/react"; +import { afterEach, vi } from "vitest"; +import type { StateCreator } from "zustand"; +import { create as actualCreate } from "zustand"; + +// a variable to hold reset functions for all stores declared in the app +export const storeResetFns = new Set<() => void>(); + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const create = (createState: StateCreator) => { + const store = actualCreate(createState); + const initialState = store.getState(); + storeResetFns.add(() => store.setState(initialState, true)); + return store; +}; + +afterEach(() => { + act(() => storeResetFns.forEach(resetFn => resetFn())); +}); + +declare global { + var tmp_store: { [key: string]: string }; +} + +globalThis.tmp_store = {}; + +globalThis.localStorage = { + length: Object.keys(tmp_store).length, + clear: () => { + tmp_store = {}; + }, + key: (index: number) => Object.keys(tmp_store)[index], + removeItem: (key: string) => { + delete tmp_store[key]; + }, + setItem: (key: string, item: string) => { + tmp_store[key] = item; + }, + getItem: (key: string) => tmp_store[key], +}; + +function channelMock() {} +channelMock.prototype.onmessage = function () {}; +channelMock.prototype.postMessage = function (data) { + this.onmessage({ data }); +}; +// @ts-ignore +global.BroadcastChannel = channelMock; diff --git a/packages/persist-and-sync/vitest.config.ts b/packages/persist-and-sync/vitest.config.ts new file mode 100644 index 0000000..53344af --- /dev/null +++ b/packages/persist-and-sync/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +// eslint-disable-next-line import/no-default-export -- export default is required for config files +export default defineConfig({ + plugins: [react()], + test: { + environment: "jsdom", + globals: true, + setupFiles: ["vitest-setup.ts"], + coverage: { + reporter: ["text", "json", "clover", "html"], + }, + threads: true, + mockReset: false, + }, +}); diff --git a/turbo.json b/turbo.json index 2c3074d..035a33f 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,7 @@ "persist-and-sync#build": { "cache": false }, + "test": {}, "lint": {}, "dev": { "cache": false,