diff --git a/.dockerignore b/.dockerignore index 062d5bf3ea3ea..32088dfa0100a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,3 +16,8 @@ Dockerfile # Tests do not need to be included in build /server/**/test/** /client/**/test/** + +# Monorepo output +/apps/**/dist/ +/packages/*/dist/ +/packages/*/types/ diff --git a/.gitignore b/.gitignore index c8570e003c3c2..9eab1ac575e82 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ cached-requests.json # Monorepo output /apps/*/dist/ /packages/*/dist/ +/packages/*/types/ diff --git a/packages/tree-select/package.json b/packages/tree-select/package.json index 56ed59435a54e..5896d107be3d4 100644 --- a/packages/tree-select/package.json +++ b/packages/tree-select/package.json @@ -11,6 +11,7 @@ "license": "GPL-2.0-or-later", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", + "types": "types/index.d.ts", "repository": { "type": "git", "url": "git+https://github.com/Automattic/wp-calypso.git", @@ -27,8 +28,8 @@ "src" ], "scripts": { - "clean": "npx rimraf dist", + "clean": "npx rimraf dist types", "prepublish": "npm run clean", - "prepare": "transpile" + "prepare": "tsc --project ./tsconfig.json && tsc --project ./tsconfig-cjs.json" } } diff --git a/packages/tree-select/src/index.js b/packages/tree-select/src/index.js deleted file mode 100644 index ccc5e2c7c1ccf..0000000000000 --- a/packages/tree-select/src/index.js +++ /dev/null @@ -1,93 +0,0 @@ -const defaultGetCacheKey = ( ...args ) => args.join(); - -const isFunction = fn => { - return fn && typeof fn === 'function'; -}; - -const isObject = o => { - return o && typeof o === 'object'; -}; - -/** - * Returns a selector that caches values. - * - * @param {Function} getDependents A Function describing the dependent(s) of the selector. - * Must return an array which gets passed as the first arg to the selector - * @param {Function} selector A standard selector for calculating cached result - * @param {Object} options Options bag with additional arguments - * @param {Function} options.getCacheKey - * Custom way to compute the cache key from the `args` list - * @return {Function} Cached selector - */ -export default function treeSelect( getDependents, selector, options = {} ) { - if ( process.env.NODE_ENV !== 'production' ) { - if ( ! isFunction( getDependents ) || ! isFunction( selector ) ) { - throw new TypeError( - 'treeSelect: invalid arguments passed, selector and getDependents must both be functions' - ); - } - } - - let cache = new WeakMap(); - - const { getCacheKey = defaultGetCacheKey } = options; - - const cachedSelector = function( state, ...args ) { - const dependents = getDependents( state, ...args ); - - if ( process.env.NODE_ENV !== 'production' ) { - if ( getCacheKey === defaultGetCacheKey && args.some( isObject ) ) { - throw new Error( 'Do not pass objects as arguments to a treeSelector' ); - } - } - - // create a dependency tree for caching selector results. - // this is beneficial over standard memoization techniques so that we can - // garbage collect any values that are based on outdated dependents - const leafCache = dependents.reduce( insertDependentKey, cache ); - - const key = getCacheKey( ...args ); - if ( leafCache.has( key ) ) { - return leafCache.get( key ); - } - - const value = selector( dependents, ...args ); - leafCache.set( key, value ); - return value; - }; - - cachedSelector.clearCache = () => { - // WeakMap doesn't have `clear` method, so we need to recreate it - cache = new WeakMap(); - }; - - return cachedSelector; -} - -/* - * This object will be used as a WeakMap key if a dependency is a falsy value (null, undefined, ...) - */ -const NULLISH_KEY = {}; - -/* - * First tries to get the value for the key. - * If the key is not present, then inserts a new map and returns it - * - * Note: Inserts WeakMaps except for the last map which will be a regular Map. - * The last map is a regular one because the the key for the last map is the string results of args.join(). - */ -function insertDependentKey( map, key, currentIndex, arr ) { - if ( key != null && Object( key ) !== key ) { - throw new TypeError( 'key must be an object, `null`, or `undefined`' ); - } - const weakMapKey = key || NULLISH_KEY; - - const existingMap = map.get( weakMapKey ); - if ( existingMap ) { - return existingMap; - } - - const newMap = currentIndex === arr.length - 1 ? new Map() : new WeakMap(); - map.set( weakMapKey, newMap ); - return newMap; -} diff --git a/packages/tree-select/src/index.ts b/packages/tree-select/src/index.ts new file mode 100644 index 0000000000000..f76c17d1aceac --- /dev/null +++ b/packages/tree-select/src/index.ts @@ -0,0 +1,145 @@ +type CacheKeyFromArgs< O extends any[] > = ( ...args: O ) => string; + +const defaultGetCacheKey: CacheKeyFromArgs< any[] > = ( ...args: any[] ) => args.join(); + +const isFunction = ( fn: any ): boolean => { + return typeof fn === 'function'; +}; + +const isObject = ( o: any ): o is object => { + // Truthiness check is required because `typeof null === 'object'`. + return o && typeof o === 'object'; +}; + +type WeakCacheableKey = object | undefined | null; + +/** + * Generics + * + * S: State - the application state + * D: Dependents - Array of dependent values returned by the DependentsSelector + * R: Result of Selector + * O: Other parameters that the computation and resulting CachedSelector are provided + */ + +/** + * DependentsSelector is a function that accepts a State (S) object and + * returns an array of values to be used in the Selector as well + * as the values used by the caching/memoization layer. + */ +type DependentsSelector< S, O extends any[], D extends WeakCacheableKey[] > = ( + state: S, + ...args: O +) => D; + +/** + * Function that computes a value based on the dependent values provided + * by the DependentsSelector. It receives the values returned by + * DependentsSelector as its first argument, the rest of the arguments + * given to the computation are the same as the CachedSelector retured + * by treeSelect. + */ +type Selector< D extends WeakCacheableKey[], R, O extends any[] > = ( + dependents: D, + ...args: O +) => R; + +/** + * The cached selector is the returned function from treeSelect. It should + * have the same signature as Selector except it accepts the State as its + * first argument instead of the result of DependentsSelector. The rest of + * the other (O) arguments are the same provided to the Selector. + */ +type CachedSelector< S, R, O extends any[] > = ( state: S, ...args: O ) => R; + +interface Options< O extends any[] > { + /** + * Custom function to compute the cache key from the selector's `args` list + */ + getCacheKey?: CacheKeyFromArgs< O >; +} + +/** + * Returns a selector that caches values. + * + * @param getDependents A Function describing the dependent(s) of the selector. Must return an array which gets passed as the first arg to the selector. + * @param selector A standard selector for calculating cached result. + * @param options Additional options + * @returns Cached selector + */ +export default function treeSelect< S, D extends WeakCacheableKey[], R, O extends any[] >( + getDependents: DependentsSelector< S, O, D >, + selector: Selector< D, R, O >, + options: Options< O > = {} +): CachedSelector< S, R, O > { + if ( process.env.NODE_ENV !== 'production' ) { + if ( ! isFunction( getDependents ) || ! isFunction( selector ) ) { + throw new TypeError( + 'treeSelect: invalid arguments passed, selector and getDependents must both be functions' + ); + } + } + + let cache = new WeakMap(); + + const { getCacheKey = defaultGetCacheKey } = options; + + const cachedSelector = function( state: S, ...args: O ) { + const dependents = getDependents( state, ...args ); + + if ( process.env.NODE_ENV !== 'production' ) { + if ( getCacheKey === defaultGetCacheKey && args.some( isObject ) ) { + throw new Error( 'Do not pass objects as arguments to a treeSelector' ); + } + } + + // create a dependency tree for caching selector results. + // this is beneficial over standard memoization techniques so that we can + // garbage collect any values that are based on outdated dependents + const leafCache = dependents.reduce( insertDependentKey, cache ); + + const key = getCacheKey( ...args ); + if ( leafCache.has( key ) ) { + return leafCache.get( key ); + } + + const value = selector( dependents, ...args ); + leafCache.set( key, value ); + return value; + }; + + cachedSelector.clearCache = () => { + // WeakMap doesn't have `clear` method, so we need to recreate it + cache = new WeakMap(); + }; + + return cachedSelector; +} + +/* + * This object will be used as a WeakMap key if a dependency is a falsy value (null, undefined, ...) + */ +const NULLISH_KEY = {}; + +/* + * First tries to get the value for the key. + * If the key is not present, then inserts a new map and returns it + * + * Note: Inserts WeakMaps except for the last map which will be a regular Map. + * The last map is a regular one because the key for the last map is the string results of args.join(). + */ +function insertDependentKey( map: any, key: WeakCacheableKey, currentIndex: number, arr: any ) { + if ( key != null && Object( key ) !== key ) { + throw new TypeError( 'key must be an object, `null`, or `undefined`' ); + } + const weakMapKey = key || NULLISH_KEY; + + const existingMap = map.get( weakMapKey ); + if ( existingMap ) { + return existingMap; + } + + const newMap = currentIndex === arr.length - 1 ? new Map() : new WeakMap(); + map.set( weakMapKey, newMap ); + return newMap; +} diff --git a/packages/tree-select/tsconfig-cjs.json b/packages/tree-select/tsconfig-cjs.json new file mode 100644 index 0000000000000..c141d1c16fad8 --- /dev/null +++ b/packages/tree-select/tsconfig-cjs.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist/cjs", + "module": "commonjs", + "declaration": false, + "declarationDir": null + }, + "extends": "." +} diff --git a/packages/tree-select/tsconfig.json b/packages/tree-select/tsconfig.json new file mode 100644 index 0000000000000..bd0c81f99e2b9 --- /dev/null +++ b/packages/tree-select/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "outDir": "./dist/esm", + "module": "esnext", + "declaration": true, + "declarationDir": "types", + + "esModuleInterop": true, + "moduleResolution": "node", + "lib": [ "es5", "es2015.collection" ], + "types": [ "node" ], + "target": "es5", + + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "files": [ "src/index.ts" ], + "exclude": [ "**/test/**/*", "**/docs/**/*" ] +}