diff --git a/packages/focal-atom/package.json b/packages/focal-atom/package.json index 9681b27..0691fe3 100644 --- a/packages/focal-atom/package.json +++ b/packages/focal-atom/package.json @@ -3,17 +3,10 @@ "version": "0.10.1", "description": "FRP Atom: observables, immutable data and lenses", "main": "dist/_cjs/src/index.js", - "module": "dist/_esm5/src/index.js", - "es2015": "dist/_esm2015/src/index.js", + "module": "dist/_esm2015/src/index.js", "types": "dist/_cjs/src/index.d.ts", - "sideEffects": [ - "./dist/_cjs/src/lens/json.js", - "./dist/_esm5/src/lens/json.js", - "./dist/_esm2015/src/lens/json.js" - ], "files": [ "dist/_cjs/src/", - "dist/_esm5/src/", "dist/_esm2015/src/" ], "scripts": { @@ -21,9 +14,8 @@ "clean": "rm -rf ./dist", "dev": "tsc -b -w tsconfig.build.cjs.json", "build:cjs": "tsc -b tsconfig.build.cjs.json", - "build:es5": "tsc -b tsconfig.build.es5.json", "build:es2015": "tsc -b tsconfig.build.es2015.json", - "build": "npm run clean && yarn build:cjs && yarn build:es5 && yarn build:es2015 && npm run lint", + "build": "npm run clean && yarn build:cjs && yarn build:es2015 && npm run lint", "test": "jest", "test:watch": "jest --watch", "lint": "eslint './src/**/*.ts*' 'test/**/*.ts*' && tsc --noemit", diff --git a/packages/focal-atom/src/lens/base.ts b/packages/focal-atom/src/lens/base.ts index e779bed..6cd5d8b 100644 --- a/packages/focal-atom/src/lens/base.ts +++ b/packages/focal-atom/src/lens/base.ts @@ -8,7 +8,7 @@ export interface Optic { // @TODO can't optic compose? } -function createModify( +export function createModify( getter: (s: TSource) => T, setter: (v: U, s: TSource) => TSource ) { @@ -17,19 +17,6 @@ function createModify( } } -export namespace Optic { - export function optic( - getter: (s: TSource) => T, - setter: (v: U, s: TSource) => TSource - ): Optic { - return { - get: getter, - set: setter, - modify: createModify(getter, setter) - } - } -} - // @NOTE lens and prism are monomorphic: can't change the type of // focused value on update @@ -51,126 +38,3 @@ export interface Prism extends Optic, T> { compose(next: Lens, U>): Lens compose(next: Prism): Prism } - -export namespace Prism { - export function create( - getter: (s: TSource) => Option, - setter: (v: T, s: TSource) => TSource - ): Prism { - return { - get: getter, - set: setter, - modify: createModify(getter, setter), - - compose(next: Lens | Prism): Prism { - // no runtime dispatch – the implementation works for both - // lens and prism argument - return create( - (s: TSource) => { - const x = getter(s) - return x !== undefined - ? next.get(x) - : undefined - }, - (v: U, s: TSource) => { - const x = getter(s) - return x !== undefined - ? setter(next.set(v, x), s) - : s - } - ) - } - } - } -} - -export namespace Lens { - /** - * Create a lens. - * - * @export - * @template O type of the source - * @template P type of the destination - * @param getter a getter function - * @param setter a setter function - * @returns a lens that operates by given getter and setter - */ - export function create( - getter: (s: TSource) => T, - setter: (v: T, s: TSource) => TSource - ): Lens { - return { - get: getter, - set: setter, - modify: createModify(getter, setter), - - compose(next: Lens): Lens { - return create( - (s: TSource) => next.get(getter(s)), - (v: U, s: TSource) => setter(next.set(v, getter(s)), s) - ) - } - } - } - - /** - * Compose several lenses, where each subsequent lens' state type is the previous - * lens' output type. - * - * You need to explicitly say what will be the type of resulting lens, and you - * need to do it right as there are no guarantees at compile time. - * - * @static - * @template S the resulting lens' state - * @template A the resulting lens' output - */ - export function compose(l: Lens): Lens - - export function compose(l1: Lens, l2: Lens): Lens - - export function compose( - l1: Lens, l2: Lens, l3: Lens - ): Lens - - export function compose( - l1: Lens, l2: Lens, l3: Lens, l4: Lens - ): Lens - - export function compose( - l1: Lens, l2: Lens, l3: Lens, l4: Lens, l5: Lens - ): Lens - - export function compose(...lenses: Lens[]): Lens - - export function compose(...lenses: Lens[]): Lens { - if (lenses.length === 0) { - throw new TypeError('Can not compose zero lenses. You probably want `Lens.identity`.') - } else if (lenses.length === 1) { - return lenses[0] - } else { - let r = lenses[0] - lenses.slice(1).forEach(l => { - r = r.compose(l) - }) - return r as Lens - } - } - - const _identity = create(x => x, (x: any, _: any) => x) - - /** - * The identity lens – a lens that reads and writes the object itself. - */ - export function identity(): Lens { - return _identity as Lens - } - - const _nothing = Prism.create(_ => undefined, (_: any, o: any) => o) - - /** - * A lens that always returns `undefined` on `get` and does no change on `set`. - */ - export function nothing() { - return _nothing as Prism - } -} diff --git a/packages/focal-atom/src/lens/index.ts b/packages/focal-atom/src/lens/index.ts index cb313de..227d57e 100644 --- a/packages/focal-atom/src/lens/index.ts +++ b/packages/focal-atom/src/lens/index.ts @@ -9,27 +9,17 @@ * * @module */ -import { - Lens, Prism, Optic -} from './base' -// This import adds JSON-specific lens functions to the Lens -// namespace, in style of RxJS. -// -// It's probably not the best way (these magic imports in general), -// but so far I haven't found a better way to: -// 1) have everything in a single namespace (for convenient usage) -// and -// 2) have base Lens definitions and JSON Lens definitions in separate -// modules. -// -// But maybe there is a way to avoid this? -import './json' +import type { Lens as LensType, Prism as PrismType, Optic as OpticType } from './base' + +export type Lens = LensType +export type Prism = PrismType +export type Optic = OpticType + +export * as Optic from './optic' +export * as Prism from './prism' +export * as Lens from './lens' export { PropExpr } from './json' - -export { - Lens, Prism, Optic -} diff --git a/packages/focal-atom/src/lens/json.ts b/packages/focal-atom/src/lens/json.ts index 850400c..57c7734 100644 --- a/packages/focal-atom/src/lens/json.ts +++ b/packages/focal-atom/src/lens/json.ts @@ -17,7 +17,8 @@ import { structEq } from './../equals' -import { Lens, Prism } from './base' +import { Lens, Prism } from './' +import { create as createPrism } from './prism' export type PropExpr = (x: O) => P @@ -33,9 +34,9 @@ const PROP_EXPR_RE = new RegExp([ const WALLABY_PROP_EXPR_RE = new RegExp([ '^', 'function', '\\(', '[^), ]+', '\\)', '\\{', '("use strict";)?', - '(\\$_\\$wf\\(\\d+\\);)?', // wallaby.js code coverage compatability (#36) + '(\\$_\\$wf\\(\\d+\\);)?', // wallaby.js code coverage compatibility (#36) 'return\\s', - '(\\$_\\$w\\(\\d+, \\d+\\),\\s)?', // wallaby.js code coverage compatability (#36) + '(\\$_\\$w\\(\\d+, \\d+\\),\\s)?', // wallaby.js code coverage compatibility (#36) '[^\\.]+\\.(\\S+?);?', '\\}', '$' ].join('\\s*')) @@ -104,7 +105,7 @@ export interface KeyImplFor { * * @param k the key to focus on */ -export function keyImpl(k: string): Prism<{ [k: string]: TValue }, TValue> +export function key(k: string): Prism<{ [k: string]: TValue }, TValue> /** * Create a lens focusing on a key of an object. @@ -140,9 +141,9 @@ export function keyImpl(k: string): Prism<{ [k: string]: TValue }, // keyImpl()('someKey') // // Pretty cool! -export function keyImpl(): KeyImplFor +export function key(): KeyImplFor -export function keyImpl(k?: string) { +export function key(k?: string) { return k === undefined // type-safe key ? (k: K): Lens => @@ -178,21 +179,50 @@ function warnPropExprDeprecated(path: string[]) { } } -export function propImpl( +/** + * DEPRECATED: please use Lens.key instead! + * + * Create a lens to an object's property. The argument is a property expression, which + * is a limited form of a getter, with following restrictions: + * - should be a pure function + * - should be a single-expression function (i.e. return immediately) + * - should only access object properties (nested access is OK) + * + * @example + * const obj = { a: { b: 5 } } + * + * const l = Lens.prop((x: typeof obj) => x.a.b) + * + * l.modify(x => x + 1, obj) + * // => { a: { b: 6 } } + * @template TObject type of the object + * @template TProperty type of the property + * @param propExpr property get expression + * @returns a lens to an object's property + */ +export function prop( getter: PropExpr ): Lens { const path = extractPropertyPath(getter as PropExpr) if (DEV_ENV) warnPropExprDeprecated(path) // @TODO can we optimize this? - return Lens.compose(...path.map(keyImpl())) + return Lens.compose(...path.map(key())) } -export function indexImpl(i: number): Prism { +/** + * Create a lens that looks at an element at particular index position + * in an array. + * + * @template TItem type of array elements + * @param i the index + * @returns a lens to an element at particular position in an array + */ +export function index(i: number): Prism { if (i < 0) throw new TypeError(`${i} is not a valid array index, expected >= 0`) - return Prism.create( + return createPrism( (xs: TItem[]) => xs[i] as Option, (v: TItem, xs: TItem[]) => { if (xs.length <= i) { @@ -206,7 +236,13 @@ export function indexImpl(i: number): Prism { ) } -export function withDefaultImpl(defaultValue: T): Lens, T> { +/** + * Create a lens that will show a given default value if the actual + * value is absent (is undefined). + * + * @param defaultValue default value to return + */ +export function withDefault(defaultValue: T): Lens, T> { // @TODO is this cast safe? return Lens.replace(undefined, defaultValue) as Lens, T> } @@ -218,14 +254,21 @@ function choose(getLens: (state: T) => Lens): Lens { ) } -export function replaceImpl(originalValue: T, newValue: T): Lens { +/** + * Create a lens that replaces a given value with a new one. + */ +export function replace(originalValue: T, newValue: T): Lens { return Lens.create( x => structEq(x, originalValue) ? newValue : x, conservatively((y: T) => structEq(y, newValue) ? originalValue : y) ) } -export function findImpl(predicate: (x: T) => boolean): Prism { +/** + * Create a prism that focuses on an array's element that + * satisfies a given predicate. + */ +export function find(predicate: (x: T) => boolean): Prism { return choose((xs: T[]) => { const i = findIndex(xs, predicate) @@ -234,72 +277,3 @@ export function findImpl(predicate: (x: T) => boolean): Prism { : Lens.index(i) }) } - -// augment the base lens module with JSON-specific lens functions. -// @TODO this doesn't look like the best way to do it. we only do it -// for a nice consumer API with all lens function under the same namespace, -// together with the lens type. -declare module './base' { - export namespace Lens { - export let key: typeof keyImpl - - /** - * DEPRECATED: please use Lens.key instead! - * - * Create a lens to an object's property. The argument is a property expression, which - * is a limited form of a getter, with following restrictions: - * - should be a pure function - * - should be a single-expression function (i.e. return immediately) - * - should only access object properties (nested access is OK) - * - * @example - * const obj = { a: { b: 5 } } - * - * const l = Lens.prop((x: typeof obj) => x.a.b) - * - * l.modify(x => x + 1, obj) - * // => { a: { b: 6 } } - * @template TObject type of the object - * @template TProperty type of the property - * @param propExpr property get expression - * @returns a lens to an object's property - */ - export let prop: typeof propImpl - - /** - * Create a lens that looks at an element at particular index position - * in an array. - * - * @template TItem type of array elements - * @param i the index - * @returns a lens to an element at particular position in an array - */ - export let index: typeof indexImpl - - /** - * Create a lens that will show a given default value if the actual - * value is absent (is undefined). - * - * @param defaultValue default value to return - */ - export let withDefault: typeof withDefaultImpl - - /** - * Create a lens that replaces a given value with a new one. - */ - export let replace: typeof replaceImpl - - /** - * Create a prism that focuses on an array's element that - * satisfies a given predicate. - */ - export let find: typeof findImpl - } -} - -Lens.key = keyImpl -Lens.prop = propImpl -Lens.index = indexImpl -Lens.withDefault = withDefaultImpl -Lens.replace = replaceImpl -Lens.find = findImpl diff --git a/packages/focal-atom/src/lens/lens.ts b/packages/focal-atom/src/lens/lens.ts new file mode 100644 index 0000000..14c6d4d --- /dev/null +++ b/packages/focal-atom/src/lens/lens.ts @@ -0,0 +1,93 @@ +import { Lens, Prism, createModify } from './base' +import { create as createPrism } from './prism' + +export { key, prop, index, withDefault, replace, find } from './json' + +/** + * Create a lens. + * + * @export + * @template O type of the source + * @template P type of the destination + * @param getter a getter function + * @param setter a setter function + * @returns a lens that operates by given getter and setter + */ +export function create( + getter: (s: TSource) => T, + setter: (v: T, s: TSource) => TSource +): Lens { + return { + get: getter, + set: setter, + modify: createModify(getter, setter), + + compose(next: Lens): Lens { + return create( + (s: TSource) => next.get(getter(s)), + (v: U, s: TSource) => setter(next.set(v, getter(s)), s) + ) + } + } +} + +/** + * Compose several lenses, where each subsequent lens' state type is the previous + * lens' output type. + * + * You need to explicitly say what will be the type of resulting lens, and you + * need to do it right as there are no guarantees at compile time. + * + * @static + * @template S the resulting lens' state + * @template A the resulting lens' output + */ +export function compose(l: Lens): Lens + +export function compose(l1: Lens, l2: Lens): Lens + +export function compose( + l1: Lens, l2: Lens, l3: Lens +): Lens + +export function compose( + l1: Lens, l2: Lens, l3: Lens, l4: Lens +): Lens + +export function compose( + l1: Lens, l2: Lens, l3: Lens, l4: Lens, l5: Lens +): Lens + +export function compose(...lenses: Lens[]): Lens + +export function compose(...lenses: Lens[]): Lens { + if (lenses.length === 0) { + throw new TypeError('Can not compose zero lenses. You probably want `Lens.identity`.') + } else if (lenses.length === 1) { + return lenses[0] + } else { + let r = lenses[0] + lenses.slice(1).forEach(l => { + r = r.compose(l) + }) + return r as Lens + } +} + +const _identity = create(x => x, (x: any, _: any) => x) + +/** + * The identity lens – a lens that reads and writes the object itself. + */ +export function identity(): Lens { + return _identity as Lens +} + +const _nothing = createPrism(_ => undefined, (_: any, o: any) => o) + +/** + * A lens that always returns `undefined` on `get` and does no change on `set`. + */ +export function nothing() { + return _nothing as Prism +} diff --git a/packages/focal-atom/src/lens/optic.ts b/packages/focal-atom/src/lens/optic.ts new file mode 100644 index 0000000..2acc5e9 --- /dev/null +++ b/packages/focal-atom/src/lens/optic.ts @@ -0,0 +1,12 @@ +import { Optic, createModify } from './base' + +export function optic( + getter: (s: TSource) => T, + setter: (v: U, s: TSource) => TSource +): Optic { + return { + get: getter, + set: setter, + modify: createModify(getter, setter) + } +} diff --git a/packages/focal-atom/src/lens/prism.ts b/packages/focal-atom/src/lens/prism.ts new file mode 100644 index 0000000..a993b18 --- /dev/null +++ b/packages/focal-atom/src/lens/prism.ts @@ -0,0 +1,32 @@ +import { Lens, Prism, createModify } from './base' +import { Option } from './../utils' + +export function create( + getter: (s: TSource) => Option, + setter: (v: T, s: TSource) => TSource +): Prism { + return { + get: getter, + set: setter, + modify: createModify(getter, setter), + + compose(next: Lens | Prism): Prism { + // no runtime dispatch – the implementation works for both + // lens and prism argument + return create( + (s: TSource) => { + const x = getter(s) + return x !== undefined + ? next.get(x) + : undefined + }, + (v: U, s: TSource) => { + const x = getter(s) + return x !== undefined + ? setter(next.set(v, x), s) + : s + } + ) + } + } +} diff --git a/packages/focal-atom/tsconfig.build.cjs.json b/packages/focal-atom/tsconfig.build.cjs.json index 69fb604..dcf4461 100644 --- a/packages/focal-atom/tsconfig.build.cjs.json +++ b/packages/focal-atom/tsconfig.build.cjs.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts*"], + "exclude": ["**/*.test.ts*"], "compilerOptions": { "outDir": "dist/_cjs", "module": "commonjs", diff --git a/packages/focal-atom/tsconfig.build.es2015.json b/packages/focal-atom/tsconfig.build.es2015.json index 34be7d9..ba3d6d6 100644 --- a/packages/focal-atom/tsconfig.build.es2015.json +++ b/packages/focal-atom/tsconfig.build.es2015.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts*"], + "exclude": ["**/*.test.ts*"], "compilerOptions": { "outDir": "dist/_esm2015", "module": "es2015", diff --git a/packages/focal-atom/tsconfig.build.es5.json b/packages/focal-atom/tsconfig.build.es5.json deleted file mode 100644 index 98f2583..0000000 --- a/packages/focal-atom/tsconfig.build.es5.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts*"], - "compilerOptions": { - "outDir": "dist/_esm5", - "module": "commonjs", - "target": "es2015", - } -} diff --git a/packages/focal/package.json b/packages/focal/package.json index 79d3745..a34a86e 100644 --- a/packages/focal/package.json +++ b/packages/focal/package.json @@ -3,20 +3,10 @@ "version": "0.10.1", "description": "FRP UI with React, observables, immutable data and lenses", "main": "dist/_cjs/src/index.js", - "module": "dist/_esm5/src/index.js", - "es2015": "dist/_esm2015/src/index.js", - "types": "dist/_cjs/src/index.d.ts", - "sideEffects": [ - "./dist/_cjs/src/lens/index.js", - "./dist/_esm5/src/lens/index.js", - "./dist/_esm2015/src/lens/index.js", - "./dist/_cjs/src/lens/json.js", - "./dist/_esm5/src/lens/json.js", - "./dist/_esm2015/src/lens/json.js" - ], + "module": "dist/_esm2015/src/index.js", + "types": "dist/_esm2015/src/index.d.ts", "files": [ "dist/_cjs/src/", - "dist/_esm5/src/", "dist/_esm2015/src/" ], "scripts": { @@ -24,9 +14,8 @@ "clean": "rm -rf ./dist", "dev": "tsc -b -w tsconfig.build.cjs.json", "build:cjs": "tsc -b tsconfig.build.cjs.json", - "build:es5": "tsc -b tsconfig.build.es5.json", "build:es2015": "tsc -b tsconfig.build.es2015.json", - "build": "yarn run clean && yarn build:cjs && yarn build:es5 && yarn build:es2015 && yarn run lint", + "build": "yarn run clean && yarn build:cjs && yarn build:es2015 && yarn run lint", "test": "jest", "test:watch": "jest --watch", "lint": "eslint './src/**/*.ts*' 'test/**/*.ts*' && tsc --noemit", diff --git a/packages/focal/src/react/index.ts b/packages/focal/src/react/index.ts index 49c5fbe..535a50e 100644 --- a/packages/focal/src/react/index.ts +++ b/packages/focal/src/react/index.ts @@ -1,9 +1,5 @@ export * from './react' -// @NOTE need the following import to prevent TS error -// "variable is using name from external module but can not be named" -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { LiftedIntrinsics } from './intrinsic' import { createLiftedIntrinsics } from './intrinsic' export const F = createLiftedIntrinsics() diff --git a/packages/focal/tsconfig.build.cjs.json b/packages/focal/tsconfig.build.cjs.json index 369b5b8..b977ea9 100644 --- a/packages/focal/tsconfig.build.cjs.json +++ b/packages/focal/tsconfig.build.cjs.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts*"], + "exclude": ["**/*.test.ts*"], "compilerOptions": { "outDir": "dist/_cjs", "module": "commonjs", diff --git a/packages/focal/tsconfig.build.es2015.json b/packages/focal/tsconfig.build.es2015.json index fe14373..d698147 100644 --- a/packages/focal/tsconfig.build.es2015.json +++ b/packages/focal/tsconfig.build.es2015.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts*"], + "exclude": ["**/*.test.ts*"], "compilerOptions": { "outDir": "dist/_esm2015", "module": "es2015", diff --git a/packages/focal/tsconfig.build.es5.json b/packages/focal/tsconfig.build.es5.json deleted file mode 100644 index 77991a8..0000000 --- a/packages/focal/tsconfig.build.es5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts*"], - "compilerOptions": { - "outDir": "dist/_esm5", - "module": "commonjs", - "target": "es2015", - }, - "references": [ - { "path": "../focal-atom/tsconfig.build.es5.json" }, - ] -}