Skip to content

Commit c8a0f88

Browse files
authored
feat!: convert react-interpolate to TypeScript (#55)
1 parent 495a153 commit c8a0f88

28 files changed

Lines changed: 2354 additions & 5284 deletions

.eslintrc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,25 @@
1818
"sourceType": "module"
1919
},
2020
"parser": "@babel/eslint-parser",
21-
"settings": { "react": { "version": "detect" } }
21+
"settings": {
22+
"react": { "version": "detect" },
23+
"import/extensions": [".js", ".jsx", ".ts", ".tsx"],
24+
"import/parsers": {
25+
"@babel/eslint-parser": [".js", ".jsx", ".ts", ".tsx"]
26+
},
27+
"import/resolver": {
28+
"node": {
29+
"extensions": [".js", ".jsx", ".ts", ".tsx"]
30+
}
31+
}
32+
},
33+
"overrides": [
34+
{
35+
"files": ["**/*.ts", "**/*.tsx"],
36+
"rules": {
37+
"no-undef": "off",
38+
"no-unused-vars": "off"
39+
}
40+
}
41+
]
2242
}
Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,46 @@
11
/* eslint-disable react/display-name */
2-
import React from 'react'
32
import { render } from '@testing-library/react'
4-
import Interpolate, { SYNTAX_I18NEXT } from '../src'
3+
import React from 'react'
4+
import Interpolate, { SYNTAX_I18NEXT, type InterpolateProps } from '../src'
55

