diff --git a/lerna.json b/lerna.json index 0f1f17e..b6ede41 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,7 @@ { "packages": [ "./packages/core", + "./packages/immer", "./packages/lens", "./packages/fp-ts", "./packages/react", diff --git a/package.json b/package.json index d8cbd91..e432342 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ "homepage": "https://github.com/raveclassic/frp-ts#readme", "workspaces": [ "./packages/core", - "./packages/lens", + "./packages/immer", "./packages/fp-ts", + "./packages/lens", "./packages/react", "./packages/test-utils", "./packages/utils" diff --git a/packages/immer/.eslintrc.json b/packages/immer/.eslintrc.json new file mode 100644 index 0000000..d3e61a2 --- /dev/null +++ b/packages/immer/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.js"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/immer/jest.config.js b/packages/immer/jest.config.js new file mode 100644 index 0000000..5cc0832 --- /dev/null +++ b/packages/immer/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + testRunner: 'jasmine2', + displayName: 'immer', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/packages/immer', +} diff --git a/packages/immer/package.json b/packages/immer/package.json new file mode 100644 index 0000000..1512301 --- /dev/null +++ b/packages/immer/package.json @@ -0,0 +1,33 @@ +{ + "name": "@frp-ts/immer", + "version": "1.0.0-alpha.15", + "description": "Immer integration package", + "typedocMain": "./src/index.ts", + "author": "raveclassic", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/raveclassic/frp-ts.git" + }, + "bugs": { + "url": "https://github.com/raveclassic/frp-ts/issues" + }, + "homepage": "https://github.com/raveclassic/frp-ts#readme", + "scripts": {}, + "private": false, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "immer": "^9.0.12", + "tslib": "^2.3.1" + }, + "dependencies": { + "@frp-ts/core": "^1.0.0-alpha.15", + "@frp-ts/utils": "^1.0.0-alpha.15" + }, + "devDependencies": { + "immer": "^9.0.12", + "tslib": "^2.3.1" + } +} diff --git a/packages/immer/project.json b/packages/immer/project.json new file mode 100644 index 0000000..dff18ac --- /dev/null +++ b/packages/immer/project.json @@ -0,0 +1,41 @@ +{ + "root": "packages/immer", + "sourceRoot": "packages/immer/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/immer", + "main": "packages/immer/src/index.ts", + "tsConfig": "packages/immer/tsconfig.lib.json", + "assets": ["packages/immer/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/immer/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/packages/immer"], + "options": { + "jestConfig": "packages/immer/jest.config.js", + "passWithNoTests": true, + "codeCoverage": true + } + }, + "deploy": { + "executor": "ngx-deploy-npm:deploy", + "options": { + "access": "public", + "noBuild": true + } + } + }, + "tags": [] +} diff --git a/packages/immer/src/immer.spec.ts b/packages/immer/src/immer.spec.ts new file mode 100644 index 0000000..d379024 --- /dev/null +++ b/packages/immer/src/immer.spec.ts @@ -0,0 +1,67 @@ +import { newAtom } from '@frp-ts/core' +import { produceMany } from './immer' +import { produce } from 'immer' + +describe('immer', () => { + describe('direct integration', () => { + it('updates the state with "produce"', () => { + interface State { + readonly foo: number + } + const state = newAtom({ foo: 0 }) + state.modify( + produce((state) => { + state.foo++ + }), + ) + expect(state.get()).toEqual({ foo: 1 }) + }) + }) + describe('produceMany', () => { + it('updates nested values', () => { + interface State { + readonly foo: number + } + const state = newAtom({ foo: 0 }) + const methods = produceMany(state, { + inc: () => (state) => { + state.foo += 1 + }, + }) + methods.inc() + expect(state.get().foo).toEqual(1) + }) + }) + describe('stress test', () => { + it('runs', () => { + interface Dog { + name: string + } + interface House { + dog: Dog + } + interface AppState { + house: House + } + + const initialState: AppState = { house: { dog: { name: 'Fido' } } } + const storeState = newAtom(initialState) + const storeMethods = produceMany(storeState, { + renameTheDog: (newName: string) => (state) => { + state.house.dog.name = newName + }, + }) + + const store = { ...storeState, ...storeMethods } + + store.renameTheDog('Odif') + expect(store.get()).toEqual({ + house: { + dog: { + name: 'Odif', + }, + }, + }) + }) + }) +}) diff --git a/packages/immer/src/immer.ts b/packages/immer/src/immer.ts new file mode 100644 index 0000000..73492f4 --- /dev/null +++ b/packages/immer/src/immer.ts @@ -0,0 +1,23 @@ +import { Atom } from '@frp-ts/core' +import { mapRecord } from '@frp-ts/utils' +import immer, { Draft } from 'immer' + +interface Recipe { + // any is needed due to variance + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (...args: readonly any[]): (draft: Draft) => void +} + +export function produceMany>>( + input: Atom, + recipes: Recipes, +): { + readonly [Name in keyof Recipes]: (...args: Parameters) => void +} { + return mapRecord( + recipes, + (recipe) => + (...args) => + input.set(immer(input.get(), recipe(...args))), + ) +} diff --git a/packages/immer/src/index.ts b/packages/immer/src/index.ts new file mode 100644 index 0000000..a6a07cb --- /dev/null +++ b/packages/immer/src/index.ts @@ -0,0 +1 @@ +export { produceMany } from './immer' diff --git a/packages/immer/tsconfig.json b/packages/immer/tsconfig.json new file mode 100644 index 0000000..8b2ad5a --- /dev/null +++ b/packages/immer/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": ["."], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/immer/tsconfig.lib.json b/packages/immer/tsconfig.lib.json new file mode 100644 index 0000000..a8b9431 --- /dev/null +++ b/packages/immer/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/immer/tsconfig.spec.json b/packages/immer/tsconfig.spec.json new file mode 100644 index 0000000..a18afb6 --- /dev/null +++ b/packages/immer/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 88a5827..c2ac32c 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1 +1,2 @@ export { memo1, memo2, constVoid, memoMany } from './function' +export { recordKeys, mapRecord } from './record' diff --git a/packages/utils/src/record.spec.ts b/packages/utils/src/record.spec.ts new file mode 100644 index 0000000..f31a001 --- /dev/null +++ b/packages/utils/src/record.spec.ts @@ -0,0 +1,14 @@ +import { mapRecord, recordKeys } from './record' + +describe('record', () => { + describe('recordKeys', () => { + it('returns keys a list of keys', () => { + expect(recordKeys({ foo: 'foo', bar: 123 })).toEqual(['foo', 'bar']) + }) + }) + describe('mapObject', () => { + it('maps values of an object', () => { + expect(mapRecord({ foo: 1, bar: 2 }, (value) => `${value}`)).toEqual({ foo: '1', bar: '2' }) + }) + }) +}) diff --git a/packages/utils/src/record.ts b/packages/utils/src/record.ts new file mode 100644 index 0000000..df02d27 --- /dev/null +++ b/packages/utils/src/record.ts @@ -0,0 +1,16 @@ +export function recordKeys>(obj: T): readonly (keyof T & string)[] { + // eslint-disable-next-line no-restricted-syntax + return Object.keys(obj) as never +} + +export function mapRecord, Result>( + input: Input, + f: (value: Input[keyof Input], key: keyof Input) => Result, +): { readonly [Key in keyof Input]: Result } { + // eslint-disable-next-line no-restricted-syntax + const result: Record = {} as never + for (const key of recordKeys(input)) { + result[key] = f(input[key], key) + } + return result +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cde341..3286f63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,19 @@ importers: tslib: 2.3.1 typescript: 4.5.4 + packages/immer: + specifiers: + '@frp-ts/core': ^1.0.0-alpha.15 + '@frp-ts/utils': ^1.0.0-alpha.15 + immer: ^9.0.12 + tslib: ^2.1.0 + dependencies: + '@frp-ts/core': link:../core + '@frp-ts/utils': link:../utils + devDependencies: + immer: 9.0.12 + tslib: 2.3.1 + packages/lens: specifiers: '@frp-ts/core': ^1.0.0-alpha.15 @@ -5046,6 +5059,10 @@ packages: engines: {node: '>= 4'} dev: true + /immer/9.0.12: + resolution: {integrity: sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==} + dev: true + /import-fresh/2.0.0: resolution: {integrity: sha1-2BNVwVYS04bGH53dOSLUMEgipUY=} engines: {node: '>=4'} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a11d24d..f32399c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,8 @@ packages: - packages/core - - packages/lens + - packages/immer - packages/fp-ts + - packages/lens - packages/react - packages/test-utils - packages/utils diff --git a/tsconfig.base.json b/tsconfig.base.json index 2852e17..d592a3d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,6 +16,7 @@ "baseUrl": ".", "paths": { "@frp-ts/core": ["packages/core/src/index.ts"], + "@frp-ts/immer": ["packages/immer/src/index.ts"], "@frp-ts/fp-ts": ["packages/fp-ts/src/index.ts"], "@frp-ts/lens": ["packages/lens/src/index.ts"], "@frp-ts/react": ["packages/react/src/index.ts"], diff --git a/workspace.json b/workspace.json index 294ffa9..f96a893 100644 --- a/workspace.json +++ b/workspace.json @@ -2,6 +2,7 @@ "version": 2, "projects": { "core": "packages/core", + "immer": "packages/immer", "fp-ts": "packages/fp-ts", "lens": "packages/lens", "react": "packages/react",