From a491ede2a7b982e66d459314ce20af45e0da4885 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 14:18:58 +0530 Subject: [PATCH 01/10] Setup plugins --- examples/remix/package.json | 14 ----- lib/r18gs/src/create.ts | 61 +++++++++++++++++++++ lib/r18gs/src/index.ts | 3 +- lib/r18gs/src/use-rgs.ts | 59 +++----------------- lib/r18gs/src/utils.ts | 105 ++++++++++++++++++++++++++++++++++++ lib/r18gs/tsup.config.ts | 2 +- 6 files changed, 174 insertions(+), 70 deletions(-) create mode 100644 lib/r18gs/src/create.ts create mode 100644 lib/r18gs/src/utils.ts diff --git a/examples/remix/package.json b/examples/remix/package.json index 96539b22..a76d13e7 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -33,19 +33,5 @@ }, "engines": { "node": ">=18.0.0" - }, - "pnpm": { - "packageExtensions": { - "@remix-run/dev": { - "peerDependencies": { - "react-dom": "18.2.0" - } - }, - "@remix-run/serve": { - "peerDependencies": { - "react-dom": "18.2.0" - } - } - } } } diff --git a/lib/r18gs/src/create.ts b/lib/r18gs/src/create.ts new file mode 100644 index 00000000..3a5042ff --- /dev/null +++ b/lib/r18gs/src/create.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style -- as ! operator is forbidden by eslint*/ +import { createHook, globalRGS, initWithPlugins } from "./utils"; + +import type { Plugin, SetStateAction } from "./utils"; + +/** + * Use this hook similar to `useState` hook. + * The difference is that you need to pass a + * unique key - unique across the app to make + * this state accessible to all client components. + * + * @example + * ```tsx + * const [state, setState] = useRGS("counter", 1); + * ``` + * + * @param key - Unique key to identify the store. + * @param value - Initial value of the store. + * @param serverValue - Server value of the store. + * @param plugins - Plugins to be applied to the store. + * @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state. + */ +export function useRGSWithPlugins( + key: string, + value?: T, + serverValue?: T, + plugins?: Plugin[], +): [T, SetStateAction] { + if (!globalRGS[key]) initWithPlugins(key, value, serverValue, plugins); + return createHook(key); +} + +/** + * Use this hook similar to `useState` hook. + * The difference is that you need to pass a + * unique key - unique across the app to make + * this state accessible to all client components. + * + * @example + * ```tsx + * // in hook file + * export const useRGS = create(key, value, serverValue, plugins); + * + * // in component file + * const [state, setState] = useRGS(); + * ``` + * + * @param key - Unique key to identify the store. + * @param value - Initial value of the store. + * @param serverValue - Server value of the store. + * @param plugins - Plugins to be applied to the store. + * @returns - A hook funciton that returns a tuple (Ordered sequance of values) containing the state and a function to set the state. + */ +export function create( + key: string, + value?: T, + serverValue?: T, + plugins?: Plugin[], +): () => [T, SetStateAction] { + return () => useRGSWithPlugins(key, value, serverValue, plugins); +} diff --git a/lib/r18gs/src/index.ts b/lib/r18gs/src/index.ts index 06fe8d72..3d70ae81 100644 --- a/lib/r18gs/src/index.ts +++ b/lib/r18gs/src/index.ts @@ -1,5 +1,4 @@ import useRGS from "./use-rgs"; +export type { SetterArgType, SetStateAction, Plugin } from "./utils"; export default useRGS; - -export * from "./use-rgs"; diff --git a/lib/r18gs/src/use-rgs.ts b/lib/r18gs/src/use-rgs.ts index 123d5c30..11070222 100644 --- a/lib/r18gs/src/use-rgs.ts +++ b/lib/r18gs/src/use-rgs.ts @@ -1,46 +1,7 @@ /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style -- as ! operator is forbidden by eslint*/ -import { useSyncExternalStore } from "react"; +import { createHook, createSetter, createSubcriber, globalRGS } from "./utils"; -type Listener = () => void; -type Subscriber = (l: Listener) => () => void; -export type SetterArgType = T | ((prevState: T) => T); -export type SetStateAction = (value: SetterArgType) => void; - -/** - * This is a hack to reduce lib size + readability + not encouraging direct access to globalThis - */ -const [VALUE, LISTENERS, SETTER, SUBSCRIBER] = [0, 1, 2, 3]; -type RGS = [unknown, Listener[], SetStateAction, Subscriber]; - -declare global { - // eslint-disable-next-line no-var -- var required for global declaration. - var rgs: Record; -} - -const globalThisForBetterMinification = globalThis; -globalThisForBetterMinification.rgs = {}; -const globalRGS = globalThisForBetterMinification.rgs; - -/** Initialize the named store when invoked for the first time. */ -function init(key: string, value?: T) { - const listeners: Listener[] = []; - /** setter function to set the state. */ - const setter: SetStateAction = val => { - const rgs = globalRGS[key] as RGS; - rgs[VALUE] = val instanceof Function ? val(rgs[VALUE] as T) : val; - (rgs[LISTENERS] as Listener[]).forEach(listener => listener()); - }; - /** subscriber function to subscribe to the store. */ - const subscriber: Subscriber = listener => { - const rgs = globalRGS[key] as RGS; - const listeners = rgs[LISTENERS] as Listener[]; - listeners.push(listener); - return () => { - rgs[LISTENERS] = listeners.filter(l => l !== listener); - }; - }; - globalRGS[key] = [value, listeners, setter as SetStateAction, subscriber]; -} +import type { SetStateAction } from "./utils"; /** * Use this hook similar to `useState` hook. @@ -59,17 +20,9 @@ function init(key: string, value?: T) { * @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state. */ export default function useRGS(key: string, value?: T, serverValue?: T): [T, SetStateAction] { - if (!globalRGS[key]) init(key, value); - - const rgs = globalRGS[key] as RGS; - - /** Function to set the state. */ - const setRGState = rgs[SETTER] as SetStateAction; - /** Function to get snapshot of the state. */ - const getSnap = () => (rgs[VALUE] ?? value) as T; - /** Function to get server snapshot. Returns server value is provided else the default value. */ - const getServerSnap = () => (serverValue ?? rgs[VALUE] ?? value) as T; + /** Initialize the named store when invoked for the first time. */ + if (!globalRGS[key]) + globalRGS[key] = [value, serverValue, [], createSetter(key), createSubcriber(key)]; - const val = useSyncExternalStore(rgs[SUBSCRIBER] as Subscriber, getSnap, getServerSnap); - return [val, setRGState]; + return createHook(key); } diff --git a/lib/r18gs/src/utils.ts b/lib/r18gs/src/utils.ts new file mode 100644 index 00000000..4e6dc190 --- /dev/null +++ b/lib/r18gs/src/utils.ts @@ -0,0 +1,105 @@ +import { useSyncExternalStore } from "react"; + +type Listener = () => void; +type Subscriber = (l: Listener) => () => void; + +export type SetterArgType = T | ((prevState: T) => T); +export type SetStateAction = (value: SetterArgType) => void; + +/** + * This is a hack to reduce lib size + readability + not encouraging direct access to globalThis + */ +const [VALUE, SERVER_VALUE, LISTENERS, SETTER, SUBSCRIBER] = [0, 1, 2, 3, 4]; +type RGS = [unknown, unknown, Listener[], SetStateAction, Subscriber]; + +declare global { + // eslint-disable-next-line no-var -- var required for global declaration. + var rgs: Record; +} + +const globalThisForBetterMinification = globalThis; +globalThisForBetterMinification.rgs = {}; +export const globalRGS = globalThisForBetterMinification.rgs; + +/** trigger all listeners */ +function triggerListeners(rgs: RGS) { + (rgs[LISTENERS] as Listener[]).forEach(listener => listener()); +} + +/** craete subscriber function to subscribe to the store. */ +export function createSubcriber(key: string): Subscriber { + return listener => { + const rgs = globalRGS[key] as RGS; + const listeners = rgs[LISTENERS] as Listener[]; + listeners.push(listener); + return () => { + rgs[LISTENERS] = listeners.filter(l => l !== listener); + }; + }; +} + +/** setter function to set the state. */ +export function createSetter(key: string): SetStateAction { + return val => { + const rgs = globalRGS[key] as RGS; + rgs[VALUE] = val instanceof Function ? val(rgs[VALUE] as T) : val; + triggerListeners(rgs); + }; +} + +/** Extract coomon create hook logic to utils */ +export function createHook(key: string): [T, SetStateAction] { + const rgs = globalRGS[key] as RGS; + /** Function to set the state. */ + const setRGState = rgs[SETTER] as SetStateAction; + /** Function to get snapshot of the state. */ + const getSnap = () => rgs[VALUE] as T; + /** Function to get server snapshot. Returns server value is provided else the default value. */ + const getServerSnap = () => (rgs[SERVER_VALUE] ?? rgs[VALUE]) as T; + + const val = useSyncExternalStore(rgs[SUBSCRIBER] as Subscriber, getSnap, getServerSnap); + return [val, setRGState]; +} + +type Mutate = (value?: T, serverValue?: T) => void; + +export type Plugin = { + init?: (key: string, value: T | undefined, serverValue: T | undefined, mutate: Mutate) => void; + onChange?: (key: string, value?: T, serverValue?: T) => void; +}; + +let allExtentionsInitialized = false; +/** Initialize extestions - wait for previous plugins's promise to be resolved before processing next */ +async function initPlugins(key: string, plugins: Plugin[]) { + const rgs = globalRGS[key] as RGS; + /** Mutate function to update the value and server value */ + const mutate: Mutate = (newValue, newServerValue) => { + [rgs[VALUE], rgs[SERVER_VALUE]] = [newValue, newServerValue]; + }; + for (const ext of plugins) { + /** Next plugins initializer will get the new value if updated by previous one */ + await ext.init?.(key, rgs[VALUE] as T, rgs[SERVER_VALUE] as T, mutate); + } + triggerListeners(rgs); + allExtentionsInitialized = true; +} + +/** Initialize the named store when invoked for the first time. */ +export function initWithPlugins( + key: string, + value?: T, + serverValue?: T, + plugins: Plugin[] = [], +) { + const listeners: Listener[] = []; + /** setter function to set the state. */ + const setterWithPlugins: SetStateAction = val => { + /** Do not allow mutating the store before all extentions are initialized */ + if (!allExtentionsInitialized) return; + createSetter(key)(val); + plugins.forEach(plugin => plugin.onChange?.(key, rgs[VALUE] as T, rgs[SERVER_VALUE] as T)); + }; + + globalRGS[key] = [value, serverValue, listeners, setterWithPlugins, createSubcriber(key)]; + initPlugins(key, plugins); +} diff --git a/lib/r18gs/tsup.config.ts b/lib/r18gs/tsup.config.ts index ad68c6f4..188d4174 100644 --- a/lib/r18gs/tsup.config.ts +++ b/lib/r18gs/tsup.config.ts @@ -12,7 +12,7 @@ export default defineConfig(options => ({ { name: "improve-minify", setup(build) { - build.onLoad({ filter: /use-rgs.ts/ }, args => { + build.onLoad({ filter: /utils.ts/ }, args => { let contents = fs.readFileSync(args.path, "utf8"); const lines = contents.split("\n"); const hackLine = lines.find(line => line.startsWith("const [VALUE,")); From 27ecfa215fbafbcd48c0142865c1145d1ced8b15 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 15:29:48 +0530 Subject: [PATCH 02/10] Update readme --- README.md | 29 ++---------------- contributing.md | 81 +++++++++++++++++++++++++++++-------------------- 2 files changed, 51 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 6c6724c8..30f775aa 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ Thus, I decided to create a bare minimum, ultra-light store that creates shared ✅ Works with all build systems/tools/frameworks for React18 -✅ Works for both client side and server side components (be careful, separate states are created for server side and client side. Any changes done on client side will not affect the server components.) - ✅ Doccumented with [Typedoc](https://react18-tools.github.io/react18-global-store) ([Docs](https://react18-tools.github.io/react18-global-store)) ✅ Next.js, Vite and Remix examples @@ -53,7 +51,7 @@ and make this state accessible to all client components. const [state, setState] = useRGS("counter", 1); ``` -You can access the same state across all client side components using unique. +You can access the same state across all client side components using unique key. > It is recommended to store your keys in separate file to avoid typos and unnecessary conflicts. @@ -106,30 +104,9 @@ export default function Counter() { } ``` -## Contribute - -### Build - -To build all apps and packages, run the following command: - -``` -cd react18-global-store -pnpm build -``` - -### Develop - -To develop all apps and packages, run the following command: - -``` -cd react18-global-store -pnpm dev -``` - -Also, please +## Contributing -1. check out discussion for providing any feedback or sugestions. -2. Report any issues or feature requests in issues tab +See [contributing.md](/contributing.md) ### 🤩 Don't forger to star [this repo](https://github.com/mayank1513/react18-global-store)! diff --git a/contributing.md b/contributing.md index 366c01b6..87045151 100644 --- a/contributing.md +++ b/contributing.md @@ -1,41 +1,14 @@ -# Contributing to fork-me +# Contributing to r18gs -## What's inside? - -### Utilities - -This Turborepo has some additional tools already setup for you: +## Contribute -- [TypeScript](https://www.typescriptlang.org/) for static type checking -- [ESLint](https://eslint.org/) for code linting -- [Prettier](https://prettier.io) for code formatting - -### Apps and Packages - -This Turborepo includes the following packages/examples: - -- `nextjs`: a [Next.js](https://nextjs.org/) app -- `vite`: a [Vite.js](https://vitest.dev) app -- `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) -- `tsconfig`: `tsconfig.json`s used throughout the monorepo - -To increase the visibility we have moved core library to lib - -- `fork-me`: a React component library shared by both `nextjs` and `vite` examples lives in `./lib/fork-me` - -Each package/example is 100% [TypeScript](https://www.typescriptlang.org/). - -## Automatic file generation - -- just run `yarn turbo gen` and follow the propts to auto generate your new component with test file and dependency linking -- follow best practices automatically +Once you have cloned the repo. `cd` to the repo directory and use following commands. ### Build To build all apps and packages, run the following command: ```bash -cd r18gs pnpm build ``` @@ -44,7 +17,6 @@ pnpm build To develop all apps and packages, run the following command: ```bash -cd r18gs pnpm dev ``` @@ -53,7 +25,6 @@ pnpm dev To run unit tests, run the following command: ```bash -cd r18gs pnpm test ``` @@ -62,7 +33,6 @@ pnpm test Before creating PR make sure lint is passing and also run formatter to properly format the code. ```bash -cd r18gs pnpm lint ``` @@ -72,6 +42,49 @@ and pnpm format ``` +You can also contribute by + +1. Sponsoring +2. Check out discussion for providing any feedback or sugestions. +3. Report any issues or feature requests in issues tab + +## What's inside? + +### Utilities + +This Turborepo has some additional tools already setup for you: + +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Prettier](https://prettier.io) for code formatting + +### Library, Examples and Packages + +This Turborepo includes the following packages/examples: + +#### React18 Global Store library + +You will find the core library code inside lib/r18gs + +#### Examples (/examples) + +- `nextjs`: a [Next.js](https://nextjs.org/) app +- `vite`: a [Vite.js](https://vitest.dev) app +- `remix`: a [Remix](https://remix.run/) app + +#### Packages (/packages) + +- `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) +- `tsconfig`: `tsconfig.json`s used throughout the monorepo +- `shared-ui`: An internal UI package for shared UI code + +Each package/example is 100% [TypeScript](https://www.typescriptlang.org/). + +## Automatic file generation + +- just run `yarn turbo gen` and follow the propts to auto generate your new component with test file and dependency linking +- follow best practices automatically + ## Useful Links Learn more about Turborepo and Next.js: @@ -89,4 +102,6 @@ Learn more about Turborepo and Next.js:
+### 🤩 Don't forger to star [this repo](https://github.com/mayank1513/react18-global-store)! +

with 💖 by Mayank Kumar Chaudhari

From 81b9d3f995bf63d3968b076b14d74df34a20f65b Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 15:50:57 +0530 Subject: [PATCH 03/10] Changesets --- .changeset/eighty-readers-repeat.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/eighty-readers-repeat.md diff --git a/.changeset/eighty-readers-repeat.md b/.changeset/eighty-readers-repeat.md new file mode 100644 index 00000000..05e0cf9a --- /dev/null +++ b/.changeset/eighty-readers-repeat.md @@ -0,0 +1,6 @@ +--- +"r18gs": minor +"remix-example": patch +--- + +Make the library extensible with plugins From d2fed4412d77a748eccd640bc961ae3f038f94f3 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 15:51:22 +0530 Subject: [PATCH 04/10] Update workflows --- .github/workflows/publish.yml | 26 ++++----- lib/r18gs/package.json | 12 ++-- lib/r18gs/scope.js | 23 -------- lib/r18gs/src/plugins/persist-and-sync.ts | 22 ++++++++ lib/r18gs/touchup.js | 69 ----------------------- turbo.json | 3 +- 6 files changed, 43 insertions(+), 112 deletions(-) delete mode 100644 lib/r18gs/scope.js create mode 100644 lib/r18gs/src/plugins/persist-and-sync.ts delete mode 100644 lib/r18gs/touchup.js diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 80bcefbd..d6c6947f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,11 +22,11 @@ jobs: working-directory: ./lib/r18gs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org @@ -35,30 +35,26 @@ jobs: # fail and not publish if any of the unit tests are failing - name: Test run: pnpm test - - name: Create release and publish to NPM + - name: publish to NPM run: pnpm build && pnpm publish-package - # continue on error to publish scoped package name <- by default repo is setup for a non-scoped + scoped package name - continue-on-error: true env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} TOKEN: ${{ secrets.GITHUB_TOKEN }} OWNER: ${{ github.event.repository.owner.login }} REPO: ${{ github.event.repository.name }} - - name: Publish Scoped package to NPM - # continue on error - expecing npm to trow error if scoping is done twice - continue-on-error: true - run: cd dist && node ../scope.js && npm publish --provenance --access public && cd .. + - name: Create GitHub release + run: | + v=$(node.exe -p -e "require('./package.json').version") + gh release create $v --generate-notes --latest -F CHANGELOG.md --title "Release $v" env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - TOKEN: ${{ secrets.GITHUB_TOKEN }} - OWNER: ${{ github.event.repository.owner.login }} - REPO: ${{ github.event.repository.name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish package with long name for better SEO + - name: Publish canonical packages continue-on-error: true run: | - cd dist + sed -i -e "s/.*name.*/\t\"name\": \"@mayank1513\/r18gs\",/" package.json + npm publish --provenance --access public sed -i -e "s/.*name.*/\t\"name\": \"react18-global-store\",/" package.json npm publish --provenance --access public sed -i -e "s/.*name.*/\t\"name\": \"react18-store\",/" package.json diff --git a/lib/r18gs/package.json b/lib/r18gs/package.json index 4860f34d..f8041623 100644 --- a/lib/r18gs/package.json +++ b/lib/r18gs/package.json @@ -4,8 +4,12 @@ "private": false, "version": "0.1.4", "description": "A simple yet elegant, light weight, react18 global store to replace Zustand for better tree shaking.", - "main": "./index.ts", - "types": "./index.ts", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "files": [ + "dist/**" + ], "repository": { "type": "git", "url": "git+https://github.com/react18-tools/react18-global-store.git" @@ -18,9 +22,9 @@ "license": "MIT", "scripts": { "test": "vitest run --coverage", - "build": "tsup src && tsc -p tsconfig-build.json && node touchup.js", + "build": "tsup src && tsc -p tsconfig-build.json", "doc": "cp ../../README.md . && typedoc", - "publish-package": "cd dist && npm publish --provenance --access public", + "publish-package": "cp ../../README.md . && npm publish --provenance --access public", "lint": "eslint ." }, "devDependencies": { diff --git a/lib/r18gs/scope.js b/lib/r18gs/scope.js deleted file mode 100644 index e9e42268..00000000 --- a/lib/r18gs/scope.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable -- no need - external file */ -"use strict"; - -const fs = require("node:fs"); -const path = require("node:path"); - -const owner = "mayank1513"; -const packageJson = require(path.resolve(process.cwd(), "package.json")); -const ref = packageJson.name; -if (!ref.startsWith(`@${owner}`)) { - packageJson.name = `@${owner}/${packageJson.name}`; - fs.writeFileSync( - path.resolve(process.cwd(), "package.json"), - JSON.stringify(packageJson, null, 2), - ); - const readMePath = path.resolve(process.cwd(), "README.md"); - let readMe = fs.readFileSync(readMePath, { encoding: "utf8" }); - const tmp = "!--|--!"; - readMe = readMe.replace(new RegExp(`$${owner}/${ref}`, "g"), tmp); - readMe = readMe.replace(new RegExp(ref, "g"), packageJson.name); - readMe = readMe.replace(new RegExp(tmp, "g"), `$${owner}/${ref}`); - fs.writeFileSync(readMePath, readMe); -} diff --git a/lib/r18gs/src/plugins/persist-and-sync.ts b/lib/r18gs/src/plugins/persist-and-sync.ts new file mode 100644 index 00000000..ea8addb3 --- /dev/null +++ b/lib/r18gs/src/plugins/persist-and-sync.ts @@ -0,0 +1,22 @@ +import { Plugin } from ".."; + +const persistAndSyncExtension: Plugin = { + init(key, value, _, mutate) { + if (window !== undefined && localStorage !== undefined) { + const persistedValue = localStorage.getItem(key); + if (persistedValue) { + mutate(JSON.parse(persistedValue).val); + } + addEventListener("storage", e => { + if (e.key === key && e.newValue) { + mutate(JSON.parse(e.newValue).val); + } + }); + } + }, + onChange(key, value) { + if (window !== undefined && localStorage !== undefined) { + localStorage.setItem(key, JSON.stringify({ val: value })); + } + }, +}; diff --git a/lib/r18gs/touchup.js b/lib/r18gs/touchup.js deleted file mode 100644 index 682a7528..00000000 --- a/lib/r18gs/touchup.js +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable -- no need - external file */ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const packageJson = require(path.resolve(__dirname, "package.json")); -if (process.env.TOKEN) { - const { Octokit } = require("octokit"); - // Octokit.js - // https://github.com/octokit/core.js#readme - const octokit = new Octokit({ - auth: process.env.TOKEN, - }); - - const octoOptions = { - owner: process.env.OWNER, - repo: process.env.REPO, - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - }; - const tag_name = `v${packageJson.version}`; - const name = `Release ${tag_name}`; - /** Create a release */ - octokit.request("POST /repos/{owner}/{repo}/releases", { - ...octoOptions, - tag_name, - target_commitish: "main", - name, - draft: false, - prerelease: false, - generate_release_notes: true, - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - }); -} - -packageJson.main = "index.js"; -packageJson.types = "index.d.ts"; -packageJson.module = "index.mjs"; - -fs.writeFileSync( - path.resolve(__dirname, "dist", "package.json"), - JSON.stringify(packageJson, null, 2), -); - -fs.copyFileSync( - path.resolve(__dirname, "..", "..", "README.md"), - path.resolve(__dirname, "dist", "README.md"), -); - -fs.copyFileSync( - path.resolve(__dirname, "CHANGELOG.md"), - path.resolve(__dirname, "dist", "CHANGELOG.md"), -); - -const dirs = [path.resolve(__dirname, "dist")]; - -while (dirs.length) { - const dir = dirs.shift(); - fs.readdirSync(dir).forEach(f => { - const f1 = path.resolve(dir, f); - if (f.includes(".test.")) fs.unlink(f1, () => {}); - else if (fs.lstatSync(f1).isDirectory()) { - dirs.push(f1); - } - }); -} diff --git a/turbo.json b/turbo.json index ac8b7ba9..da3f5320 100644 --- a/turbo.json +++ b/turbo.json @@ -3,7 +3,8 @@ "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { - "outputs": [".next/**", "!.next/cache/**"] + "outputs": [".next/**", "!.next/cache/**"], + "dependsOn": ["^build"] }, "lint": {}, "test": {}, From b7d1e0d0d56febafa422d79fcb4c529cd338809b Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 15:51:57 +0530 Subject: [PATCH 05/10] Update workflows --- .github/workflows/docs.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5b408f87..8c2a6b39 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,11 +13,11 @@ jobs: packages: write contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: registry-url: https://registry.npmjs.org node-version: 20 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c83f11f1..15e4a2ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,8 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 20 - run: npm i -g pnpm && pnpm i @@ -21,7 +21,7 @@ jobs: run: pnpm test - name: Upload coverage reports to Codecov continue-on-error: true - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: directory: ./lib/r18gs token: ${{ secrets.CODECOV_TOKEN }} From a68d2bbb206011bccb8fc2e9de79fdcf6057ce22 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 17:34:32 +0530 Subject: [PATCH 06/10] Add persist plugin --- lib/r18gs/src/plugins/persist-and-sync.ts | 34 +++++++++-------- lib/r18gs/src/utils.ts | 16 ++++---- lib/r18gs/src/{create.ts => with-plugins.ts} | 0 lib/r18gs/tests/create.test.tsx | 38 +++++++++++++++++++ lib/r18gs/tests/use-rgs.test.tsx | 6 ++- lib/r18gs/tsconfig-build.json | 2 +- lib/r18gs/tsup.config.ts | 2 +- .../shared-ui/hooks/use-persistant-counter.ts | 6 +++ packages/shared-ui/src/root/hero.tsx | 5 +++ .../shared-ui/src/root/persistant-counter.tsx | 22 +++++++++++ turbo.json | 7 +++- 11 files changed, 110 insertions(+), 28 deletions(-) rename lib/r18gs/src/{create.ts => with-plugins.ts} (100%) create mode 100644 lib/r18gs/tests/create.test.tsx create mode 100644 packages/shared-ui/hooks/use-persistant-counter.ts create mode 100644 packages/shared-ui/src/root/persistant-counter.tsx diff --git a/lib/r18gs/src/plugins/persist-and-sync.ts b/lib/r18gs/src/plugins/persist-and-sync.ts index ea8addb3..0c54ac03 100644 --- a/lib/r18gs/src/plugins/persist-and-sync.ts +++ b/lib/r18gs/src/plugins/persist-and-sync.ts @@ -1,22 +1,26 @@ import { Plugin } from ".."; -const persistAndSyncExtension: Plugin = { - init(key, value, _, mutate) { - if (window !== undefined && localStorage !== undefined) { +function persistAndSyncPlugin(): Plugin { + return { + init(key, value, _, mutate) { + if (typeof window == undefined) return; const persistedValue = localStorage.getItem(key); - if (persistedValue) { - mutate(JSON.parse(persistedValue).val); - } + const newVal = JSON.parse(persistedValue || "{}").val; + if (newVal) mutate(newVal); + addEventListener("storage", e => { if (e.key === key && e.newValue) { - mutate(JSON.parse(e.newValue).val); + const newVal = JSON.parse(e.newValue).val; + if (newVal !== undefined) mutate(newVal); } }); - } - }, - onChange(key, value) { - if (window !== undefined && localStorage !== undefined) { - localStorage.setItem(key, JSON.stringify({ val: value })); - } - }, -}; + }, + onChange(key, value) { + if (typeof window !== undefined) { + localStorage.setItem(key, JSON.stringify({ val: value })); + } + }, + }; +} + +export default persistAndSyncPlugin; diff --git a/lib/r18gs/src/utils.ts b/lib/r18gs/src/utils.ts index 4e6dc190..0adcd1a0 100644 --- a/lib/r18gs/src/utils.ts +++ b/lib/r18gs/src/utils.ts @@ -74,13 +74,14 @@ async function initPlugins(key: string, plugins: Plugin[]) { const rgs = globalRGS[key] as RGS; /** Mutate function to update the value and server value */ const mutate: Mutate = (newValue, newServerValue) => { - [rgs[VALUE], rgs[SERVER_VALUE]] = [newValue, newServerValue]; + rgs[VALUE] = newValue; + rgs[SERVER_VALUE] = newServerValue; + triggerListeners(rgs); }; - for (const ext of plugins) { + for (const plugin of plugins) { /** Next plugins initializer will get the new value if updated by previous one */ - await ext.init?.(key, rgs[VALUE] as T, rgs[SERVER_VALUE] as T, mutate); + await plugin.init?.(key, rgs[VALUE] as T, rgs[SERVER_VALUE] as T, mutate); } - triggerListeners(rgs); allExtentionsInitialized = true; } @@ -91,15 +92,16 @@ export function initWithPlugins( serverValue?: T, plugins: Plugin[] = [], ) { - const listeners: Listener[] = []; /** setter function to set the state. */ const setterWithPlugins: SetStateAction = val => { /** Do not allow mutating the store before all extentions are initialized */ if (!allExtentionsInitialized) return; - createSetter(key)(val); + const rgs = globalRGS[key] as RGS; + rgs[VALUE] = val instanceof Function ? val(rgs[VALUE] as T) : val; + triggerListeners(rgs); plugins.forEach(plugin => plugin.onChange?.(key, rgs[VALUE] as T, rgs[SERVER_VALUE] as T)); }; - globalRGS[key] = [value, serverValue, listeners, setterWithPlugins, createSubcriber(key)]; + globalRGS[key] = [value, serverValue, [], setterWithPlugins, createSubcriber(key)]; initPlugins(key, plugins); } diff --git a/lib/r18gs/src/create.ts b/lib/r18gs/src/with-plugins.ts similarity index 100% rename from lib/r18gs/src/create.ts rename to lib/r18gs/src/with-plugins.ts diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx new file mode 100644 index 00000000..23c6d50a --- /dev/null +++ b/lib/r18gs/tests/create.test.tsx @@ -0,0 +1,38 @@ +import { describe, test } from "vitest"; +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { create } from "../src/with-plugins"; +import persistAndSyncPlugin from "../src/plugins/persist-and-sync"; +import { ChangeEvent, useCallback } from "react"; + +const COUNTER_RGS_KEY = "count"; + +const useMyRGS = create(COUNTER_RGS_KEY, 0, 0, [persistAndSyncPlugin()]); + +function Component1() { + const [count, setCount] = useMyRGS(); + const handleChange = useCallback( + (e: ChangeEvent) => { + setCount(parseInt(e.target.value)); + }, + [setCount], + ); + return ( +
+ +
+ ); +} + +function Component2() { + const [count] = useMyRGS(); + return

{count}

; +} + +describe("React18GlobalStore", () => { + test("check state update to multiple components", async ({ expect }) => { + render(); + render(); + await act(() => fireEvent.input(screen.getByTestId("input"), { target: { value: 5 } })); + expect(screen.getByTestId("display").textContent).toBe("5"); + }); +}); diff --git a/lib/r18gs/tests/use-rgs.test.tsx b/lib/r18gs/tests/use-rgs.test.tsx index afd0de1c..ecd7751e 100644 --- a/lib/r18gs/tests/use-rgs.test.tsx +++ b/lib/r18gs/tests/use-rgs.test.tsx @@ -3,8 +3,10 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import useRGS from "../src"; import { ChangeEvent, useCallback } from "react"; +const COUNT_RGS_KEY = "count"; + function Component1() { - const [count, setCount] = useRGS("count", 0); + const [count, setCount] = useRGS(COUNT_RGS_KEY, 0); const handleChange = useCallback( (e: ChangeEvent) => { setCount(parseInt(e.target.value)); @@ -19,7 +21,7 @@ function Component1() { } function Component2() { - const [count] = useRGS("count"); + const [count] = useRGS(COUNT_RGS_KEY); return

{count}

; } diff --git a/lib/r18gs/tsconfig-build.json b/lib/r18gs/tsconfig-build.json index 3785799b..fd87a4f2 100644 --- a/lib/r18gs/tsconfig-build.json +++ b/lib/r18gs/tsconfig-build.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "dist", "emitDeclarationOnly": true, - "sourceMap": false + "declarationMap": false }, "include": ["src"], "exclude": ["dist", "build", "node_modules", "**/*.test.*", "**/*.spec.*"] diff --git a/lib/r18gs/tsup.config.ts b/lib/r18gs/tsup.config.ts index 188d4174..7eca4d05 100644 --- a/lib/r18gs/tsup.config.ts +++ b/lib/r18gs/tsup.config.ts @@ -30,7 +30,7 @@ export default defineConfig(options => ({ .split(",") .map((t, i) => ({ token: t.trim(), i })); - tokens.sort((a, b) => a.token.length - b.token.length); + tokens.sort((a, b) => b.token.length - a.token.length); for (const t of tokens) { contents = contents.replace(new RegExp(`${t.token}`, "g"), t.i + ""); diff --git a/packages/shared-ui/hooks/use-persistant-counter.ts b/packages/shared-ui/hooks/use-persistant-counter.ts new file mode 100644 index 00000000..5852f566 --- /dev/null +++ b/packages/shared-ui/hooks/use-persistant-counter.ts @@ -0,0 +1,6 @@ +import { create } from "r18gs/dist/with-plugins"; +import persistNSyncPlugin from "r18gs/dist/plugins/persist-and-sync"; + +export const useMyPersistantCounterStore = create("persistant-counter", 1, 1, [ + persistNSyncPlugin(), +]); diff --git a/packages/shared-ui/src/root/hero.tsx b/packages/shared-ui/src/root/hero.tsx index d1d37cee..bc8ee57d 100644 --- a/packages/shared-ui/src/root/hero.tsx +++ b/packages/shared-ui/src/root/hero.tsx @@ -2,6 +2,7 @@ import styles from "../root-layout.module.css"; import { Logo } from "../common/logo"; import Counter from "./counter"; import Display from "./display"; +import PersistantCounter from "./persistant-counter"; export function Hero() { return ( @@ -26,6 +27,10 @@ export function Hero() { +

Persistant counter - Also synced with multiple tabs

+
+ +
); diff --git a/packages/shared-ui/src/root/persistant-counter.tsx b/packages/shared-ui/src/root/persistant-counter.tsx new file mode 100644 index 00000000..013f893c --- /dev/null +++ b/packages/shared-ui/src/root/persistant-counter.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { ChangeEvent, useCallback } from "react"; +import { useMyPersistantCounterStore } from "../../hooks/use-persistant-counter"; + +export default function PersistantCounter() { + const [count, setCount] = useMyPersistantCounterStore(); + + const handleChange = useCallback( + (e: ChangeEvent) => { + setCount(parseInt(e.target.value)); + }, + [setCount], + ); + return ( +
+

Persistant Counter

+

Duplicate this tab to see state sharing between tabs in action

+ +
+ ); +} diff --git a/turbo.json b/turbo.json index da3f5320..e9a4620f 100644 --- a/turbo.json +++ b/turbo.json @@ -7,11 +7,14 @@ "dependsOn": ["^build"] }, "lint": {}, - "test": {}, + "test": { + "dependsOn": ["^build"] + }, "doc": {}, "dev": { "cache": false, - "persistent": true + "persistent": true, + "dependsOn": ["^build"] } } } From 36bbfe761c22846d37d4badbbc07f9079177eff4 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 17:50:30 +0530 Subject: [PATCH 07/10] Update tests --- lib/r18gs/tests/create.test.tsx | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/r18gs/tests/create.test.tsx b/lib/r18gs/tests/create.test.tsx index 23c6d50a..72615c0e 100644 --- a/lib/r18gs/tests/create.test.tsx +++ b/lib/r18gs/tests/create.test.tsx @@ -8,6 +8,9 @@ const COUNTER_RGS_KEY = "count"; const useMyRGS = create(COUNTER_RGS_KEY, 0, 0, [persistAndSyncPlugin()]); +const TESTID_INPUT = "in1"; +const TESTID_DISPLAY = "d1"; + function Component1() { const [count, setCount] = useMyRGS(); const handleChange = useCallback( @@ -18,21 +21,41 @@ function Component1() { ); return (
- +
); } function Component2() { const [count] = useMyRGS(); - return

{count}

; + console.log("c2", { count }); + return

{count}

; } describe("React18GlobalStore", () => { test("check state update to multiple components", async ({ expect }) => { render(); render(); - await act(() => fireEvent.input(screen.getByTestId("input"), { target: { value: 5 } })); - expect(screen.getByTestId("display").textContent).toBe("5"); + /** Await and allow for the state to update from localStorate */ + await new Promise(resolve => setTimeout(resolve, 100)); + await act(() => fireEvent.input(screen.getByTestId(TESTID_INPUT), { target: { value: 5 } })); + expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("5"); + expect(JSON.parse(localStorage.getItem(COUNTER_RGS_KEY) ?? "{}").val).toBe(5); + }); + + test("storage event", async ({ expect }) => { + render(); + render(); + /** Await and allow for the state to update from localStorate */ + await new Promise(resolve => setTimeout(resolve, 100)); + await act(() => + window.dispatchEvent( + new StorageEvent("storage", { + key: COUNTER_RGS_KEY, + newValue: JSON.stringify({ val: 15 }), + }), + ), + ); + expect(screen.getByTestId(TESTID_DISPLAY).textContent).toBe("15"); }); }); From 17853a4c622f6e315496a659701ba83609f28aee Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 18:37:39 +0530 Subject: [PATCH 08/10] Fix typeof expressions --- lib/r18gs/src/plugins/persist-and-sync.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/r18gs/src/plugins/persist-and-sync.ts b/lib/r18gs/src/plugins/persist-and-sync.ts index 0c54ac03..c695decb 100644 --- a/lib/r18gs/src/plugins/persist-and-sync.ts +++ b/lib/r18gs/src/plugins/persist-and-sync.ts @@ -3,7 +3,7 @@ import { Plugin } from ".."; function persistAndSyncPlugin(): Plugin { return { init(key, value, _, mutate) { - if (typeof window == undefined) return; + if (typeof window === "undefined") return; const persistedValue = localStorage.getItem(key); const newVal = JSON.parse(persistedValue || "{}").val; if (newVal) mutate(newVal); @@ -16,7 +16,7 @@ function persistAndSyncPlugin(): Plugin { }); }, onChange(key, value) { - if (typeof window !== undefined) { + if (typeof window !== "undefined") { localStorage.setItem(key, JSON.stringify({ val: value })); } }, From 63aea8f7cd318f688c2e02ae4fba98d136e7f26c Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 18:41:25 +0530 Subject: [PATCH 09/10] Update doc comments --- lib/r18gs/src/plugins/persist-and-sync.ts | 5 +++++ packages/shared-ui/src/root/persistant-counter.tsx | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/lib/r18gs/src/plugins/persist-and-sync.ts b/lib/r18gs/src/plugins/persist-and-sync.ts index c695decb..9d04c9cf 100644 --- a/lib/r18gs/src/plugins/persist-and-sync.ts +++ b/lib/r18gs/src/plugins/persist-and-sync.ts @@ -1,5 +1,10 @@ import { Plugin } from ".."; +/** + * A plugin that persists and syncs RGS store between tabs. + * + * @returns A plugin that persists and syncs a value between tabs. + */ function persistAndSyncPlugin(): Plugin { return { init(key, value, _, mutate) { diff --git a/packages/shared-ui/src/root/persistant-counter.tsx b/packages/shared-ui/src/root/persistant-counter.tsx index 013f893c..48ea33c6 100644 --- a/packages/shared-ui/src/root/persistant-counter.tsx +++ b/packages/shared-ui/src/root/persistant-counter.tsx @@ -3,6 +3,11 @@ import { ChangeEvent, useCallback } from "react"; import { useMyPersistantCounterStore } from "../../hooks/use-persistant-counter"; +/** + * Persistant Counter + * + * @returns {JSX.Element} + */ export default function PersistantCounter() { const [count, setCount] = useMyPersistantCounterStore(); From 8cd72ecf8253da6d01b783f37943c9f6ceaa324f Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Mon, 29 Apr 2024 18:44:44 +0530 Subject: [PATCH 10/10] Changelog --- .changeset/eighty-readers-repeat.md | 6 ------ examples/nextjs/CHANGELOG.md | 9 +++++++++ examples/nextjs/package.json | 2 +- examples/remix/CHANGELOG.md | 10 ++++++++++ examples/remix/package.json | 2 +- examples/vite/CHANGELOG.md | 9 +++++++++ examples/vite/package.json | 2 +- lib/r18gs/CHANGELOG.md | 10 ++++++++++ lib/r18gs/package.json | 2 +- 9 files changed, 42 insertions(+), 10 deletions(-) delete mode 100644 .changeset/eighty-readers-repeat.md diff --git a/.changeset/eighty-readers-repeat.md b/.changeset/eighty-readers-repeat.md deleted file mode 100644 index 05e0cf9a..00000000 --- a/.changeset/eighty-readers-repeat.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"r18gs": minor -"remix-example": patch ---- - -Make the library extensible with plugins diff --git a/examples/nextjs/CHANGELOG.md b/examples/nextjs/CHANGELOG.md index 73691cb9..20c63a52 100644 --- a/examples/nextjs/CHANGELOG.md +++ b/examples/nextjs/CHANGELOG.md @@ -1,5 +1,14 @@ # nextjs-example +## 0.0.11 + +### Patch Changes + +- Updated dependencies [81b9d3f] +- Updated dependencies + - r18gs@0.2.0 + - shared-ui@0.0.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 75c45be8..b92f5d3f 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "nextjs-example", - "version": "0.0.10", + "version": "0.0.11", "private": true, "scripts": { "dev": "next dev", diff --git a/examples/remix/CHANGELOG.md b/examples/remix/CHANGELOG.md index 2a2260ce..f9cd871d 100644 --- a/examples/remix/CHANGELOG.md +++ b/examples/remix/CHANGELOG.md @@ -1,5 +1,15 @@ # remix-example +## 0.0.11 + +### Patch Changes + +- 81b9d3f: Make the library extensible with plugins +- Updated dependencies [81b9d3f] +- Updated dependencies + - r18gs@0.2.0 + - shared-ui@0.0.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/remix/package.json b/examples/remix/package.json index a76d13e7..015a2b3c 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -1,6 +1,6 @@ { "name": "remix-example", - "version": "0.0.10", + "version": "0.0.11", "private": true, "sideEffects": false, "type": "module", diff --git a/examples/vite/CHANGELOG.md b/examples/vite/CHANGELOG.md index c01b7cc1..0604d258 100644 --- a/examples/vite/CHANGELOG.md +++ b/examples/vite/CHANGELOG.md @@ -1,5 +1,14 @@ # vite-example +## 0.0.11 + +### Patch Changes + +- Updated dependencies [81b9d3f] +- Updated dependencies + - r18gs@0.2.0 + - shared-ui@0.0.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/vite/package.json b/examples/vite/package.json index c3b4cf88..d1ce5450 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -1,7 +1,7 @@ { "name": "vite-example", "private": true, - "version": "0.0.10", + "version": "0.0.11", "type": "module", "scripts": { "dev": "vite --port 3001", diff --git a/lib/r18gs/CHANGELOG.md b/lib/r18gs/CHANGELOG.md index 170af46d..b0319ffa 100644 --- a/lib/r18gs/CHANGELOG.md +++ b/lib/r18gs/CHANGELOG.md @@ -1,5 +1,15 @@ # r18gs +## 0.2.0 + +### Minor Changes + +- 81b9d3f: Make the library extensible with plugins + +### Patch Changes + +- Add Persist and Sync plugin + ## 0.1.4 ### Patch Changes diff --git a/lib/r18gs/package.json b/lib/r18gs/package.json index f8041623..f579d2f8 100644 --- a/lib/r18gs/package.json +++ b/lib/r18gs/package.json @@ -2,7 +2,7 @@ "name": "r18gs", "author": "Mayank Kumar Chaudhari ", "private": false, - "version": "0.1.4", + "version": "0.2.0", "description": "A simple yet elegant, light weight, react18 global store to replace Zustand for better tree shaking.", "main": "./dist/index.js", "types": "./dist/index.d.ts",