Skip to content

Commit

Permalink
feat(ts): properly type getComponent and hasComponent of ICatalog (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
natterstefan committed Mar 30, 2020
1 parent ae69468 commit 4584408
Show file tree
Hide file tree
Showing 17 changed files with 196 additions and 53 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/**/*.d.ts
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module.exports = {
'react-hooks/exhaustive-deps': 'error',

// typescript settings
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-unused-vars': [
'error',
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ dist
es
esm
lib
*.tgz
*.tgz

# typescript declaration files generated by tsc in src
src/**/*.d.ts
17 changes: 13 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
language: node_js

env:
global:
- YARN_VERSION="1.21.1"

node_js:
- "node"

before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version $YARN_VERSION
- export PATH="$HOME/.yarn/bin:$PATH"

install:
- npm install
- yarn

script:
- npm test --silent
- npm run build
- npm run size
- yarn test
- yarn build
- yarn size

notifications:
email:
Expand All @@ -18,6 +26,7 @@ notifications:
after_success: "npm run coveralls"

cache:
yarn: true
directories:
- ~/.npm # cache npm's cache
- ~/npm # cache latest npm
Expand Down
11 changes: 8 additions & 3 deletions example/client/base/components/button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React from 'react'
/* eslint-disable no-console */
import React, { FunctionComponent } from 'react'

const Button = () => (
type ButtonProps = {
text?: string
}

const Button: FunctionComponent<ButtonProps> = ({ text = 'Hey it is me' }) => (
<button type="button" onClick={() => console.log('Hey :)')}>
Hey, it is me!
{text}
</button>
)

Expand Down
15 changes: 13 additions & 2 deletions example/client/client1/components/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import React, { FunctionComponent } from 'react'
import CatalogComponent, { useCatalog } from 'react-component-catalog'
import { catalog as outerCatalog, innerCatalog } from '../../catalog'

type Catalog = typeof innerCatalog & typeof outerCatalog

const FallbackComponent: FunctionComponent = () => (
<div>Component not found</div>
)

const App: FunctionComponent = () => {
const catalog = useCatalog()
const Button = catalog.getComponent('Button')
const catalog = useCatalog<Catalog>()
const hasButton = catalog.hasComponent('Button')

let Button
if (hasButton) {
Button = catalog.getComponent('Button')
// this would work too:
// const Button = catalog.getComponent(['Button'])
}

// or you use them with the <CatalogComponent /> component
return (
Expand Down
10 changes: 9 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
module.exports = {
collectCoverageFrom: ['src/**/*.ts'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.dt.ts',
'!src/**/(__mocks__|__stories__|__tests__)/*.{js,jsx,ts,tsx}',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testMatch: [
'<rootDir>/src/**/__tests__/*.test.ts',
'<rootDir>/src/**/__tests__/*.test.tsx',
],
testPathIgnorePatterns: ['<rootDir>/(dist|es|esm|lib|node_modules)/'],
transform: {
'^.+\\.(t|j)sx?$': 'ts-jest',
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"lib"
],
"scripts": {
"build": " npm run build-types && npm run build-cjs && npm run build-es && npm run build-esm && npm run build-umd",
"build-types": "npm run build-cjs-types && npm run build-es-types && npm run build-esm-types",
"build": " yarn run build-types && yarn run build-cjs && yarn run build-es && yarn run build-esm && yarn run build-umd",
"build-types": "yarn run build-cjs-types && yarn run build-es-types && yarn run build-esm-types",
"build-cjs": "BABEL_ENV=cjs babel src --ignore **/*.test.tsx,**/*.test.ts --out-dir lib --extensions '.ts,.tsx'",
"build-cjs-types": "tsc --outDir lib --module commonjs --target es5 -d --emitDeclarationOnly",
"build-es": "BABEL_ENV=es babel src --ignore **/*.test.tsx,**/*.test.ts --out-dir es --extensions '.ts,.tsx'",
Expand All @@ -36,11 +36,11 @@
"coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls",
"lint": "eslint --cache 'src/**/*.{ts,tsx}' --quiet",
"prebuild": "rimraf dist && rimraf es && rimraf esm && rimraf lib",
"prepublishOnly": "npm run prerelease",
"prerelease": "npm run build && npm run size && npm run test",
"prepublishOnly": "yarn prerelease",
"prerelease": "yarn build && yarn size && yarn test",
"release": "HUSKY_SKIP_HOOKS=1 standard-version",
"size": "size-limit",
"test": "jest && npm run test-types",
"test": "jest --detectOpenHandles && yarn test-types",
"test-types": "tsc --noEmit",
"watch": "BABEL_ENV=esm babel --watch src --ignore **/@types/*,**/*.test.tsx,**/*.test.ts --out-dir esm --extensions '.ts,.tsx'",
"watch-test": "jest --watch"
Expand Down
106 changes: 95 additions & 11 deletions src/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,111 @@
import { get } from './utils'
import { get, PropertyPath } from './utils'
import { CatalogComponents } from './types'

export interface ICatalog<T extends CatalogComponents = CatalogComponents> {
export interface ICatalog<T extends CatalogComponents> {
// contains the raw catalog
_catalog: T | Record<string, any>
// get a component by id, if not available it will return null
getComponent: (component: string) => any
// validates if the given component exists in the catalog
hasComponent: (component: string) => boolean
_catalog: T
/**
* Get a Component by it's path in the given catalog. If it is not available,
* getComponent will return undefined
*
* @example
* ```
* const Button = catalog.getComponent('button');
* const Button = catalog.getComponent(['button']);
*
* // or, if the components in the catalog are nested
* const Button = catalog.getComponent("common.button")
* const Button = catalog.getComponent(["common", "button"])
* ```
*
* types are inspired by
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/7caeca4bfbd5ca9f306c14def3dd6b416869c615/types/lodash/common/object.d.ts#L1669
* @see https://codewithstyle.info/Deep-property-access-in-TypeScript/
*
* Playground
* @see https://nttr.st/2xu77eG
*
* Additional references:
* @see https://github.com/pirix-gh/ts-toolbelt
* @see https://stackoverflow.com/q/47256723/1238150
* @see https://stackoverflow.com/a/58436959/1238150
*/
getComponent<TKey extends keyof NonNullable<T>>(
component: TKey | [TKey],
): NonNullable<T>[TKey] | undefined
getComponent<
TKey1 extends keyof NonNullable<T>,
TKey2 extends keyof NonNullable<T>[TKey1]
>(
component: [TKey1, TKey2],
): NonNullable<T>[TKey1][TKey2] | undefined
getComponent<
TKey1 extends keyof NonNullable<T>,
TKey2 extends keyof NonNullable<T>[TKey1],
TKey3 extends keyof NonNullable<T>[TKey1][TKey2]
>(
component: [TKey1, TKey2, TKey3],
): NonNullable<T>[TKey1][TKey2][TKey3] | undefined
getComponent<
TKey1 extends keyof NonNullable<T>,
TKey2 extends keyof NonNullable<T>[TKey1],
TKey3 extends keyof NonNullable<T>[TKey1][TKey2],
TKey4 extends keyof NonNullable<T>[TKey1][TKey2][TKey3]
>(
component: [TKey1, TKey2, TKey3, TKey4],
): NonNullable<T>[TKey1][TKey2][TKey3][TKey4] | undefined
getComponent(component: PropertyPath): any
/**
* validates if the given component exists in the catalog
*
* @example
* ```
* const hasButton = catalog.hasComponent('button');
* const hasButton = catalog.hasComponent(['button']);
*
* // or, if the components in the catalog are nested
* const hasButton = catalog.hasComponent("common.button")
* const hasButton = catalog.hasComponent(["common", "button"])
* ```
*/
hasComponent<TKey extends keyof NonNullable<T>>(
component: TKey | [TKey],
): boolean
hasComponent<
TKey1 extends keyof NonNullable<T>,
TKey2 extends keyof NonNullable<T>[TKey1]
>(
component: [TKey1, TKey2],
): boolean
hasComponent<
TKey1 extends keyof NonNullable<T>,
TKey2 extends keyof NonNullable<T>[TKey1],
TKey3 extends keyof NonNullable<T>[TKey1][TKey2]
>(
component: [TKey1, TKey2, TKey3],
): boolean
hasComponent<
TKey1 extends keyof NonNullable<T>,
TKey2 extends keyof NonNullable<T>[TKey1],
TKey3 extends keyof NonNullable<T>[TKey1][TKey2],
TKey4 extends keyof NonNullable<T>[TKey1][TKey2][TKey3]
>(
component: [TKey1, TKey2, TKey3, TKey4],
): boolean
hasComponent(component: PropertyPath): boolean
}

export class Catalog<T extends CatalogComponents = CatalogComponents>
implements ICatalog<T> {
export class Catalog<T extends CatalogComponents> implements ICatalog<T> {
public _catalog: T

constructor(catalog: T) {
this._catalog = catalog
}

public getComponent: ICatalog<T>['getComponent'] = component =>
public getComponent: ICatalog<T>['getComponent'] = (component: any) =>
get(this._catalog, component)

public hasComponent: ICatalog<T>['hasComponent'] = component =>
public hasComponent: ICatalog<T>['hasComponent'] = (component: any) =>
!!get(this._catalog, component)
}

Expand Down
20 changes: 13 additions & 7 deletions src/components/__tests__/catalog-component.test.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
/* eslint-disable react/no-multi-comp */
import React from 'react'
import React, { FunctionComponent } from 'react'
import { mount } from 'enzyme'

import Catalog from '../../catalog'
import CatalogComponent from '../catalog-component'
import CatalogProvider from '../catalog-provider'
import { withCatalog } from '../with-catalog'

const TestComponent = () => <div>Hello World</div>
const BaseArticle = () => <div>Hello BaseArticle</div>
const TestComponent: FunctionComponent = () => <div>Hello World</div>
const BaseArticle: FunctionComponent = () => <div>Hello BaseArticle</div>

const FallbackComponent = () => <div>Fallback</div>
const FallbackFromCatalog = () => <div>FallbackFromCatalog</div>
const FallbackComponent: FunctionComponent = () => <div>Fallback</div>
const FallbackFromCatalog: FunctionComponent = () => (
<div>FallbackFromCatalog</div>
)

type TestCatalog = {
[name: string]: JSX.Element | FunctionComponent | TestCatalog
}

describe('CatalogComponent', () => {
let backupError: () => void
let testCatalog: {}
let emptyTestCatalog: {}
let testCatalog: TestCatalog = null
let emptyTestCatalog: TestCatalog = null

const components = {
FallbackFromCatalog,
Expand Down
14 changes: 8 additions & 6 deletions src/components/__tests__/with-catalog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { FunctionComponent } from 'react'
import { mount } from 'enzyme'

import Catalog, { ICatalog } from '../../catalog'
Expand All @@ -10,15 +10,17 @@ const TestComponent = withCatalog(() => <div>Hello World</div>)
const TestButton = () => <button type="button">Hello</button>
const TestButtonComponent = withCatalog(TestButton)

type TestCatalog = {
[name: string]: FunctionComponent
}

describe('withCatalog', () => {
let testCatalog: ICatalog
let testCatalog: ICatalog<TestCatalog> = null

beforeEach(() => {
testCatalog = new Catalog({
components: {
TestComponent,
TestButtonComponent,
},
TestComponent,
TestButtonComponent,
})
})

Expand Down
2 changes: 1 addition & 1 deletion src/components/catalog-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const CatalogComponent = React.forwardRef((props: IProps, ref) => {
} = props

// get catalog from the context
const catalog = useCatalog()
const catalog = useCatalog<any>()
if (!catalog || typeof catalog.getComponent !== 'function') {
if (__DEV__) {
console.error(
Expand Down
10 changes: 5 additions & 5 deletions src/components/catalog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ const CatalogProvider = <T extends CatalogComponents>(
): JSX.Element => {
const { catalog, catalogPrefix, children } = props
const outerCatalog = useCatalog()
let prepCatalog: ICatalog<T> = new Catalog({})

const prefixedCatalog: { [prop: string]: any } = {}
const prefixedCatalog: { [prop: string]: unknown } = {}
if (catalog && catalogPrefix) {
Object.keys(catalog).forEach(c => {
prefixedCatalog[`${catalogPrefix}${c}`] = (catalog as any)[c]
const cE = c as keyof typeof catalog
prefixedCatalog[`${catalogPrefix}${cE}`] = catalog[cE]
})
}

Expand All @@ -39,11 +39,11 @@ const CatalogProvider = <T extends CatalogComponents>(
*
* Attention: the innerCatalog will overwrite the outerCatalog!
*/
prepCatalog = new Catalog({
const prepCatalog = new Catalog({
...(outerCatalog && outerCatalog._catalog),
// either use the prefixed or the raw catalog
...((catalogPrefix && prefixedCatalog) || catalog),
})
}) as ICatalog<T>

return (
<CatalogContext.Provider value={prepCatalog}>
Expand Down
11 changes: 8 additions & 3 deletions src/components/use-catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import CatalogContext from './catalog-context'

/**
* `useCatalog` (react-hook) returns the `catalog` provided to `CatalogProvider`
*
* @example
* ```
* const catalog = useCatalog()
* const Button = catalog.getComponent("button")
* ```
*/
const useCatalog = <
T extends CatalogComponents = CatalogComponents
>(): ICatalog<T> => React.useContext<ICatalog<T>>(CatalogContext)
const useCatalog = <T extends CatalogComponents>(): ICatalog<T> =>
React.useContext(CatalogContext as any)

export default useCatalog
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
/**
* This interface can be augmented by users to add types to
* `react-component-catalog`'s default catalog
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CatalogComponents {}

0 comments on commit 4584408

Please sign in to comment.