From 5304470b7d57d148144ef96406af0716089806be Mon Sep 17 00:00:00 2001 From: Emmanuel Chambon Date: Thu, 15 Jul 2021 17:36:32 +0200 Subject: [PATCH 1/5] refactor: migrate to typescript --- .eslintrc | 11 +- babel.config.json | 2 +- package.json | 10 +- .../src/__tests__/{index.js => index.ts} | 2 +- .../random-name/src/{index.js => index.ts} | 2 +- .../src/__tests__/{index.js => index.ts} | 225 ++++++++---------- packages/regex/src/{index.js => index.ts} | 0 .../use-dataloader/src/{index.js => index.ts} | 0 .../src/__tests__/{index.js => usei18n.js} | 2 +- packages/use-i18n/src/index.ts | 1 + .../use-i18n/src/{index.js => usei18n.js} | 0 packages/use-query-params/package.json | 1 + .../src/__tests__/{index.js => index.tsx} | 8 +- .../src/{index.js => index.ts} | 22 +- rollup.config.mjs | 38 +-- tsconfig.json | 12 + types/docker-names.d.ts | 3 + yarn.lock | 105 +++++++- 18 files changed, 283 insertions(+), 161 deletions(-) rename packages/random-name/src/__tests__/{index.js => index.ts} (92%) rename packages/random-name/src/{index.js => index.ts} (83%) rename packages/regex/src/__tests__/{index.js => index.ts} (73%) rename packages/regex/src/{index.js => index.ts} (100%) rename packages/use-dataloader/src/{index.js => index.ts} (100%) rename packages/use-i18n/src/__tests__/{index.js => usei18n.js} (99%) create mode 100644 packages/use-i18n/src/index.ts rename packages/use-i18n/src/{index.js => usei18n.js} (100%) rename packages/use-query-params/src/__tests__/{index.js => index.tsx} (97%) rename packages/use-query-params/src/{index.js => index.ts} (70%) create mode 100644 tsconfig.json create mode 100644 types/docker-names.d.ts diff --git a/.eslintrc b/.eslintrc index 9ab6aed8e..f8cbfaffb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,5 +13,14 @@ "devDependencies": ["**/__tests__/*", "rollup.config.mjs"] } ] - } + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "parserOptions": { + "project": ["tsconfig.json"] + }, + "extends": ["./packages/eslint-config-react/typescript.js"] + } + ] } diff --git a/babel.config.json b/babel.config.json index d3014957b..4771d8ffa 100644 --- a/babel.config.json +++ b/babel.config.json @@ -1,4 +1,4 @@ { - "presets": ["@babel/preset-env", "@babel/preset-react"], + "presets": ["@babel/preset-typescript", "@babel/preset-env", "@babel/preset-react"], "plugins": ["@babel/plugin-transform-runtime"] } diff --git a/package.json b/package.json index 053d6389c..13726e500 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@babel/plugin-transform-runtime": "^7.12.10", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.13.13", + "@babel/preset-typescript": "^7.14.5", "@commitlint/cli": "^12.0.0", "@commitlint/config-conventional": "^12.0.0", "@rollup/plugin-babel": "^5.2.2", @@ -18,6 +19,7 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^12.0.0", "@testing-library/react-hooks": "^7.0.0", + "@types/react-router-dom": "^5.1.8", "builtin-modules": "^3.2.0", "cross-env": "^7.0.3", "eslint": "^7.18.0", @@ -30,10 +32,12 @@ "prettier": "^2.2.1", "read-pkg": "^6.0.0", "rollup": "^2.36.1", - "rollup-plugin-visualizer": "^5.5.0" + "rollup-plugin-dts": "^3.0.2", + "rollup-plugin-visualizer": "^5.5.0", + "typescript": "^4.3.5" }, "scripts": { - "lint": "eslint --cache .", + "lint": "eslint --ext js,ts,tsx --cache .", "lint:fix": "yarn run lint --fix", "build": "lerna exec --stream --ignore @scaleway/eslint-* --ignore @scaleway/countries -- rollup -c ../../rollup.config.mjs", "build:profile": "cross-env PROFILE=true yarn run build", @@ -65,7 +69,7 @@ "jest-localstorage-mock" ], "collectCoverageFrom": [ - "packages/*/src/**/*.{js,jsx}" + "packages/*/src/**/*.{ts,tsx,js,jsx}" ], "modulePathIgnorePatterns": [ "locales" diff --git a/packages/random-name/src/__tests__/index.js b/packages/random-name/src/__tests__/index.ts similarity index 92% rename from packages/random-name/src/__tests__/index.js rename to packages/random-name/src/__tests__/index.ts index 392a08a57..b0db30a0d 100644 --- a/packages/random-name/src/__tests__/index.js +++ b/packages/random-name/src/__tests__/index.ts @@ -22,7 +22,7 @@ describe('randomNames', () => { }) it('should never includes the word "cocks"', () => { - const names = Array.from(Array(1000000), randomName) + const names = Array.from(Array(1000000), () => randomName()) expect(names).not.toEqual( expect.arrayContaining([expect.stringMatching('cocks')]), ) diff --git a/packages/random-name/src/index.js b/packages/random-name/src/index.ts similarity index 83% rename from packages/random-name/src/index.js rename to packages/random-name/src/index.ts index f08b16876..fb057c0a5 100644 --- a/packages/random-name/src/index.js +++ b/packages/random-name/src/index.ts @@ -1,6 +1,6 @@ import dockerNames from 'docker-names' -const randomName = (prefix = '', separator = '-') => { +const randomName = (prefix = '', separator = '-'): string => { let random = dockerNames.getRandomName().replace(/_/g, separator) while (random.includes('cocks')) { random = dockerNames.getRandomName().replace(/_/g, separator) diff --git a/packages/regex/src/__tests__/index.js b/packages/regex/src/__tests__/index.ts similarity index 73% rename from packages/regex/src/__tests__/index.js rename to packages/regex/src/__tests__/index.ts index 6fb4f5d85..858f64ea1 100644 --- a/packages/regex/src/__tests__/index.js +++ b/packages/regex/src/__tests__/index.ts @@ -50,7 +50,7 @@ const macAddress1 = '1F:B5:FA:47:CD:C4' describe('@regex', () => { describe('alpha', () => { - ;[ + test.each([ [alphanumdashText, false], [alphanumdashdotsText, false], [asciiLetters, true], @@ -65,14 +65,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alpha.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alpha.test(string)).toBe(expected) + }) }) describe('alphanum', () => { - ;[ + test.each([ [alphanumdashText, false], [alphanumdashdotsText, false], [asciiLetters, true], @@ -87,14 +86,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanum.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanum.test(string)).toBe(expected) + }) }) describe('alphanumdash', () => { - ;[ + test.each([ [alphanumdashText, true], [alphanumdashdotsText, false], [asciiLetters, true], @@ -109,14 +107,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumdash.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumdash.test(string)).toBe(expected) + }) }) describe('alphanumdashdots', () => { - ;[ + test.each([ [alphanumdashText, true], [alphanumdashdotsText, true], [asciiLetters, true], @@ -131,14 +128,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumdashdots.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumdashdots.test(string)).toBe(expected) + }) }) describe('alphanumdashdotsorempty', () => { - ;[ + test.each([ [alphanumdashText, true], [alphanumdashdotsText, true], [asciiLetters, true], @@ -153,14 +149,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumdashdotsorempty.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumdashdotsorempty.test(string)).toBe(expected) + }) }) describe('alphanumdashdotsspaces', () => { - ;[ + test.each([ [alphanumdashText, true], [alphanumdashdotsText, true], [asciiLetters, true], @@ -175,14 +170,13 @@ describe('@regex', () => { [whitespace, true], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumdashdotsspaces.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumdashdotsspaces.test(string)).toBe(expected) + }) }) describe('alphanumdashorempty', () => { - ;[ + test.each([ [alphanumdashText, true], [alphanumdashdotsText, false], [asciiLetters, true], @@ -197,14 +191,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumdashorempty.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumdashorempty.test(string)).toBe(expected) + }) }) describe('alphanumdashspaces', () => { - ;[ + test.each([ [alphanumdashText, true], [alphanumdashdotsText, false], [asciiLetters, true], @@ -219,14 +212,13 @@ describe('@regex', () => { [whitespace, true], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumdashspaces.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumdashspaces.test(string)).toBe(expected) + }) }) describe('alphanumdots', () => { - ;[ + test.each([ [alphanumdashText, false], [alphanumdashdotsText, false], [asciiLetters, true], @@ -241,14 +233,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumdots.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumdots.test(string)).toBe(expected) + }) }) describe('alphanumLowercase', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, true], [asciiUppercase, false], @@ -261,14 +252,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumLowercase.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumLowercase.test(string)).toBe(expected) + }) }) describe('alphanumSpacesDotsUnderscoreDash', () => { - ;[ + test.each([ [asciiLetters, true], [asciiLowercase, true], [asciiUppercase, true], @@ -281,14 +271,13 @@ describe('@regex', () => { [whitespace, true], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumSpacesDotsUnderscoreDash.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumSpacesDotsUnderscoreDash.test(string)).toBe(expected) + }) }) describe('alphanumUnderscoreDash', () => { - ;[ + test.each([ [asciiLetters, true], [asciiLowercase, true], [asciiUppercase, true], @@ -301,14 +290,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumUnderscoreDash.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumUnderscoreDash.test(string)).toBe(expected) + }) }) describe('alphanumUnderscoreDollarDash', () => { - ;[ + test.each([ [asciiLetters, true], [asciiLowercase, true], [asciiUppercase, true], @@ -321,14 +309,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(alphanumUnderscoreDollarDash.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(alphanumUnderscoreDollarDash.test(string)).toBe(expected) + }) }) describe('ascii', () => { - ;[ + test.each([ [asciiLetters, true], [asciiLowercase, true], [asciiUppercase, true], @@ -340,14 +327,13 @@ describe('@regex', () => { [punctuation, true], [whitespace, true], [cronTest, true], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(ascii.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(ascii.test(string)).toBe(expected) + }) }) describe('backupKey', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -361,14 +347,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(backupKey.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(backupKey.test(string)).toBe(expected) + }) }) describe('cron', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -380,14 +365,13 @@ describe('@regex', () => { [punctuation, false], [whitespace, false], [cronTest, true], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(cron.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(cron.test(string)).toBe(expected) + }) }) describe('digits', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -400,14 +384,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(digits.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(digits.test(string)).toBe(expected) + }) }) describe('email', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -420,14 +403,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(email.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(email.test(string)).toBe(expected) + }) }) describe('fourDigitsCode', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -441,14 +423,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(fourDigitsCode.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(fourDigitsCode.test(string)).toBe(expected) + }) }) describe('macAddress', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -462,14 +443,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, true], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(macAddress.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(macAddress.test(string)).toBe(expected) + }) }) describe('phone', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -484,14 +464,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(phone.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(phone.test(string)).toBe(expected) + }) }) describe('spaces', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -506,14 +485,13 @@ describe('@regex', () => { [whitespace, true], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(spaces.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(spaces.test(string)).toBe(expected) + }) }) describe('sixDigitsCode', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -529,14 +507,13 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [macAddress1, false], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(sixDigitsCode.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(sixDigitsCode.test(string)).toBe(expected) + }) }) describe('url', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -554,14 +531,13 @@ describe('@regex', () => { [macAddress1, false], [url1, true], [url2, true], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(url.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(url.test(string)).toBe(expected) + }) }) describe('hexadecimal', () => { - ;[ + test.each([ [asciiLetters, false], [asciiLowercase, false], [asciiUppercase, false], @@ -576,9 +552,8 @@ describe('@regex', () => { [whitespace, false], [cronTest, false], [hexdigits, true], - ].forEach(([string, result]) => - it(`should match regex ${string} to be ${result}`, () => - expect(hexadecimal.test(string)).toBe(result)), - ) + ])('should match regex %s to be %s)', (string, expected) => { + expect(hexadecimal.test(string)).toBe(expected) + }) }) }) diff --git a/packages/regex/src/index.js b/packages/regex/src/index.ts similarity index 100% rename from packages/regex/src/index.js rename to packages/regex/src/index.ts diff --git a/packages/use-dataloader/src/index.js b/packages/use-dataloader/src/index.ts similarity index 100% rename from packages/use-dataloader/src/index.js rename to packages/use-dataloader/src/index.ts diff --git a/packages/use-i18n/src/__tests__/index.js b/packages/use-i18n/src/__tests__/usei18n.js similarity index 99% rename from packages/use-i18n/src/__tests__/index.js rename to packages/use-i18n/src/__tests__/usei18n.js index 050e34a19..3124b174a 100644 --- a/packages/use-i18n/src/__tests__/index.js +++ b/packages/use-i18n/src/__tests__/usei18n.js @@ -2,7 +2,7 @@ import { act, renderHook } from '@testing-library/react-hooks' import mockdate from 'mockdate' import React from 'react' -import I18n, { useI18n, useTranslation } from '..' +import I18n, { useI18n, useTranslation } from '../usei18n' import en from './locales/en' import es from './locales/es' import fr from './locales/fr' diff --git a/packages/use-i18n/src/index.ts b/packages/use-i18n/src/index.ts new file mode 100644 index 000000000..9c1175d04 --- /dev/null +++ b/packages/use-i18n/src/index.ts @@ -0,0 +1 @@ +export * from './usei18n' diff --git a/packages/use-i18n/src/index.js b/packages/use-i18n/src/usei18n.js similarity index 100% rename from packages/use-i18n/src/index.js rename to packages/use-i18n/src/usei18n.js diff --git a/packages/use-query-params/package.json b/packages/use-query-params/package.json index 6a43e70a3..182746a54 100644 --- a/packages/use-query-params/package.json +++ b/packages/use-query-params/package.json @@ -27,6 +27,7 @@ }, "license": "MIT", "dependencies": { + "history": "^5.0.0", "query-string": "^7.0.0", "react-router-dom": "^5.2.0" }, diff --git a/packages/use-query-params/src/__tests__/index.js b/packages/use-query-params/src/__tests__/index.tsx similarity index 97% rename from packages/use-query-params/src/__tests__/index.js rename to packages/use-query-params/src/__tests__/index.tsx index e1755956f..8af706244 100644 --- a/packages/use-query-params/src/__tests__/index.js +++ b/packages/use-query-params/src/__tests__/index.tsx @@ -1,12 +1,12 @@ import { act, renderHook } from '@testing-library/react-hooks' -import React from 'react' +import React, { ReactNode } from 'react' import { MemoryRouter } from 'react-router-dom' import useQueryParams from '..' const wrapper = - ({ pathname = 'one', search }) => + ({ pathname = 'one', search }: { pathname?: string, search: string }) => // eslint-disable-next-line react/prop-types - ({ children }) => + ({ children }: { children: ReactNode }) => ( {children} @@ -159,7 +159,7 @@ describe('useQueryParam', () => { }) }) - it('should render good params with parallel changes', async () => { + it('should render good params with parallel changes', () => { const { result } = renderHook(() => useQueryParams(), { wrapper: wrapper({ search: '' }), }) diff --git a/packages/use-query-params/src/index.js b/packages/use-query-params/src/index.ts similarity index 70% rename from packages/use-query-params/src/index.js rename to packages/use-query-params/src/index.ts index 06257ec59..8e1b616d5 100644 --- a/packages/use-query-params/src/index.js +++ b/packages/use-query-params/src/index.ts @@ -1,10 +1,17 @@ -import { parse, stringify } from 'query-string' +import { History } from 'history' +import { ParsedQuery, parse, stringify } from 'query-string' import { useCallback, useMemo } from 'react' import { useHistory, useLocation } from 'react-router-dom' -const useQueryParams = () => { - const { replace } = useHistory() - const location = useLocation() +const useQueryParams = (): { + queryParams: ParsedQuery; + replaceQueryParams: (newParams: Record) => void; + setQueryParams: (nextParams: Record) => void; +} => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { replace } = useHistory() + // eslint-disable-next-line @typescript-eslint/unbound-method + const location = useLocation() const currentState = useMemo( () => @@ -17,7 +24,7 @@ const useQueryParams = () => { ) const stringyFormat = useCallback( - params => + (params): string => stringify(params, { arrayFormat: 'comma', skipEmptyString: true, @@ -44,7 +51,7 @@ const useQueryParams = () => { * @param {Object} nextParams The params to set in the url as query params */ const setQueryParams = useCallback( - nextParams => { + (nextParams: Record): void => { replaceInUrlIfNeeded({ ...currentState, ...nextParams }) }, [currentState, replaceInUrlIfNeeded], @@ -55,7 +62,7 @@ const useQueryParams = () => { * @param {Object} newParams */ const replaceQueryParams = useCallback( - newParams => { + (newParams: Record): void => { replaceInUrlIfNeeded({ ...newParams }) }, [replaceInUrlIfNeeded], @@ -67,4 +74,5 @@ const useQueryParams = () => { setQueryParams, } } + export default useQueryParams diff --git a/rollup.config.mjs b/rollup.config.mjs index 2c65b9031..ea15952c1 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -2,6 +2,7 @@ import { babel } from '@rollup/plugin-babel' import { nodeResolve } from '@rollup/plugin-node-resolve' import builtins from 'builtin-modules' import { readPackageAsync } from 'read-pkg' +import dts from 'rollup-plugin-dts' import { visualizer } from 'rollup-plugin-visualizer' const PROFILE = !!process.env.PROFILE @@ -24,37 +25,40 @@ const getConfig = (pkg, isBrowser = false) => { ].find(dep => new RegExp(`^${dep}`).test(id)) return { - input: './src/index.js', + external, + input: './src/index.ts', + output: { + file: isBrowser ? 'dist/index.browser.js' : 'dist/index.js', + format: 'es', + }, plugins: [ babel({ babelHelpers: 'runtime', babelrc: false, exclude: 'node_modules/**', - presets: [ - ['@babel/env', { modules: false, targets }], - ], + extensions: ['.js', '.jsx', '.ts', '.tsx', '.es', '.mjs'], plugins: [ '@babel/plugin-transform-runtime', '@babel/plugin-transform-react-jsx', ], + presets: [ + '@babel/preset-typescript', + ['@babel/env', { modules: false, targets }], + ], }), nodeResolve({ browser: isBrowser, + extensions: [ '.mjs', '.js', '.json', '.ts', '.tsx' ], preferBuiltins: true, }), PROFILE && visualizer({ - gzipSize: true, brotliSize: true, - open: true, filename: '.reports/report.html', + gzipSize: true, + open: true, }), ].filter(Boolean), - external, - output: { - format: 'es', - file: isBrowser ? 'dist/module.browser.js' : 'dist/module.js', - }, } } @@ -63,7 +67,13 @@ export default async () => { const doesAlsoTargetBrowser = 'browser' in pkg - return [getConfig(pkg), doesAlsoTargetBrowser && getConfig(pkg, true)].filter( - Boolean, - ) + return [ + getConfig(pkg), + doesAlsoTargetBrowser && getConfig(pkg, true), + { + input: './src/index.ts', + output: [{ file: 'dist/index.d.ts', format: 'es' }], + plugins: [dts()], + }, + ].filter(Boolean) } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..7de61d6ae --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "strict": true, + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "jsx": "preserve", + }, + "include": ["**/*.ts", "**/*.js", "**/.*.js", "**/*.tsx"] +} diff --git a/types/docker-names.d.ts b/types/docker-names.d.ts new file mode 100644 index 000000000..0881aa290 --- /dev/null +++ b/types/docker-names.d.ts @@ -0,0 +1,3 @@ +declare module 'docker-names' { + const getRandomName: (appendNumber?: boolean | number) => string +} diff --git a/yarn.lock b/yarn.lock index 2d3ed2e61..3d3c6b838 100644 --- a/yarn.lock +++ b/yarn.lock @@ -97,6 +97,18 @@ "@babel/helper-replace-supers" "^7.14.5" "@babel/helper-split-export-declaration" "^7.14.5" +"@babel/helper-create-class-features-plugin@^7.14.6": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz#f114469b6c06f8b5c59c6c4e74621f5085362542" + integrity sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-member-expression-to-functions" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/helper-create-regexp-features-plugin@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz#c7d5ac5e9cf621c26057722fb7a8a4c5889358c4" @@ -528,6 +540,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-syntax-typescript@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz#b82c6ce471b165b5ce420cf92914d6fb46225716" + integrity sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-typescript@^7.7.2": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz#9dff111ca64154cef0f4dc52cf843d9f12ce4474" @@ -813,6 +832,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-transform-typescript@^7.14.5": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.6.tgz#6e9c2d98da2507ebe0a883b100cde3c7279df36c" + integrity sha512-XlTdBq7Awr4FYIzqhmYY80WN0V0azF74DMPyFqVHBvf81ZUgc4X7ZOpx6O8eLDK6iM5cCQzeyJw0ynTaefixRA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.6" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-typescript" "^7.14.5" + "@babel/plugin-transform-unicode-escapes@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz#9d4bd2a681e3c5d7acf4f57fa9e51175d91d0c6b" @@ -930,6 +958,15 @@ "@babel/plugin-transform-react-jsx-development" "^7.14.5" "@babel/plugin-transform-react-pure-annotations" "^7.14.5" +"@babel/preset-typescript@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.14.5.tgz#aa98de119cf9852b79511f19e7f44a2d379bcce0" + integrity sha512-u4zO6CdbRKbS9TypMqrlGH7sd2TAJppZwn3c/ZRLeO/wGsbddxgbPDUZVNrie3JWYLQ9vpineKlsrWFvO6Pwkw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + "@babel/plugin-transform-typescript" "^7.14.5" + "@babel/runtime-corejs3@^7.10.2": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4" @@ -945,6 +982,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.7.6": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" + integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.14.5", "@babel/template@^7.3.3": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" @@ -2396,6 +2440,11 @@ dependencies: "@types/node" "*" +"@types/history@*": + version "4.7.9" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.9.tgz#1cfb6d60ef3822c589f18e70f8b12f9a28ce8724" + integrity sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -2475,6 +2524,23 @@ dependencies: "@types/react" "*" +"@types/react-router-dom@^5.1.8": + version "5.1.8" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.8.tgz#bf3e1c8149b3d62eaa206d58599de82df0241192" + integrity sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.16" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.16.tgz#f3ba045fb96634e38b21531c482f9aeb37608a99" + integrity sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-test-renderer@>=16.9.0": version "17.0.1" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" @@ -3187,9 +3253,9 @@ camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001219: - version "1.0.30001228" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa" - integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A== + version "1.0.30001245" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz" + integrity sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA== caseless@~0.12.0: version "0.12.0" @@ -4709,6 +4775,13 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" +history@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" + integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg== + dependencies: + "@babel/runtime" "^7.7.6" + hoist-non-react-statics@^3.1.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -6097,6 +6170,13 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -7622,6 +7702,15 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rollup-plugin-dts@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-3.0.2.tgz#2b628d88f864d271d6eaec2e4c2a60ae4e944c5c" + integrity sha512-hswlsdWu/x7k5pXzaLP6OvKRKcx8Bzprksz9i9mUe72zvt8LvqAb/AZpzs6FkLgmyRaN8B6rUQOVtzA3yEt9Yw== + dependencies: + magic-string "^0.25.7" + optionalDependencies: + "@babel/code-frame" "^7.12.13" + rollup-plugin-visualizer@^5.5.0: version "5.5.2" resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.5.2.tgz#ae2130ee5ae4a2d901e764e492b71357cb95eed7" @@ -7862,6 +7951,11 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -8433,6 +8527,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== + uglify-js@^3.1.4: version "3.13.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113" From dcd3f7d4008c4ba9dd1f3740f4cdecf16289a46a Mon Sep 17 00:00:00 2001 From: Emmanuel Chambon Date: Thu, 15 Jul 2021 21:40:01 +0200 Subject: [PATCH 2/5] refactor: migrate use-i18n --- ...{formatUnit.js.snap => formatUnit.ts.snap} | 0 .../{formatUnit.js => formatUnit.ts} | 5 +- .../src/__tests__/{usei18n.js => usei18n.tsx} | 13 +- .../src/{formatUnit.js => formatUnit.ts} | 63 +++++---- .../use-i18n/src/{usei18n.js => usei18n.tsx} | 128 +++++++++++++----- tsconfig.json | 5 + types/global.d.ts | 5 + types/intl.d.ts | 15 ++ 8 files changed, 172 insertions(+), 62 deletions(-) rename packages/use-i18n/src/__tests__/__snapshots__/{formatUnit.js.snap => formatUnit.ts.snap} (100%) rename packages/use-i18n/src/__tests__/{formatUnit.js => formatUnit.ts} (86%) rename packages/use-i18n/src/__tests__/{usei18n.js => usei18n.tsx} (97%) rename packages/use-i18n/src/{formatUnit.js => formatUnit.ts} (67%) rename packages/use-i18n/src/{usei18n.js => usei18n.tsx} (58%) create mode 100644 types/global.d.ts create mode 100644 types/intl.d.ts diff --git a/packages/use-i18n/src/__tests__/__snapshots__/formatUnit.js.snap b/packages/use-i18n/src/__tests__/__snapshots__/formatUnit.ts.snap similarity index 100% rename from packages/use-i18n/src/__tests__/__snapshots__/formatUnit.js.snap rename to packages/use-i18n/src/__tests__/__snapshots__/formatUnit.ts.snap diff --git a/packages/use-i18n/src/__tests__/formatUnit.js b/packages/use-i18n/src/__tests__/formatUnit.ts similarity index 86% rename from packages/use-i18n/src/__tests__/formatUnit.js rename to packages/use-i18n/src/__tests__/formatUnit.ts index 9b4405ae5..a18a3dfa0 100644 --- a/packages/use-i18n/src/__tests__/formatUnit.js +++ b/packages/use-i18n/src/__tests__/formatUnit.ts @@ -1,6 +1,6 @@ import memoizeIntlConstructor from 'intl-format-cache' import IntlTranslationFormat from 'intl-messageformat' -import formatUnit, { supportedUnits } from '../formatUnit' +import formatUnit, { FormatUnitOptions, supportedUnits } from '../formatUnit' const locales = ['en', 'fr', 'ro'] @@ -56,13 +56,14 @@ const tests = [ describe('formatUnit', () => { test('should return empty string for unknown unit', () => { expect( + // @ts-expect-error We test the use case when unit is unknown formatUnit('fr', 123, { unit: 'unknown' }, getTranslationFormat), ).toMatchSnapshot() }) test.each(tests)('%s %o', (_, options, locale, amount) => { expect( - formatUnit(locale, amount, options, getTranslationFormat), + formatUnit(locale as string, amount as number, options as FormatUnitOptions, getTranslationFormat), ).toMatchSnapshot() }) }) diff --git a/packages/use-i18n/src/__tests__/usei18n.js b/packages/use-i18n/src/__tests__/usei18n.tsx similarity index 97% rename from packages/use-i18n/src/__tests__/usei18n.js rename to packages/use-i18n/src/__tests__/usei18n.tsx index 3124b174a..cb7487d07 100644 --- a/packages/use-i18n/src/__tests__/usei18n.js +++ b/packages/use-i18n/src/__tests__/usei18n.tsx @@ -11,8 +11,8 @@ const LOCALE_ITEM_STORAGE = 'locales' const wrapper = ({ - loadDateLocale = async locale => import(`date-fns/locale/${locale}/index`), - defaultLoad = async ({ locale }) => import(`./locales/${locale}`), + loadDateLocale = async (locale: string) => import(`date-fns/locale/${locale}/index`), + defaultLoad = async ({ locale }: { locale: string }) => import(`./locales/${locale}`), defaultLocale = 'en', defaultTranslations = {}, enableDebugKey = false, @@ -20,7 +20,7 @@ const wrapper = localeItemStorage = LOCALE_ITEM_STORAGE, supportedLocales = ['en', 'fr', 'es'], } = {}) => - ({ children }) => + ({ children }: { children: React.ReactNode }) => ( { mockdate.reset() jest.clearAllMocks() }) - it('useTranslation should not be defined without I18nProvider', async () => { + + it('useTranslation should not be defined without I18nProvider', () => { const { result } = renderHook(() => useTranslation(), { wrapper: ({ children }) =>
{children}
, }) @@ -85,7 +86,7 @@ describe('i18n hook', () => { }) it('should use specific load on useTranslation', async () => { - const load = async ({ locale, namespace }) => + const load = async ({ locale, namespace }: { locale: string, namespace: string }) => import(`./locales/namespaces/${locale}/${namespace}.json`) const { result, waitForNextUpdate } = renderHook( @@ -144,7 +145,7 @@ describe('i18n hook', () => { }) it("should use specific load and fallback default local if the key doesn't exist", async () => { - const load = async ({ locale, namespace }) => + const load = async ({ locale, namespace }: { locale: string, namespace: string }) => import(`./locales/namespaces/${locale}/${namespace}.json`) const { result, waitForNextUpdate } = renderHook( diff --git a/packages/use-i18n/src/formatUnit.js b/packages/use-i18n/src/formatUnit.ts similarity index 67% rename from packages/use-i18n/src/formatUnit.js rename to packages/use-i18n/src/formatUnit.ts index da898fcc1..11a3df87c 100644 --- a/packages/use-i18n/src/formatUnit.js +++ b/packages/use-i18n/src/formatUnit.ts @@ -1,4 +1,5 @@ import filesize from 'filesize' +import { Options } from 'intl-messageformat' // We are on base 10, so we should use IEC standard here ... const exponents = [ @@ -13,6 +14,8 @@ const exponents = [ { name: 'yotta', symbol: 'Y' }, ] +type Exponent = typeof exponents[number] + const frOctet = { plural: 'octets', singular: 'octet', @@ -50,7 +53,11 @@ const compoundUnitsSymbols = { second: 'ps', } -const formatShortUnit = (locale, exponent, unit, compoundUnit) => { +type Unit = 'bit' | 'byte' +type CompoundUnit = 'second' +type FormatPlural = (message: string, locales?: string | string[] | undefined, overrideFormats?: undefined, opts?: Options | undefined) => { format: ({ amount }: { amount: number}) => string} + +const formatShortUnit = (locale: string, exponent: Exponent, unit: Unit, compoundUnit?: CompoundUnit) => { let shortenedUnit = symbols.short[unit] if ( @@ -65,14 +72,14 @@ const formatShortUnit = (locale, exponent, unit, compoundUnit) => { }` } -const formatLongUnit = (locale, exponent, unit, number, messageFormat) => { +const formatLongUnit = (locale: string, exponent: Exponent, unit: Unit, amount: number, messageFormat: FormatPlural) => { let translation = symbols.long[unit] if ( unit === 'byte' && Object.keys(localesWhoFavorOctetOverByte).includes(locale) ) { - translation = localesWhoFavorOctetOverByte[locale] + translation = localesWhoFavorOctetOverByte[locale as keyof typeof localesWhoFavorOctetOverByte] } return `${exponent.name}${messageFormat( @@ -82,38 +89,43 @@ const formatLongUnit = (locale, exponent, unit, number, messageFormat) => { other {${translation.plural}} }`, locale, - ).format({ amount: number })}` + ).format({ amount })}` } const format = - ({ compoundUnit, exponent, unit, humanize = false }) => + ({ compoundUnit, exponent, unit, humanize = false }: { + compoundUnit?: CompoundUnit, + unit: Unit, + exponent?: Exponent, + humanize?: boolean, + }) => ( - locale, - number, - { maximumFractionDigits, minimumFractionDigits, short = true }, - messageFormat, - ) => { + locale: string, + amount: number, + { maximumFractionDigits, minimumFractionDigits, short = true }: { maximumFractionDigits?: number, minimumFractionDigits?: number, short?: boolean }, + messageFormat: FormatPlural, + ): string => { let computedExponent = exponent - let computedValue = number + let computedValue = amount if (humanize) { - if (!exponent) { - const value = filesize(number, { + if (computedExponent) { + const value = filesize(amount, { base: 10, + exponent: exponents.findIndex( + exp => exp.name === (computedExponent as Exponent).name, + ), output: 'object', round: maximumFractionDigits, - }) - computedExponent = exponents[value.exponent] + }) as unknown as { value: number, symbol: string, exponent: number } computedValue = value.value } else { - const value = filesize(number, { + const value = filesize(amount, { base: 10, - exponent: exponents.findIndex( - exp => exp.name === computedExponent.name, - ), output: 'object', round: maximumFractionDigits, - }) + }) as unknown as { value: number, symbol: string, exponent: number } + computedExponent = exponents[value.exponent] computedValue = value.value } } @@ -123,10 +135,10 @@ const format = minimumFractionDigits, }).format(computedValue)} ${ short - ? formatShortUnit(locale, computedExponent, unit, compoundUnit) + ? formatShortUnit(locale, computedExponent as Exponent, unit, compoundUnit) : formatLongUnit( locale, - computedExponent, + computedExponent as Exponent, unit, computedValue, messageFormat, @@ -182,7 +194,12 @@ export const supportedUnits = { ), } -const formatUnit = (locale, number, { unit, ...options }, messageFormat) => +export interface FormatUnitOptions { + unit: keyof typeof supportedUnits + short?: boolean +} + +const formatUnit = (locale: string, number: number, { unit, ...options }: FormatUnitOptions, messageFormat: FormatPlural): string => supportedUnits?.[unit]?.(locale, number, options, messageFormat) ?? '' export default formatUnit diff --git a/packages/use-i18n/src/usei18n.js b/packages/use-i18n/src/usei18n.tsx similarity index 58% rename from packages/use-i18n/src/usei18n.js rename to packages/use-i18n/src/usei18n.tsx index c95619e70..6e68623e1 100644 --- a/packages/use-i18n/src/usei18n.js +++ b/packages/use-i18n/src/usei18n.tsx @@ -1,8 +1,10 @@ -import { formatDistanceToNow, formatDistanceToNowStrict } from 'date-fns' +import { Locale, formatDistanceToNow, formatDistanceToNowStrict } from 'date-fns' import memoizeIntlConstructor from 'intl-format-cache' import IntlTranslationFormat from 'intl-messageformat' import PropTypes from 'prop-types' import React, { + ReactElement, + ReactNode, createContext, useCallback, useContext, @@ -12,27 +14,36 @@ import React, { } from 'react' import ReactDOM from 'react-dom' import 'intl-pluralrules' -import unitFormat from './formatUnit' +import unitFormat, { FormatUnitOptions } from './formatUnit' + const LOCALE_ITEM_STORAGE = 'locale' -const prefixKeys = prefix => obj => - Object.keys(obj).reduce((acc, key) => { - acc[prefix + key] = obj[key] +type Translations = Record & { prefix?: string } +type TranslationsByLocales = Record +type TranslateFn = (key: string, context?: Record) => string + +const prefixKeys = (prefix: string) => (obj: { [key: string]: string }) => + Object.keys(obj).reduce((acc: { [key: string ]: string }, key) => { + acc[`${prefix}${key}`] = obj[key] return acc }, {}) -const areNamespacesLoaded = (namespaces, loadedNamespaces) => +const areNamespacesLoaded = (namespaces: string[], loadedNamespaces: string[] = []) => namespaces.every(n => loadedNamespaces.includes(n)) -const getLocaleFallback = locale => locale.split('-')[0].split('_')[0] +const getLocaleFallback = (locale: string) => locale.split('-')[0].split('_')[0] const getCurrentLocale = ({ defaultLocale, supportedLocales, localeItemStorage, -}) => { +}: { + defaultLocale: string, + supportedLocales: string[], + localeItemStorage: string, +}): string => { const languages = navigator.languages || [navigator.language] const browserLocales = [...new Set(languages.map(getLocaleFallback))] const localeStorage = localStorage.getItem(localeItemStorage) @@ -44,9 +55,36 @@ const getCurrentLocale = ({ ) } -const I18nContext = createContext() +interface Context { + currentLocale: string + dateFnsLocale?: Locale, + datetime?: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string, + formatList?: (listFormat: string[], options?: Intl.ListFormatOptions) => string, + formatNumber?: (numb: number, options?: Intl.NumberFormatOptions) => string, + formatUnit?: (value: number, options: FormatUnitOptions) => string, + loadTranslations?: (namespace: string, load?: LoadTranslationsFn) => Promise, + locales?: string[], + namespaces?: string[], + namespaceTranslation?: (namespace: string, t?: TranslateFn) => TranslateFn + relativeTime?: (date: Date | number, options?: { + includeSeconds?: boolean; + addSuffix?: boolean; + }) => string, + relativeTimeStrict?: (date: Date | number, options?: { + addSuffix?: boolean; + unit?: 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year'; + roundingMethod?: 'floor' | 'ceil' | 'round'; + }) => string, + setTranslations?: React.Dispatch>, + switchLocale?: (locale: string) => void, + t?: TranslateFn, + translations?: TranslationsByLocales, +} -export const useI18n = () => { +// @ts-expect-error we force the context to undefined, should be corrected with default values +const I18nContext = createContext(undefined) + +export const useI18n = (): Context => { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useI18n must be used within a I18nProvider') @@ -55,7 +93,7 @@ export const useI18n = () => { return context } -export const useTranslation = (namespaces = [], load) => { +export const useTranslation = (namespaces: string[] = [], load: LoadTranslationsFn): Context & { isLoaded: boolean } => { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useTranslation must be used within a I18nProvider') @@ -64,7 +102,7 @@ export const useTranslation = (namespaces = [], load) => { const key = namespaces.join(',') useEffect(() => { - key.split(',').map(async namespace => loadTranslations(namespace, load)) + key.split(',').map(async (namespace: string) => loadTranslations?.(namespace, load)) }, [loadTranslations, key, load]) const isLoaded = useMemo( @@ -81,6 +119,9 @@ const getNumberFormat = memoizeIntlConstructor(Intl.NumberFormat) const getDateTimeFormat = memoizeIntlConstructor(Intl.DateTimeFormat) const getListFormat = memoizeIntlConstructor(Intl.ListFormat) +type LoadTranslationsFn = ({ namespace, locale }: { namespace: string, locale: string}) => Promise<{ default: Translations}> +type LoadLocaleFn = (locale: string) => Promise + const I18nContextProvider = ({ children, defaultLoad, @@ -91,13 +132,23 @@ const I18nContextProvider = ({ enableDebugKey, localeItemStorage, supportedLocales, -}) => { - const [currentLocale, setCurrentLocale] = useState( +}: { + children: ReactNode, + defaultLoad: LoadTranslationsFn, + loadDateLocale: LoadLocaleFn, + defaultLocale: string, + defaultTranslations: TranslationsByLocales, + enableDefaultLocale: boolean, + enableDebugKey: boolean, + localeItemStorage: string, + supportedLocales: string[], +}): ReactElement => { + const [currentLocale, setCurrentLocale] = useState( getCurrentLocale({ defaultLocale, localeItemStorage, supportedLocales }), ) - const [translations, setTranslations] = useState(defaultTranslations) - const [namespaces, setNamespaces] = useState([]) - const [dateFnsLocale, setDateFnsLocale] = useState() + const [translations, setTranslations] = useState(defaultTranslations) + const [namespaces, setNamespaces] = useState([]) + const [dateFnsLocale, setDateFnsLocale] = useState() useEffect(() => { loadDateLocale(currentLocale === 'en' ? 'en-GB' : currentLocale) @@ -106,7 +157,7 @@ const I18nContextProvider = ({ }, [loadDateLocale, currentLocale]) const loadTranslations = useCallback( - async (namespace, load = defaultLoad) => { + async (namespace: string, load: LoadTranslationsFn = defaultLoad) => { const result = { [currentLocale]: { default: {} }, defaultLocale: { default: {} }, @@ -123,7 +174,7 @@ const I18nContextProvider = ({ namespace, }) - const trad = { + const trad: Translations = { ...result.defaultLocale.default, ...result[currentLocale].default, } @@ -154,7 +205,7 @@ const I18nContextProvider = ({ ) const switchLocale = useCallback( - locale => { + (locale: string) => { if (supportedLocales.includes(locale)) { localStorage.setItem(localeItemStorage, locale) setCurrentLocale(locale) @@ -164,13 +215,17 @@ const I18nContextProvider = ({ ) const formatNumber = useCallback( - (numb, options) => getNumberFormat(currentLocale, options).format(numb), + // intl-format-chache does not forwrad return types + // eslint-disable-next-line + (numb: number, options?: Intl.NumberFormatOptions) => getNumberFormat(currentLocale, options).format(numb), [currentLocale], ) const formatList = useCallback( - (listFormat, options) => - getListFormat(currentLocale, options).format(listFormat), + (listFormat: string[], options?: Intl.ListFormatOptions) => + // intl-format-chache does not forwrad return types + // eslint-disable-next-line + getListFormat(currentLocale, options).format(listFormat), [currentLocale], ) @@ -178,18 +233,24 @@ const I18nContextProvider = ({ // Once https://github.com/tc39/proposal-smart-unit-preferences is stable we should // be able to use formatNumber directly const formatUnit = useCallback( - (value, options) => + (value: number, options: FormatUnitOptions) => unitFormat(currentLocale, value, options, getTranslationFormat), [currentLocale], ) const datetime = useCallback( - (date, options) => getDateTimeFormat(currentLocale, options).format(date), + // intl-format-chache does not forwrad return types + // eslint-disable-next-line + (date: Date | number, options?: Intl.DateTimeFormatOptions): string => getDateTimeFormat(currentLocale, options).format(date), [currentLocale], ) const relativeTimeStrict = useCallback( - (date, options = { addSuffix: true, unit: 'day' }) => { + (date: Date | number, options: { + addSuffix?: boolean + unit?: 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' + roundingMethod?: 'floor' | 'ceil' | 'round' + } = { addSuffix: true, unit: 'day' }) => { const finalDate = new Date(date) return formatDistanceToNowStrict(finalDate, { @@ -201,7 +262,10 @@ const I18nContextProvider = ({ ) const relativeTime = useCallback( - (date, options = { addSuffix: true }) => { + (date: Date | number, options: { + includeSeconds?: boolean + addSuffix?: boolean + } = { addSuffix: true }) => { const finalDate = new Date(date) return formatDistanceToNow(finalDate, { @@ -213,7 +277,7 @@ const I18nContextProvider = ({ ) const translate = useCallback( - (key, context) => { + (key: string, context?: Record) => { const value = translations[currentLocale]?.[key] if (!value) { if (enableDebugKey) { @@ -223,6 +287,8 @@ const I18nContextProvider = ({ return '' } if (context) { + // intl-format-chache does not forwrad return types + // eslint-disable-next-line return getTranslationFormat(value, currentLocale).format(context) } @@ -232,9 +298,9 @@ const I18nContextProvider = ({ ) const namespaceTranslation = useCallback( - (namespace, t = translate) => - (identifier, ...args) => - t(`${namespace}.${identifier}`, ...args) || t(identifier, ...args), + (namespace: string, t: TranslateFn = translate) => + (identifier: string, context?: Record) => + t(`${namespace}.${identifier}`, context) || t(identifier, context), [translate], ) diff --git a/tsconfig.json b/tsconfig.json index 7de61d6ae..097ccc386 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,10 @@ { "compilerOptions": { + "target": "esnext", + "lib": [ + "DOM", + "ES2019" + ], "allowJs": true, "strict": true, "module": "esnext", diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 000000000..c3575a580 --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,5 @@ +export {}; + +declare global { + type PrimitiveType = string | number | boolean | null | undefined | Date; +} diff --git a/types/intl.d.ts b/types/intl.d.ts new file mode 100644 index 000000000..5bb566bd4 --- /dev/null +++ b/types/intl.d.ts @@ -0,0 +1,15 @@ +declare namespace Intl { + interface ListFormatOptions { + localeMatcher: 'best fit' | 'lookup' + type: 'conjunction' | 'disjunction' | 'unit' + style: 'long' | 'short' | 'narrow' + } + + interface ListFormat { + format: (items: [string?]) => string; + } + + const ListFormat: { + new(locales?: string | string[], options?: ListFormatOptions): ListFormat; + } +} From 572b7a734caccabbedc938e87be88ba7f7db4a08 Mon Sep 17 00:00:00 2001 From: Emmanuel Chambon Date: Fri, 16 Jul 2021 18:07:57 +0200 Subject: [PATCH 3/5] refactor: use-dataloader --- ...aderProvider.js => DataLoaderProvider.tsx} | 50 ++++++++---- ...rovider.test.js => DataLoaderProvider.tsx} | 12 +-- ...seDataLoader.test.js => useDataLoader.tsx} | 40 ++++++---- .../src/{constants.js => constants.ts} | 0 .../src/{reducer.js => reducer.ts} | 15 +++- .../{useDataLoader.js => useDataLoader.ts} | 76 +++++++++++++------ 6 files changed, 131 insertions(+), 62 deletions(-) rename packages/use-dataloader/src/{DataLoaderProvider.js => DataLoaderProvider.tsx} (64%) rename packages/use-dataloader/src/__tests__/{DataLoaderProvider.test.js => DataLoaderProvider.tsx} (94%) rename packages/use-dataloader/src/__tests__/{useDataLoader.test.js => useDataLoader.tsx} (90%) rename packages/use-dataloader/src/{constants.js => constants.ts} (100%) rename packages/use-dataloader/src/{reducer.js => reducer.ts} (70%) rename packages/use-dataloader/src/{useDataLoader.js => useDataLoader.ts} (73%) diff --git a/packages/use-dataloader/src/DataLoaderProvider.js b/packages/use-dataloader/src/DataLoaderProvider.tsx similarity index 64% rename from packages/use-dataloader/src/DataLoaderProvider.js rename to packages/use-dataloader/src/DataLoaderProvider.tsx index c32d811c4..36b13e911 100644 --- a/packages/use-dataloader/src/DataLoaderProvider.js +++ b/packages/use-dataloader/src/DataLoaderProvider.tsx @@ -1,5 +1,7 @@ import PropTypes from 'prop-types' import React, { + ReactElement, + ReactNode, createContext, useCallback, useContext, @@ -7,13 +9,33 @@ import React, { useRef, } from 'react' -export const DataLoaderContext = createContext() +interface Context { + addCachedData: (key: string, newData: unknown) => void; + addReload: (key: string, method: () => Promise) => void; + cacheKeyPrefix: string; + clearAllCachedData: () => void; + clearAllReloads: () => void; + clearCachedData: (key?: string | undefined) => void; + clearReload: (key?: string | undefined) => void; + getCachedData: (key?: string | undefined) => unknown; + getReloads: (key?: string | undefined) => (() => Promise) | Reloads; + reload: (key?: string | undefined) => Promise; + reloadAll: () => Promise; +} + +type CachedData = Record +type Reloads = Record Promise> + +// @ts-expect-error we force the context to undefined, should be corrected with default values +export const DataLoaderContext = createContext(undefined) -const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { - const cachedData = useRef({}) - const reloads = useRef({}) +const DataLoaderProvider = ({ children, cacheKeyPrefix }: { + children: ReactNode, cacheKeyPrefix: string +}): ReactElement => { + const cachedData = useRef({}) + const reloads = useRef({}) - const setCachedData = useCallback(compute => { + const setCachedData = useCallback((compute: CachedData | ((data: CachedData) => CachedData)) => { if (typeof compute === 'function') { cachedData.current = compute(cachedData.current) } else { @@ -21,7 +43,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { } }, []) - const setReloads = useCallback(compute => { + const setReloads = useCallback((compute: Reloads | ((data: Reloads) => Reloads)) => { if (typeof compute === 'function') { reloads.current = compute(reloads.current) } else { @@ -30,7 +52,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { }, []) const addCachedData = useCallback( - (key, newData) => { + (key: string, newData: unknown) => { if (key && typeof key === 'string' && newData) { setCachedData(actualCachedData => ({ ...actualCachedData, @@ -42,7 +64,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { ) const addReload = useCallback( - (key, method) => { + (key: string, method: () => Promise) => { if (key && typeof key === 'string' && method) { setReloads(actualReloads => ({ ...actualReloads, @@ -54,7 +76,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { ) const clearReload = useCallback( - key => { + (key?: string) => { if (key && typeof key === 'string') { setReloads(actualReloads => { const tmp = actualReloads @@ -72,7 +94,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { }, [setReloads]) const clearCachedData = useCallback( - key => { + (key?: string) => { if (key && typeof key === 'string') { setCachedData(actualCachedData => { const tmp = actualCachedData @@ -88,7 +110,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { setCachedData({}) }, [setCachedData]) - const reload = useCallback(async key => { + const reload = useCallback(async (key?: string) => { if (key && typeof key === 'string') { await (reloads.current[key] && reloads.current[key]()) } @@ -100,7 +122,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { ) }, []) - const getCachedData = useCallback(key => { + const getCachedData = useCallback((key?: string) => { if (key) { return cachedData.current[key] || undefined } @@ -108,7 +130,7 @@ const DataLoaderProvider = ({ children, cacheKeyPrefix }) => { return cachedData.current }, []) - const getReloads = useCallback(key => { + const getReloads = useCallback((key?: string) => { if (key) { return reloads.current[key] || undefined } @@ -161,6 +183,6 @@ DataLoaderProvider.defaultProps = { cacheKeyPrefix: undefined, } -export const useDataLoaderContext = () => useContext(DataLoaderContext) +export const useDataLoaderContext = (): Context => useContext(DataLoaderContext) export default DataLoaderProvider diff --git a/packages/use-dataloader/src/__tests__/DataLoaderProvider.test.js b/packages/use-dataloader/src/__tests__/DataLoaderProvider.tsx similarity index 94% rename from packages/use-dataloader/src/__tests__/DataLoaderProvider.test.js rename to packages/use-dataloader/src/__tests__/DataLoaderProvider.tsx index 6e5324333..144b28fd3 100644 --- a/packages/use-dataloader/src/__tests__/DataLoaderProvider.test.js +++ b/packages/use-dataloader/src/__tests__/DataLoaderProvider.tsx @@ -8,11 +8,11 @@ const wrapper = ({ children }) => ( ) describe('DataLoaderProvider', () => { - test('should render correctly', async () => { + test('should render correctly', () => { render(Test) expect(screen.getByText('Test')).toBeTruthy() }) - test('should add cached data', async () => { + test('should add cached data', () => { const { result } = renderHook(useDataLoaderContext, { wrapper }) expect(result.current.getCachedData()).toStrictEqual({}) @@ -25,7 +25,7 @@ describe('DataLoaderProvider', () => { expect(result.current.getCachedData().test).toBe('test') }) - test('should delete cached data', async () => { + test('should delete cached data', () => { const { result } = renderHook(useDataLoaderContext, { wrapper }) act(() => { @@ -51,7 +51,7 @@ describe('DataLoaderProvider', () => { expect(result.current.getCachedData()).toStrictEqual({}) }) - test('should get cached data', async () => { + test('should get cached data', () => { const { result } = renderHook(useDataLoaderContext, { wrapper }) expect(result.current.getCachedData()).toStrictEqual({}) @@ -78,10 +78,10 @@ describe('DataLoaderProvider', () => { expect(Object.values(result.current.getReloads()).length).toBe(1) expect(result.current.getReloads('test')).toBe(fn) expect(result.current.getReloads('testWrong')).toBe(undefined) - expect(await result.current.getReloads().test()).toBe(true) + expect(await (result.current.getReloads() as { test: () => Promise }).test()).toBe(true) }) - test('should clear reload', async () => { + test('should clear reload', () => { const { result } = renderHook(useDataLoaderContext, { wrapper }) expect(result.current.getCachedData()).toStrictEqual({}) diff --git a/packages/use-dataloader/src/__tests__/useDataLoader.test.js b/packages/use-dataloader/src/__tests__/useDataLoader.tsx similarity index 90% rename from packages/use-dataloader/src/__tests__/useDataLoader.test.js rename to packages/use-dataloader/src/__tests__/useDataLoader.tsx index 3fc7db3f5..9f8f801c5 100644 --- a/packages/use-dataloader/src/__tests__/useDataLoader.test.js +++ b/packages/use-dataloader/src/__tests__/useDataLoader.tsx @@ -102,7 +102,7 @@ describe('useDataLoader', () => { }) test('should render and cache correctly with cacheKeyPrefix', async () => { - const { result, waitForNextUpdate } = renderHook( + const { result, waitForNextUpdate } = renderHook)[]>( props => [ useDataLoader(props.key, props.method, props.config), useDataLoader(props.key, props.method, { @@ -124,8 +124,9 @@ describe('useDataLoader', () => { expect(result.current[0].data).toBe(true) expect(result.current[0].isSuccess).toBe(true) - act(() => { - result.current[1].reload() + act((): void => { + // eslint-disable-next-line no-void + void result.current[1].reload() }) expect(result.current[1].data).toBe(true) @@ -136,7 +137,7 @@ describe('useDataLoader', () => { }) test('should render correctly with enabled true', async () => { - const { result, waitForNextUpdate } = renderHook( + const { result, waitForNextUpdate } = renderHook>( props => useDataLoader(props.key, props.method, props.config), { initialProps, @@ -151,10 +152,12 @@ describe('useDataLoader', () => { expect(result.current.isLoading).toBe(false) act(() => { - result.current.reload() + // eslint-disable-next-line no-void + void result.current.reload() }) act(() => { - result.current.reload() + // eslint-disable-next-line no-void + void result.current.reload() }) expect(result.current.data).toBe(true) @@ -219,7 +222,7 @@ describe('useDataLoader', () => { }), ) - const { result, waitForNextUpdate, rerender } = renderHook( + const { result, waitForNextUpdate, rerender } = renderHook>( props => useDataLoader(props.key, props.method, props.config), { initialProps: pollingProps, @@ -250,7 +253,8 @@ describe('useDataLoader', () => { method: method2, }) act(() => { - result.current.reload() + // eslint-disable-next-line no-void + void result.current.reload() }) expect(result.current.data).toBe(true) expect(result.current.isPolling).toBe(true) @@ -284,7 +288,7 @@ describe('useDataLoader', () => { }) test('should render correctly with enabled off', async () => { - const { result, waitForNextUpdate } = renderHook( + const { result, waitForNextUpdate } = renderHook>( props => useDataLoader(props.key, props.method, props.config), { initialProps: { @@ -300,7 +304,8 @@ describe('useDataLoader', () => { expect(result.current.isIdle).toBe(true) act(() => { - result.current.reload() + // eslint-disable-next-line no-void + void result.current.reload() }) expect(result.current.data).toBe(undefined) @@ -375,7 +380,7 @@ describe('useDataLoader', () => { success = true }) const error = new Error('Test error') - const { result, waitForNextUpdate } = renderHook( + const { result, waitForNextUpdate } = renderHook>( props => useDataLoader(props.key, props.method, props.config), { initialProps: { @@ -409,7 +414,8 @@ describe('useDataLoader', () => { expect(onSuccess).toBeCalledTimes(0) act(() => { - result.current.reload() + // eslint-disable-next-line no-void + void result.current.reload() }) await waitForNextUpdate() @@ -421,7 +427,7 @@ describe('useDataLoader', () => { }) test('should use cached data', async () => { - const { result, waitForNextUpdate } = renderHook( + const { result, waitForNextUpdate } = renderHook[])>( props => [ useDataLoader(props.key, props.method, props.config), useDataLoader(props.key, props.method, { @@ -444,7 +450,8 @@ describe('useDataLoader', () => { expect(result.current[0].isSuccess).toBe(true) act(() => { - result.current[1].reload() + // eslint-disable-next-line no-void + void result.current[1].reload() }) expect(result.current[1].data).toBe(true) @@ -463,7 +470,7 @@ describe('useDataLoader', () => { }, 500) }), ) - const { result, waitForNextUpdate } = renderHook( + const { result, waitForNextUpdate } = renderHook, ReturnType])>( props => [ useDataLoader(props.key, props.method, props.config), useDataLoaderContext(), @@ -486,7 +493,8 @@ describe('useDataLoader', () => { expect(mockedFn).toBeCalledTimes(1) act(() => { - result.current[1].reloadAll() + // eslint-disable-next-line no-void + void result.current[1].reloadAll() }) expect(result.current[0].data).toBe(true) diff --git a/packages/use-dataloader/src/constants.js b/packages/use-dataloader/src/constants.ts similarity index 100% rename from packages/use-dataloader/src/constants.js rename to packages/use-dataloader/src/constants.ts diff --git a/packages/use-dataloader/src/reducer.js b/packages/use-dataloader/src/reducer.ts similarity index 70% rename from packages/use-dataloader/src/reducer.js rename to packages/use-dataloader/src/reducer.ts index 106ee325f..f3236e523 100644 --- a/packages/use-dataloader/src/reducer.js +++ b/packages/use-dataloader/src/reducer.ts @@ -2,7 +2,18 @@ /* eslint-disable default-case */ import { ActionEnum, StatusEnum } from './constants' -export default (state, action) => { +interface Action { + type: typeof ActionEnum[keyof typeof ActionEnum]; + error?: Error; +} + +interface State { + error?: Error; + status: typeof StatusEnum[keyof typeof StatusEnum]; + [key: string]: unknown +} + +export default (state: State, action: Action): State => { switch (action.type) { case ActionEnum.ON_LOADING: return { @@ -28,4 +39,6 @@ export default (state, action) => { status: StatusEnum.ERROR, } } + + return state } diff --git a/packages/use-dataloader/src/useDataLoader.js b/packages/use-dataloader/src/useDataLoader.ts similarity index 73% rename from packages/use-dataloader/src/useDataLoader.js rename to packages/use-dataloader/src/useDataLoader.ts index b3d7264c6..4da22f66a 100644 --- a/packages/use-dataloader/src/useDataLoader.js +++ b/packages/use-dataloader/src/useDataLoader.ts @@ -11,14 +11,14 @@ import { ActionEnum, StatusEnum } from './constants' import reducer from './reducer' const Actions = { - createOnError: error => ({ error, type: ActionEnum.ON_ERROR }), + createOnError: (error: Error) => ({ error, type: ActionEnum.ON_ERROR }), createOnLoading: () => ({ type: ActionEnum.ON_LOADING }), createOnSuccess: () => ({ type: ActionEnum.ON_SUCCESS }), createReset: () => ({ type: ActionEnum.RESET }), } /** - * @typedef {Object} useDataLoaderConfig + * @typedef {Object} UseDataLoaderConfig * @property {Function} [onSuccess] callback when a request success * @property {Function} [onError] callback when a error is occured * @property {*} [initialData] initial data if no one is present in the cache before the request @@ -26,9 +26,17 @@ const Actions = { * @property {boolean} [enabled=true] launch request automatically (default true) * @property {boolean} [keepPreviousData=true] do we need to keep the previous data after reload (default true) */ +interface UseDataLoaderConfig { + enabled?: boolean, + initialData?: T, + keepPreviousData?: boolean, + onError?: (err: Error) => Promise, + onSuccess?: (data: T) => Promise, + pollingInterval?: number, +} /** - * @typedef {Object} useDataLoaderResult + * @typedef {Object} UseDataLoaderResult * @property {boolean} isIdle true if the hook in initial state * @property {boolean} isLoading true if the request is launched * @property {boolean} isSuccess true if the request success @@ -39,6 +47,17 @@ const Actions = { * @property {string} error the error occured during the request * @property {Function} reload reload the data */ +interface UseDataLoaderResult { + data?: T; + error?: Error; + isError: boolean; + isIdle: boolean; + isLoading: boolean; + isPolling: boolean; + isSuccess: boolean; + previousData?: T; + reload: () => Promise; +} /** * @param {string} key key to save the data fetched in a local cache @@ -46,9 +65,9 @@ const Actions = { * @param {useDataLoaderConfig} config hook configuration * @returns {useDataLoaderResult} hook result containing data, request state, and method to reload the data */ -const useDataLoader = ( - fetchKey, - method, +const useDataLoader = ( + fetchKey: string, + method: () => Promise, { enabled = true, initialData, @@ -56,8 +75,8 @@ const useDataLoader = ( onError, onSuccess, pollingInterval, - } = {}, -) => { + }: UseDataLoaderConfig = {}, +): UseDataLoaderResult => { const { addReload, clearReload, @@ -81,7 +100,7 @@ const useDataLoader = ( return `${cacheKeyPrefix ? `${cacheKeyPrefix}-` : ''}${fetchKey}` }, [cacheKeyPrefix, fetchKey]) - const previousDataRef = useRef() + const previousDataRef = useRef() const isLoading = useMemo(() => status === StatusEnum.LOADING, [status]) const isIdle = useMemo(() => status === StatusEnum.IDLE, [status]) @@ -89,7 +108,7 @@ const useDataLoader = ( const isError = useMemo(() => status === StatusEnum.ERROR, [status]) const isPolling = useMemo( - () => enabled && pollingInterval && (isSuccess || isLoading), + () => !!(enabled && pollingInterval && (isSuccess || isLoading)) ?? false, [isSuccess, isLoading, enabled, pollingInterval], ) @@ -100,7 +119,7 @@ const useDataLoader = ( const result = await method() if (keepPreviousData) { - previousDataRef.current = getCachedData(cacheKey) + previousDataRef.current = getCachedData(cacheKey) as T } if (result !== undefined && result !== null && cacheKey) addCachedData(cacheKey, result) @@ -126,19 +145,26 @@ const useDataLoader = ( const handleRequestRef = useRef(handleRequest) useEffect(() => { - let handler - if (enabled) { - if (isIdle) { - handleRequestRef.current(key) - } - if (pollingInterval && (isSuccess || isError)) { - handler = setTimeout( - () => handleRequestRef.current(key), - pollingInterval, - ) + let handler: ReturnType + + async function fetch() { + if (enabled) { + if (isIdle) { + await handleRequestRef.current(key) + } + if (pollingInterval && (isSuccess || isError)) { + handler = setTimeout( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + () => handleRequestRef.current(key), + pollingInterval, + ) + } } } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetch() + return () => { if (handler) clearTimeout(handler) } @@ -147,8 +173,8 @@ const useDataLoader = ( useLayoutEffect(() => { dispatch(Actions.createReset()) if (key && typeof key === 'string') { - addReloadRef.current?.(key, reloadArgs => - handleRequestRef.current(key, reloadArgs), + addReloadRef.current?.(key, () => + handleRequestRef.current(key), ) } @@ -169,7 +195,7 @@ const useDataLoader = ( }, [handleRequest]) return { - data: getCachedData(key) || initialData, + data: (getCachedData(key) || initialData) as T, error, isError, isIdle, @@ -177,7 +203,7 @@ const useDataLoader = ( isPolling, isSuccess, previousData: previousDataRef.current, - reload: args => handleRequestRef.current(key, args), + reload: () => handleRequestRef.current(key), } } From e1b39fef51767bc3e99964413a72419b301b585c Mon Sep 17 00:00:00 2001 From: Emmanuel Chambon Date: Mon, 19 Jul 2021 10:31:53 +0200 Subject: [PATCH 4/5] fix: remove return fallback --- packages/use-dataloader/src/reducer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/use-dataloader/src/reducer.ts b/packages/use-dataloader/src/reducer.ts index f3236e523..bb4670db4 100644 --- a/packages/use-dataloader/src/reducer.ts +++ b/packages/use-dataloader/src/reducer.ts @@ -13,6 +13,7 @@ interface State { [key: string]: unknown } +// @ts-expect-error we have no default return export default (state: State, action: Action): State => { switch (action.type) { case ActionEnum.ON_LOADING: @@ -39,6 +40,4 @@ export default (state: State, action: Action): State => { status: StatusEnum.ERROR, } } - - return state } From bc8af84ab906a3849e5e558c00c2bf7e3e8422d8 Mon Sep 17 00:00:00 2001 From: Emmanuel Chambon Date: Tue, 20 Jul 2021 13:01:18 +0200 Subject: [PATCH 5/5] fix: correct package.json --- packages/random-name/package.json | 7 ++++--- packages/regex/package.json | 7 ++++--- packages/use-dataloader/package.json | 7 ++++--- packages/use-i18n/package.json | 7 ++++--- packages/use-query-params/package.json | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/random-name/package.json b/packages/random-name/package.json index a6d561d7b..08e96fff3 100644 --- a/packages/random-name/package.json +++ b/packages/random-name/package.json @@ -3,10 +3,11 @@ "version": "1.3.2", "description": "A small utility to generate a random name", "type": "module", - "main": "dist/module.js", - "module": "dist/module.js", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", "browser": { - "dist/module.js": "./dist/module.browser.js" + "dist/index.js": "./dist/index.browser.js" }, "publishConfig": { "access": "public" diff --git a/packages/regex/package.json b/packages/regex/package.json index 1fe1c16e9..5942ddc46 100644 --- a/packages/regex/package.json +++ b/packages/regex/package.json @@ -3,10 +3,11 @@ "version": "1.4.3", "description": "A small utility to use regex", "type": "module", - "main": "dist/module.js", - "module": "dist/module.js", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", "browser": { - "dist/module.js": "./dist/module.browser.js" + "dist/index.js": "./dist/index.browser.js" }, "publishConfig": { "access": "public" diff --git a/packages/use-dataloader/package.json b/packages/use-dataloader/package.json index d32f1f129..7cba6b05d 100644 --- a/packages/use-dataloader/package.json +++ b/packages/use-dataloader/package.json @@ -9,10 +9,11 @@ "dataloader" ], "type": "module", - "main": "dist/module.js", - "module": "dist/module.js", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", "browser": { - "dist/module.js": "./dist/module.browser.js" + "dist/index.js": "./dist/index.browser.js" }, "publishConfig": { "access": "public" diff --git a/packages/use-i18n/package.json b/packages/use-i18n/package.json index 990e00d2d..0d8faa776 100644 --- a/packages/use-i18n/package.json +++ b/packages/use-i18n/package.json @@ -11,10 +11,11 @@ "react-intl" ], "type": "module", - "main": "dist/module.js", - "module": "dist/module.js", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", "browser": { - "dist/module.js": "./dist/module.browser.js" + "dist/index.js": "./dist/index.browser.js" }, "publishConfig": { "access": "public" diff --git a/packages/use-query-params/package.json b/packages/use-query-params/package.json index 182746a54..d90f65af7 100644 --- a/packages/use-query-params/package.json +++ b/packages/use-query-params/package.json @@ -12,10 +12,11 @@ "query-params" ], "type": "module", - "main": "dist/module.js", - "module": "dist/module.js", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", "browser": { - "dist/module.js": "./dist/module.browser.js" + "dist/index.js": "./dist/index.browser.js" }, "publishConfig": { "access": "public"