6-
Interpolate.defaultProps = {
7-
graceful: false,
6+
afterEach(() => {
7+
jest.restoreAllMocks()
8+
})
9+
10+
const suppressConsole = () => {
11+
jest.spyOn(console, 'warn').mockImplementation(() => undefined)
12+
jest.spyOn(console, 'error').mockImplementation(() => undefined)
813
}
914

10-
const surpressConsole = () => {
11-
const w = jest.spyOn(console, 'warn').mockImplementation()
12-
const e = jest.spyOn(console, 'error').mockImplementation()
15+
type RenderSuccessProps = InterpolateProps & {
16+
expected: string
17+
}
1318

14-
return () => {
15-
w.mockRestore()
16-
e.mockRestore()
17-
}
19+
type RenderErrorProps = InterpolateProps & {
20+
expectedError: string
1821
}
1922

2023
describe('Interpolate', () => {
21-
function renderTest({ expected, ...props }) {
22-
const { container } = render(<Interpolate {...props} />)
24+
function renderTest({ expected, graceful = false, ...props }: RenderSuccessProps) {
25+
const { container } = render(<Interpolate {...props} graceful={graceful} />)
2326
expect(container.innerHTML).toEqual(expected)
2427
}
2528

2629
test('when no mapping is provide', () => {
27-
const restore = surpressConsole() // Interpolate will output warning when no mapping is provided
30+
suppressConsole()
2831

2932
renderTest({
3033
string: '<h1>hello <b>{name}</b></h1><br/>. welcome to todoist',
3134
expected: '<h1>hello <b>{name}</b></h1><br>. welcome to todoist',
3235
})
33-
34-
restore()
3536
})
3637

3738
test('tag mapping', () =>
3839
renderTest({
3940
string: '<h1>hello <b>steven</b></h1>. welcome to todoist',
4041
mapping: {
41-
b: (child) => <i>{child}</i>,
42-
h1: (child) => <h2>{child}</h2>,
42+
b: (children) => <i>{children}</i>,
43+
h1: (children) => <h2>{children}</h2>,
4344
},
4445
expected: '<h2>hello <i>steven</i></h2>. welcome to todoist',
4546
}))
@@ -77,25 +78,24 @@ describe('Interpolate', () => {
7778
renderTest({
7879
string: '<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist',
7980
mapping: {
80-
h1: (child) => <h2>{child}</h2>,
81-
b: (child) => <i>{child}</i>,
81+
h1: (children) => <h2>{children}</h2>,
82+
b: (children) => <i>{children}</i>,
8283
name: 'steven',
8384
br: <hr />,
8485
},
8586
expected: '<h2>hello <i>steven</i></h2>.<hr> welcome to todoist',
8687
}))
8788

8889
test('combination of mapping with function component', () => {
89-
// eslint-disable-next-line
90-
const Subheader = ({ children }) => {
90+
const Subheader = ({ children }: React.PropsWithChildren) => {
9191
return <h2 className="subheader">{children}</h2>
9292
}
9393

9494
renderTest({
9595
string: '<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist',
9696
mapping: {
97-
h1: (child) => <Subheader>{child}</Subheader>,
98-
b: (child) => <i>{child}</i>,
97+
h1: (children) => <Subheader>{children}</Subheader>,
98+
b: (children) => <i>{children}</i>,
9999
name: 'steven',
100100
br: <hr />,
101101
},
@@ -124,28 +124,26 @@ describe('Interpolate', () => {
124124
'hello &lt;script&gt;window.xss = 1&lt;/script&gt;&lt;script&gt;window.xss = 1&lt;/script&gt; welcome to todoist',
125125
})
126126

127-
expect(window.css).toBeUndefined()
127+
expect((window as typeof window & { xss?: number }).xss).toBeUndefined()
128128
})
129129

130130
test('when graceful flag is on and string contains syntax error, interpolate should return the original string and should not throw error', () => {
131-
const restore = surpressConsole()
131+
suppressConsole()
132132

133133
renderTest({
134134
string: '</h1>',
135135
expected: '&lt;/h1&gt;',
136136
graceful: true,
137137
})
138-
139-
restore()
140138
})
141139

142140
test('using SYNTAX_I18NEXT', () => {
143141
renderTest({
144142
syntax: SYNTAX_I18NEXT,
145143
string: '<0>hello <b>{{name}}</b></0>.<br/> welcome to todoist',
146144
mapping: {
147-
0: (child) => <h2 className="subheader">{child}</h2>,
148-
b: (child) => <i>{child}</i>,
145+
0: (children) => <h2 className="subheader">{children}</h2>,
146+
b: (children) => <i>{children}</i>,
149147
name: 'steven',
150148
br: <hr />,
151149
},
@@ -155,17 +153,15 @@ describe('Interpolate', () => {
155153
})
156154

157155
describe('Interpolate: error cases', () => {
158-
let restore
159-
beforeAll(() => {
160-
restore = surpressConsole()
161-
})
162-
afterAll(() => {
163-
restore()
156+
beforeEach(() => {
157+
suppressConsole()
164158
})
165159

166-
function renderTest({ expectedError, ...props }) {
160+
function renderTest({ expectedError, graceful = false, ...props }: RenderErrorProps) {
167161
return () => {
168-
expect(() => render(<Interpolate {...props} />)).toThrow(expectedError)
162+
expect(() => render(<Interpolate {...props} graceful={graceful} />)).toThrow(
163+
expectedError,
164+
)
169165
}
170166
}
171167

__test__/package-smoke.cjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const React = require('react')
2+
const { renderToStaticMarkup } = require('react-dom/server')
3+
4+
function assert(condition, message) {
5+
if (!condition) {
6+
throw new Error(message)
7+
}
8+
}
9+
10+
async function main() {
11+
const cjsPackage = require('..')
12+
const esmPackage = await import('../dist/react-interpolate.mjs')
13+
14+
assert(typeof cjsPackage.default === 'function', 'CJS default export should be a function')
15+
assert(typeof esmPackage.default === 'function', 'ESM default export should be a function')
16+
assert(cjsPackage.TOKEN_PLACEHOLDER === esmPackage.TOKEN_PLACEHOLDER, 'Named exports should match')
17+
18+
const html = renderToStaticMarkup(
19+
React.createElement(cjsPackage.default, {
20+
string: '<b>{name}</b>',
21+
mapping: {
22+
b: React.createElement('strong'),
23+
name: 'William',
24+
},
25+
}),
26+
)
27+
28+
assert(html === '<strong>William</strong>', 'Built package should render expected HTML')
29+
}
30+
31+
main().catch((error) => {
32+
console.error(error)
33+
process.exit(1)
34+
})

__test__/package-types.cts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pkg = require('@doist/react-interpolate')
2+
3+
import type { ReactNode } from 'react'
4+
5+
const mapping: pkg.Mapping = {
6+
b: (children?: ReactNode) => children ?? null,
7+
name: 'William',
8+
}
9+
10+
const props: pkg.InterpolateProps = {
11+
string: '<b>{{name}}</b>',
12+
syntax: pkg.SYNTAX_I18NEXT,
13+
mapping,
14+
graceful: true,
15+
}
16+
17+
pkg.default(props)
18+
pkg.TOKEN_PLACEHOLDER

__test__/package-types.mts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Interpolate, {
2+
SYNTAX_I18NEXT,
3+
TOKEN_PLACEHOLDER,
4+
type InterpolateProps,
5+
type Mapping,
6+
} from '@doist/react-interpolate'
7+
import type { ReactNode } from 'react'
8+
9+
const mapping: Mapping = {
10+
b: (children?: ReactNode) => children ?? null,
11+
name: 'William',
12+
}
13+
14+
const props: InterpolateProps = {
15+
string: '<b>{{name}}</b>',
16+
syntax: SYNTAX_I18NEXT,
17+
mapping,
18+
graceful: true,
19+
}
20+
21+
void Interpolate
22+
void TOKEN_PLACEHOLDER
23+
void props
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,16 @@ describe('parser: error handling', () => {
4141
})
4242
})
4343
})
44+
45+
describe('parser: custom syntax validation', () => {
46+
test('syntax rules must use global regexes', () => {
47+
expect(() => {
48+
parser('hello {name}', [
49+
{
50+
type: 'TOKEN_PLACEHOLDER',
51+
regex: /{\s*(\w+)\s*}/,
52+
},
53+
])
54+
}).toThrow('Syntax rule regex must use the global flag')
55+
})
56+
})

__test__/tsconfig.package-cjs.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"strict": true,
7+
"skipLibCheck": true,
8+
"baseUrl": "..",
9+
"noEmit": true,
10+
"paths": {
11+
"@doist/react-interpolate": ["./dist/index.d.ts"]
12+
},
13+
"types": ["node", "react"]
14+
},
15+
"files": ["package-types.cts"]
16+
}

__test__/tsconfig.package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"strict": true,
7+
"skipLibCheck": true,
8+
"baseUrl": "..",
9+
"noEmit": true,
10+
"paths": {
11+
"@doist/react-interpolate": ["./dist/index.d.ts"]
12+
},
13+
"types": ["node", "react"]
14+
},
15+
"files": ["package-types.mts"]
16+
}

babel.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = function (api) {
1212
},
1313
],
1414
'@babel/react',
15+
'@babel/preset-typescript',
1516
],
1617
plugins: ['@babel/plugin-transform-runtime'],
1718
}

0 commit comments

Comments
 (0)